From fb29ddab2029245edac4ceaa1e811ee1f9428e4c Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 24 Oct 2025 19:02:39 -0700 Subject: [PATCH 01/26] add scaffolding, and client authentication. --- go.mod | 1 + go.sum | 1 + v1/providers/sfcompute/capabilities.go | 24 +++++++++ v1/providers/sfcompute/client.go | 73 ++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 v1/providers/sfcompute/capabilities.go create mode 100644 v1/providers/sfcompute/client.go diff --git a/go.mod b/go.mod index 652a253..bae1143 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/stretchr/testify v1.11.0 golang.org/x/crypto v0.41.0 gopkg.in/validator.v2 v2.0.1 + github.com/sfcompute/nodes-go v0.1.0-alpha.3 ) require ( diff --git a/go.sum b/go.sum index 64dd129..02ac61b 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sfcompute/nodes-go v0.1.0-alpha.3/go.mod h1:dF3O8MCxLz3FTVYhjCa876Z9O3EAM8E8fONivDpfmkM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/v1/providers/sfcompute/capabilities.go b/v1/providers/sfcompute/capabilities.go new file mode 100644 index 0000000..ac0604a --- /dev/null +++ b/v1/providers/sfcompute/capabilities.go @@ -0,0 +1,24 @@ +package v1 + +import ( + "context" + + v1 "github.com/brevdev/cloud/v1" +) + +func getSFCCapabilities() v1.Capabilities { + return v1.Capabilities{ + v1.CapabilityCreateInstance, + v1.CapabilityTerminateInstance, + v1.CapabilityCreateTerminateInstance, + // add others supported by your provider: reboot, stop/start, machine-image, tags, resize-volume, modify-firewall, etc. + } +} + +func (c *SFCClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return getSFCCapabilities(), nil +} + +func (c *SFCCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return getSFCCapabilities(), nil +} diff --git a/v1/providers/sfcompute/client.go b/v1/providers/sfcompute/client.go new file mode 100644 index 0000000..d68e5aa --- /dev/null +++ b/v1/providers/sfcompute/client.go @@ -0,0 +1,73 @@ +package v1 + +import ( + "context" + + v1 "github.com/brevdev/cloud/v1" + "github.com/sfcompute/nodes-go/option" + + sfcnodes "github.com/sfcompute/nodes-go" +) + +type SFCCredential struct { + RefID string + apiKey string +} + +var _ v1.CloudCredential = &SFCCredential{} + +func NewSFCCredential(refID string, apiKey string /* auth fields */) *SFCCredential { + return &SFCCredential{ + RefID: refID, + apiKey: apiKey, + // ... + } +} + +func (c *SFCCredential) GetReferenceID() string { return c.RefID } +func (c *SFCCredential) GetAPIType() v1.APIType { return v1.APITypeLocational /* or v1.APITypeGlobal */ } +func (c *SFCCredential) GetCloudProviderID() v1.CloudProviderID { + return "sfcompute" // e.g., "lambdalabs" +} +func (c *SFCCredential) GetTenantID() (string, error) { + // sfc does not have a tenant system, return empty string + return "", nil +} + +func (c *SFCCredential) MakeClient(ctx context.Context, location string) (v1.CloudClient, error) { + // Create a client configured for a given location if locational API + return NewSFCClient(c.RefID, c.apiKey /* auth fields */).MakeClient(ctx, location) +} + +// ---------------- Client ---------------- + +type SFCClient struct { + v1.NotImplCloudClient + refID string + location string + apiKey string + client sfcnodes.Client // Add this field + // add http/sdk client fields, base URLs, etc. +} + +var _ v1.CloudClient = &SFCClient{} + +func NewSFCClient(refID string, apiKey string /* auth fields */) *SFCClient { + return &SFCClient{ + refID: refID, + apiKey: apiKey, + client: sfcnodes.NewClient( + option.WithBearerToken(apiKey)), + // init http/sdk clients here + } +} + +func (c *SFCClient) GetAPIType() v1.APIType { return v1.APITypeLocational /* or Global */ } +func (c *SFCClient) GetCloudProviderID() v1.CloudProviderID { return "sfcompute" } +func (c *SFCClient) GetReferenceID() string { return c.refID } +func (c *SFCClient) GetTenantID() (string, error) { return "", nil } + +func (c *SFCClient) MakeClient(_ context.Context, location string) (v1.CloudClient, error) { + c.location = location + return c, nil +} From 15549b723aa4ab5112364d77331a37598d3e9ec8 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Mon, 27 Oct 2025 15:19:38 -0700 Subject: [PATCH 02/26] return APITypeGlobal from GetAPIType function, as SFC accounts are not tied to specific regions. --- v1/providers/sfcompute/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/client.go b/v1/providers/sfcompute/client.go index d68e5aa..3b69a41 100644 --- a/v1/providers/sfcompute/client.go +++ b/v1/providers/sfcompute/client.go @@ -62,7 +62,7 @@ func NewSFCClient(refID string, apiKey string /* auth fields */) *SFCClient { } } -func (c *SFCClient) GetAPIType() v1.APIType { return v1.APITypeLocational /* or Global */ } +func (c *SFCClient) GetAPIType() v1.APIType { return v1.APITypeGlobal /* or Global */ } func (c *SFCClient) GetCloudProviderID() v1.CloudProviderID { return "sfcompute" } func (c *SFCClient) GetReferenceID() string { return c.refID } func (c *SFCClient) GetTenantID() (string, error) { return "", nil } From ce73d06d056fc01f139f8bc2452d7417c32bf322 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Mon, 27 Oct 2025 15:27:00 -0700 Subject: [PATCH 03/26] fix apiKey in SFCCredential struct --- v1/providers/sfcompute/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/client.go b/v1/providers/sfcompute/client.go index 3b69a41..fce08ca 100644 --- a/v1/providers/sfcompute/client.go +++ b/v1/providers/sfcompute/client.go @@ -11,7 +11,7 @@ import ( type SFCCredential struct { RefID string - apiKey string + apiKey string `json:"api_key"` } var _ v1.CloudCredential = &SFCCredential{} From 008158634b2966c8e0489f8952b4a499a5afb6fe Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Tue, 28 Oct 2025 16:20:14 -0700 Subject: [PATCH 04/26] scaffolding for instance.go --- v1/providers/sfcompute/instance.go | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 v1/providers/sfcompute/instance.go diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go new file mode 100644 index 0000000..639b961 --- /dev/null +++ b/v1/providers/sfcompute/instance.go @@ -0,0 +1,46 @@ +package v1 + +import ( + "context" + "fmt" + + v1 "github.com/brevdev/cloud/v1" +) + +func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { + // 1) ensure SSH key present (or inject via API) per ../docs/SECURITY.md + // 2) map attrs to provider request (location, instance type, image, tags, firewall rules if supported) + // 3) launch and return instance converted to v1.Instance + return nil, fmt.Errorf("not implemented") +} + +func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) { + return nil, fmt.Errorf("not implemented") +} + +func (c *SFCClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) { + return nil, fmt.Errorf("not implemented") +} + +func (c *SFCClient) TerminateInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { + return fmt.Errorf("not implemented") +} + +// Optional if supported: +func (c *SFCClient) RebootInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { + return fmt.Errorf("not implemented") +} +func (c *SFCClient) StopInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { + return fmt.Errorf("not implemented") +} +func (c *SFCClient) StartInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { + return fmt.Errorf("not implemented") +} + +// Merge strategies (pass-through is acceptable baseline). +func (c *SFCClient) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Instance) v1.Instance { + return newInst +} +func (c *SFCClient) MergeInstanceTypeForUpdate(_ v1.InstanceType, newIt v1.InstanceType) v1.InstanceType { + return newIt +} From 138d63ddb7c7ddffcc5ac6474f594456cb9c296a Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 31 Oct 2025 12:40:06 -0700 Subject: [PATCH 05/26] add instance creation implementation with SSH key support --- v1/providers/sfcompute/instance.go | 50 +++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 639b961..c83d230 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -2,16 +2,58 @@ package v1 import ( "context" + "encoding/base64" "fmt" + "time" v1 "github.com/brevdev/cloud/v1" + sfcnodes "github.com/sfcompute/nodes-go" + "github.com/sfcompute/nodes-go/packages/param" ) +// define function to convert string to b64 +func toBase64(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) +} + +// define function to add ssh key to cloud init +func sshKeyCloudInit(sshKey string) string { + return toBase64(fmt.Sprintf("#cloud-config\nssh_authorized_keys:\n - %s", sshKey)) +} + func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { - // 1) ensure SSH key present (or inject via API) per ../docs/SECURITY.md - // 2) map attrs to provider request (location, instance type, image, tags, firewall rules if supported) - // 3) launch and return instance converted to v1.Instance - return nil, fmt.Errorf("not implemented") + resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{ + CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ + DesiredCount: 1, + MaxPricePerNodeHour: 1000, + Zone: attrs.Location, + ImageID: param.Opt[string]{Value: attrs.ImageID}, //this needs to point to a valid image + CloudInitUserData: param.Opt[string]{Value: sshKeyCloudInit(attrs.PublicKey)}, // encode ssh key to b64-wrapped cloud-init script + }, + }) + if err != nil { + return nil, err + } + + if len(resp.Data) == 0 { + return nil, fmt.Errorf("no nodes returned") + } + node := resp.Data[0] + + inst := &v1.Instance{ + Name: attrs.Name, + RefID: attrs.RefID, + CloudCredRefID: c.refID, + CloudID: v1.CloudProviderInstanceID(node.ID), // SFC ID + ImageID: attrs.ImageID, + InstanceType: attrs.InstanceType, + Location: attrs.Location, + CreatedAt: time.Now(), + Status: v1.Status{LifecycleStatus: v1.LifecycleStatusPending}, // or map from SDK status + InstanceTypeID: v1.InstanceTypeID(node.GPUType), + } + + return inst, nil } func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) { From 30dbe43509a0157af527fe039af10f9d06ea5715 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Mon, 3 Nov 2025 15:44:48 -0800 Subject: [PATCH 06/26] add function to map the status of a node reported from SFC API to v1.LifecycleStatus in Brev --- v1/providers/sfcompute/instance.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index c83d230..8c1b09c 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "strings" "time" v1 "github.com/brevdev/cloud/v1" @@ -21,6 +22,25 @@ func sshKeyCloudInit(sshKey string) string { return toBase64(fmt.Sprintf("#cloud-config\nssh_authorized_keys:\n - %s", sshKey)) } +func mapSFCStatus(s string) v1.LifecycleStatus { + switch strings.ToLower(s) { + case "pending", "nodefailure", "unspecified", "awaitingcapacity", "unknown", "failed": + return v1.LifecycleStatusPending + case "running": + return v1.LifecycleStatusRunning + // case "stopping": + //return v1.LifecycleStatusStopping + case "stopped": + return v1.LifecycleStatusStopped + case "terminating", "released": + return v1.LifecycleStatusTerminating + case "destroyed", "deleted": + return v1.LifecycleStatusTerminated + default: + return v1.LifecycleStatusPending + } +} + func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ @@ -49,7 +69,7 @@ func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceA InstanceType: attrs.InstanceType, Location: attrs.Location, CreatedAt: time.Now(), - Status: v1.Status{LifecycleStatus: v1.LifecycleStatusPending}, // or map from SDK status + Status: v1.Status{LifecycleStatus: mapSFCStatus(fmt.Sprint(node.Status))}, // map SDK status to our lifecycle InstanceTypeID: v1.InstanceTypeID(node.GPUType), } From 778871070ea59ba366140d81712e591f1f053a4c Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Tue, 4 Nov 2025 16:03:43 -0800 Subject: [PATCH 07/26] implement GetInstance in sfcompute with node data retrieval inluding SSH Hostname / Public IP --- v1/providers/sfcompute/instance.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 8c1b09c..d6b1be9 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -77,7 +77,27 @@ func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceA } func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) { - return nil, fmt.Errorf("not implemented") + node, err := c.client.Nodes.Get(ctx, string(id)) + if err != nil { + panic(err.Error()) + } + + ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: string(id)}) + if err != nil { + panic(err.Error()) + } + + inst := &v1.Instance{ + Name: node.Name, + RefID: c.refID, + CloudCredRefID: c.refID, + CloudID: v1.CloudProviderInstanceID(node.ID), // SFC ID + PublicIP: ssh.SSHHostname, + CreatedAt: time.Unix(node.CreatedAt, 0), + Status: v1.Status{LifecycleStatus: mapSFCStatus(fmt.Sprint(node.Status))}, // map SDK status to our lifecycle + InstanceTypeID: v1.InstanceTypeID(node.GPUType), + } + return inst, nil } func (c *SFCClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) { From 60ef71977ae162e026b8d0971ce846484292eb80 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 6 Nov 2025 14:07:39 -0800 Subject: [PATCH 08/26] add TerminateInstance implementation with node release and delete logic --- v1/providers/sfcompute/instance.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index d6b1be9..2d1652b 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -105,7 +105,17 @@ func (c *SFCClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs } func (c *SFCClient) TerminateInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { - return fmt.Errorf("not implemented") + // release the node first + _, errRelease := c.client.Nodes.Release(ctx, string(id)) + if errRelease != nil { + panic(errRelease.Error()) + } + // then delete the node + errDelete := c.client.Nodes.Delete(ctx, string(id)) + if errDelete != nil { + panic(errDelete.Error()) + } + return nil } // Optional if supported: From 692ccf8578174e5f77c0260f79af403a9388b8ec Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 6 Nov 2025 16:38:19 -0800 Subject: [PATCH 09/26] set default SSH port to 2222, as is standard for our platform --- v1/providers/sfcompute/instance.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 2d1652b..097a4dc 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -71,6 +71,7 @@ func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceA CreatedAt: time.Now(), Status: v1.Status{LifecycleStatus: mapSFCStatus(fmt.Sprint(node.Status))}, // map SDK status to our lifecycle InstanceTypeID: v1.InstanceTypeID(node.GPUType), + SSHPort: 2222, // we use 2222/tcp for all of our SSH ports } return inst, nil From 67f3ca425321cd0bdb3823e74f5cb68197996e5b Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 6 Nov 2025 16:53:58 -0800 Subject: [PATCH 10/26] implement GetSSHHostname for retrieving the SSH hostname of an instance in sfcompute --- v1/providers/sfcompute/instance.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 097a4dc..5c9cd9a 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -41,6 +41,18 @@ func mapSFCStatus(s string) v1.LifecycleStatus { } } +func (c *SFCClient) GetSSHHostname(ctx context.Context, id v1.CloudProviderInstanceID) (string, error) { + _, err := c.client.Nodes.Get(ctx, string(id)) + if err != nil { + panic(err.Error()) + } + ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: string(id)}) + if err != nil { + panic(err.Error()) + } + return ssh.SSHHostname, nil +} + func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ From be27295f8cdbeadcbb5a5257768807714c5a4fed Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 6 Nov 2025 21:39:25 -0800 Subject: [PATCH 11/26] remove unneeded call to api for GetSSHHostname --- v1/providers/sfcompute/instance.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 5c9cd9a..ff147b8 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -42,10 +42,6 @@ func mapSFCStatus(s string) v1.LifecycleStatus { } func (c *SFCClient) GetSSHHostname(ctx context.Context, id v1.CloudProviderInstanceID) (string, error) { - _, err := c.client.Nodes.Get(ctx, string(id)) - if err != nil { - panic(err.Error()) - } ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: string(id)}) if err != nil { panic(err.Error()) From a556e6b04a201306cddf19d1bc1773a7dcbf2e57 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 7 Nov 2025 10:39:41 -0800 Subject: [PATCH 12/26] use VM ID instead of instance ID to retrieve SSH Hostname --- v1/providers/sfcompute/instance.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index ff147b8..ced0141 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -90,8 +90,13 @@ func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstance if err != nil { panic(err.Error()) } + var vmID string + if len(node.VMs.Data) > 0 { + vmID = node.VMs.Data[0].ID + fmt.Println(vmID) + } - ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: string(id)}) + ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: vmID}) if err != nil { panic(err.Error()) } From d6f3729a6bb18d7d359778a2b97c82d91bb57771 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 7 Nov 2025 10:54:12 -0800 Subject: [PATCH 13/26] remove get ssh hostname function --- v1/providers/sfcompute/instance.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index ced0141..b18a642 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -41,14 +41,6 @@ func mapSFCStatus(s string) v1.LifecycleStatus { } } -func (c *SFCClient) GetSSHHostname(ctx context.Context, id v1.CloudProviderInstanceID) (string, error) { - ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: string(id)}) - if err != nil { - panic(err.Error()) - } - return ssh.SSHHostname, nil -} - func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ From 42771b906eab1fbf7f0852a2d3138d8f178f03c7 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 7 Nov 2025 13:16:44 -0800 Subject: [PATCH 14/26] implement ListInstances --- v1/providers/sfcompute/instance.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index b18a642..4cc8f7b 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -107,7 +107,22 @@ func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstance } func (c *SFCClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) { - return nil, fmt.Errorf("not implemented") + resp, err := c.client.Nodes.List(ctx, sfcnodes.NodeListParams{}) + if err != nil { + return nil, err + } + + var instances []v1.Instance + for _, node := range resp.Data { + inst, err := c.GetInstance(ctx, v1.CloudProviderInstanceID(node.ID)) + if err != nil { + return nil, err + } + if inst != nil { + instances = append(instances, *inst) + } + } + return instances, nil } func (c *SFCClient) TerminateInstance(ctx context.Context, id v1.CloudProviderInstanceID) error { From edd9918a99f9b50973f2b848fe4f21a518f05e1b Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 21 Nov 2025 09:57:39 -0800 Subject: [PATCH 15/26] add validation test for sfcompute with API key check and skip logic --- go.mod | 7 ++++- go.sum | 11 ++++++++ v1/providers/sfcompute/validation_test.go | 33 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 v1/providers/sfcompute/validation_test.go diff --git a/go.mod b/go.mod index bae1143..75fb996 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,11 @@ require ( github.com/jarcoal/httpmock v1.4.0 github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de github.com/pkg/errors v0.9.1 + github.com/sfcompute/nodes-go v0.1.0-alpha.3 github.com/stretchr/testify v1.11.0 golang.org/x/crypto v0.41.0 gopkg.in/validator.v2 v2.0.1 - github.com/sfcompute/nodes-go v0.1.0-alpha.3 +//github.com/sfcompute/brevdev-cloud 0.0.1-testing ) require ( @@ -32,6 +33,10 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index 02ac61b..2c40901 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sfcompute/nodes-go v0.1.0-alpha.3 h1:DE4/P1TR+AQlXdQ8Sp12XqVXwDl55IG99V5NatPHyR4= github.com/sfcompute/nodes-go v0.1.0-alpha.3/go.mod h1:dF3O8MCxLz3FTVYhjCa876Z9O3EAM8E8fONivDpfmkM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -69,6 +70,16 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= diff --git a/v1/providers/sfcompute/validation_test.go b/v1/providers/sfcompute/validation_test.go new file mode 100644 index 0000000..b81990e --- /dev/null +++ b/v1/providers/sfcompute/validation_test.go @@ -0,0 +1,33 @@ +package v1 + +import ( + "os" + "testing" + + "github.com/brevdev/cloud/internal/validation" +) + +func TestValidationFunctions(t *testing.T) { + checkSkip(t) + apiKey := getAPIKey() + + config := validation.ProviderConfig{ + Credential: NewSFCCredential("validation-test", apiKey), + } + + validation.RunValidationSuite(t, config) +} + +func checkSkip(t *testing.T) { + apiKey := getAPIKey() + isValidation := os.Getenv("VALIDATION_TEST") + if apiKey == "" && isValidation != "" { + t.Fatal("SFCOMPUTE_API_KEY not set, but VALIDATION_TEST is set") + } else if apiKey == "" && isValidation == "" { + t.Skip("SFCOMPUTE_API_KEY not set, skipping sfcompute validation tests") + } +} + +func getAPIKey() string { + return os.Getenv("SFCOMPUTE_API_KEY") +} From a645611776812c5f91abc05e8ebe1ea5e3bad73e Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 21 Nov 2025 14:58:59 -0800 Subject: [PATCH 16/26] add getInstanceTypeID method for generating instance type IDs in sfcompute --- v1/providers/sfcompute/instancetype.go | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 v1/providers/sfcompute/instancetype.go diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go new file mode 100644 index 0000000..dfa2589 --- /dev/null +++ b/v1/providers/sfcompute/instancetype.go @@ -0,0 +1,9 @@ +package v1 + +import ( + "fmt" +) + +func (c *SFCClient) getInstanceTypeID(region string) string { + return fmt.Sprintf("h100v_%v", region) +} From afc47d734dbfadaa8517b95f26746587ecf85b07 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Mon, 1 Dec 2025 14:37:44 -0800 Subject: [PATCH 17/26] bump sfcnodes version to v0.1.0-alpha.4 which adds support for the /v0/zones endpoint --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7c7cd80..2d85bc3 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/text v0.29.0 google.golang.org/grpc v1.75.0 gopkg.in/validator.v2 v2.0.1 - github.com/sfcompute/nodes-go v0.1.0-alpha.3 + github.com/sfcompute/nodes-go v0.1.0-alpha.4 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 diff --git a/go.sum b/go.sum index cf59e5b..be1bbda 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,8 @@ github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUO github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sfcompute/nodes-go v0.1.0-alpha.3/go.mod h1:dF3O8MCxLz3FTVYhjCa876Z9O3EAM8E8fONivDpfmkM= +github.com/sfcompute/nodes-go v0.1.0-alpha.4 h1:oFBWcMPSpqLYm/NDs5I1jTvzgx9rsXDL9Ghsm30Hc0Q= +github.com/sfcompute/nodes-go v0.1.0-alpha.4/go.mod h1:nUviHgK+Fgt2hDFcRL3M8VoyiypC8fc0dsY8C30QU8M= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= From cdd39b20093282de310725b8597fb6ed56122102 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Wed, 3 Dec 2025 16:28:44 -0800 Subject: [PATCH 18/26] implement GetLocations --- v1/providers/sfcompute/instancetype.go | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index dfa2589..65d97c1 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -1,9 +1,37 @@ package v1 import ( + "context" "fmt" + + v1 "github.com/brevdev/cloud/v1" ) func (c *SFCClient) getInstanceTypeID(region string) string { return fmt.Sprintf("h100v_%v", region) } + +func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { + resp, err := c.client.Zones.List(ctx) + if err != nil { + return nil, err + } + var locations map[string]v1.Location + if resp != nil { + for _, zone := range resp.Data { + var available = false + if len(zone.AvailableCapacity) != 0 && zone.DeliveryType == "VM" { + available = true + locations[zone.Name] = v1.Location{ + Name: zone.Name, + Description: string(zone.HardwareType), + Available: available} + } + } + } + availableLocations := []v1.Location{} + for _, location := range locations { + availableLocations = append(availableLocations, location) + } + return availableLocations, nil +} From 1e26a8d472dc9554c4a2e84f1e86e184637d12f8 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 4 Dec 2025 15:44:37 -0800 Subject: [PATCH 19/26] only return approved zones --- v1/providers/sfcompute/instancetype.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index 65d97c1..d530d86 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -3,6 +3,7 @@ package v1 import ( "context" "fmt" + "slices" v1 "github.com/brevdev/cloud/v1" ) @@ -17,10 +18,11 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] return nil, err } var locations map[string]v1.Location + allowedZones := []string{"hayesvalley"} if resp != nil { for _, zone := range resp.Data { var available = false - if len(zone.AvailableCapacity) != 0 && zone.DeliveryType == "VM" { + if len(zone.AvailableCapacity) != 0 && zone.DeliveryType == "VM" && slices.Contains(allowedZones, zone.Name) == true { available = true locations[zone.Name] = v1.Location{ Name: zone.Name, From ac9caa0720f0851e535194a69ab05732b3652032 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 4 Dec 2025 15:51:43 -0800 Subject: [PATCH 20/26] only return regions that have more than zero capacity instead of any zones that have capacity not equal to zero, in case the availability ever returns a negative number --- v1/providers/sfcompute/instancetype.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index d530d86..0a0af5a 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -22,7 +22,7 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] if resp != nil { for _, zone := range resp.Data { var available = false - if len(zone.AvailableCapacity) != 0 && zone.DeliveryType == "VM" && slices.Contains(allowedZones, zone.Name) == true { + if len(zone.AvailableCapacity) > 0 && zone.DeliveryType == "VM" && slices.Contains(allowedZones, zone.Name) == true { available = true locations[zone.Name] = v1.Location{ Name: zone.Name, From 818df8cebf603c06412dc5a003271a0ba508ba6c Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 11 Dec 2025 13:17:52 -0800 Subject: [PATCH 21/26] update location description to include formatted hardware type information. example: `sfc_hayesvalley_h100` --- v1/providers/sfcompute/instancetype.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index 0a0af5a..a370675 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -26,7 +26,7 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] available = true locations[zone.Name] = v1.Location{ Name: zone.Name, - Description: string(zone.HardwareType), + Description: fmt.Sprintf("sfc_%s_%s", zone.Name, string(zone.HardwareType)), Available: available} } } From f130bf401f971dbac6ed11989faea8b4de2e26e2 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Fri, 12 Dec 2025 15:54:31 -0800 Subject: [PATCH 22/26] return unavailable regions with v1.Location{Available: false} --- v1/providers/sfcompute/instancetype.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index a370675..860ded0 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -28,6 +28,12 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] Name: zone.Name, Description: fmt.Sprintf("sfc_%s_%s", zone.Name, string(zone.HardwareType)), Available: available} + } else { + available = false + locations[zone.Name] = v1.Location{ + Name: zone.Name, + Description: fmt.Sprintf("sfc_%s_%s", zone.Name, string(zone.HardwareType)), + Available: available} } } } From 899d9b50348e15dd87e15a94870d63b01e1918dd Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Mon, 5 Jan 2026 16:37:47 -0500 Subject: [PATCH 23/26] fix an error where a nil map was returned --- go.mod | 6 +++++- go.sum | 10 ++++++++++ v1/providers/sfcompute/instancetype.go | 4 ++-- v1/providers/sfcompute/validation_test.go | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 2d85bc3..2c47a49 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,12 @@ require ( github.com/jarcoal/httpmock v1.4.0 github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de github.com/pkg/errors v0.9.1 + github.com/sfcompute/nodes-go v0.1.0-alpha.4 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.42.0 golang.org/x/text v0.29.0 google.golang.org/grpc v1.75.0 gopkg.in/validator.v2 v2.0.1 - github.com/sfcompute/nodes-go v0.1.0-alpha.4 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 @@ -84,6 +84,10 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index be1bbda..7727c30 100644 --- a/go.sum +++ b/go.sum @@ -183,6 +183,16 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index 860ded0..e342dc1 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -17,7 +17,7 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] if err != nil { return nil, err } - var locations map[string]v1.Location + locations := make(map[string]v1.Location) allowedZones := []string{"hayesvalley"} if resp != nil { for _, zone := range resp.Data { @@ -33,7 +33,7 @@ func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([] locations[zone.Name] = v1.Location{ Name: zone.Name, Description: fmt.Sprintf("sfc_%s_%s", zone.Name, string(zone.HardwareType)), - Available: available} + Available: false} } } } diff --git a/v1/providers/sfcompute/validation_test.go b/v1/providers/sfcompute/validation_test.go index b81990e..5ae07d4 100644 --- a/v1/providers/sfcompute/validation_test.go +++ b/v1/providers/sfcompute/validation_test.go @@ -21,9 +21,9 @@ func TestValidationFunctions(t *testing.T) { func checkSkip(t *testing.T) { apiKey := getAPIKey() isValidation := os.Getenv("VALIDATION_TEST") - if apiKey == "" && isValidation != "" { + if apiKey == "" && isValidation != "true" { t.Fatal("SFCOMPUTE_API_KEY not set, but VALIDATION_TEST is set") - } else if apiKey == "" && isValidation == "" { + } else if apiKey == "" && isValidation == "false" { t.Skip("SFCOMPUTE_API_KEY not set, skipping sfcompute validation tests") } } From e6ebe506071d08675b98211e7a78c9e2a08827aa Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 22 Jan 2026 15:35:30 -0800 Subject: [PATCH 24/26] start implementing GetInstanceTypes --- v1/providers/sfcompute/instancetype.go | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index e342dc1..9fa53c7 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -3,7 +3,9 @@ package v1 import ( "context" "fmt" + "github.com/brevdev/cloud/internal/collections" "slices" + "time" v1 "github.com/brevdev/cloud/v1" ) @@ -12,6 +14,40 @@ func (c *SFCClient) getInstanceTypeID(region string) string { return fmt.Sprintf("h100v_%v", region) } +func (c *SFCClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { + resp, err := c.client.Zones.List(ctx) + if err != nil { + return nil, err + } + types := make([]v1.InstanceType, 0) + for _, zone := range resp.Data { + var available = false + if len(zone.AvailableCapacity) > 0 && zone.DeliveryType == "VM" { + available = true + } + + types = append(types, v1.InstanceType{ + ID: v1.InstanceTypeID(c.getInstanceTypeID(zone.Name)), + IsAvailable: available, + Type: "h100v", + Location: zone.Name, + Stoppable: false, + Rebootable: false, + IsContainer: false, + EstimatedDeployTime: collections.Ptr(time.Duration(15 * time.Minute)), + SupportedGPUs: []v1.GPU{{ + Count: 8, + Type: "h100v", + Manufacturer: "nvidia", + Name: "h100v", + MemoryBytes: v1.NewBytes(80, v1.Gibibyte), + }}, + }) + + } + return types, nil +} + func (c *SFCClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { resp, err := c.client.Zones.List(ctx) if err != nil { From cf1b4001e27e035112b7e6ed8c940c74b2921605 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 22 Jan 2026 17:55:04 -0800 Subject: [PATCH 25/26] fix tests failing due to ValidateRegionalInstanceTypes and ValidateStableInstanceTypeIDs fails errors --- v1/providers/sfcompute/instancetype.go | 22 +++++++++++++++++++++- v1/providers/sfcompute/validation_test.go | 4 ++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instancetype.go b/v1/providers/sfcompute/instancetype.go index 9fa53c7..733a8b9 100644 --- a/v1/providers/sfcompute/instancetype.go +++ b/v1/providers/sfcompute/instancetype.go @@ -3,10 +3,13 @@ package v1 import ( "context" "fmt" - "github.com/brevdev/cloud/internal/collections" "slices" + "strconv" "time" + "github.com/bojanz/currency" + "github.com/brevdev/cloud/internal/collections" + v1 "github.com/brevdev/cloud/v1" ) @@ -19,13 +22,18 @@ func (c *SFCClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTyp if err != nil { return nil, err } + types := make([]v1.InstanceType, 0) for _, zone := range resp.Data { + if len(args.Locations) > 0 && !args.Locations.IsAllowed(zone.Name) { + continue + } var available = false if len(zone.AvailableCapacity) > 0 && zone.DeliveryType == "VM" { available = true } + price, _ := currency.NewAmount(strconv.Itoa(2), "USD") types = append(types, v1.InstanceType{ ID: v1.InstanceTypeID(c.getInstanceTypeID(zone.Name)), IsAvailable: available, @@ -34,6 +42,7 @@ func (c *SFCClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTyp Stoppable: false, Rebootable: false, IsContainer: false, + BasePrice: &price, EstimatedDeployTime: collections.Ptr(time.Duration(15 * time.Minute)), SupportedGPUs: []v1.GPU{{ Count: 8, @@ -45,6 +54,17 @@ func (c *SFCClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTyp }) } + + if len(args.InstanceTypes) > 0 { + filteredTypes := make([]v1.InstanceType, 0) + for _, t := range types { + if slices.Contains(args.InstanceTypes, t.Type) { + filteredTypes = append(filteredTypes, t) + } + } + return filteredTypes, nil + } + return types, nil } diff --git a/v1/providers/sfcompute/validation_test.go b/v1/providers/sfcompute/validation_test.go index 5ae07d4..1785e2c 100644 --- a/v1/providers/sfcompute/validation_test.go +++ b/v1/providers/sfcompute/validation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/brevdev/cloud/internal/validation" + v1 "github.com/brevdev/cloud/v1" ) func TestValidationFunctions(t *testing.T) { @@ -13,6 +14,9 @@ func TestValidationFunctions(t *testing.T) { config := validation.ProviderConfig{ Credential: NewSFCCredential("validation-test", apiKey), + StableIDs: []v1.InstanceTypeID{ + "h100v_hayesvalley", + }, } validation.RunValidationSuite(t, config) From d3b8abafca80241403804b9cec65e35c3f9421d8 Mon Sep 17 00:00:00 2001 From: Brian Lechthaler Date: Thu, 22 Jan 2026 21:41:52 -0800 Subject: [PATCH 26/26] set MaxPricePerNodePerHour to 1600 ($16/node/h, $2/gpu/h) --- v1/providers/sfcompute/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/sfcompute/instance.go b/v1/providers/sfcompute/instance.go index 4cc8f7b..4658eee 100644 --- a/v1/providers/sfcompute/instance.go +++ b/v1/providers/sfcompute/instance.go @@ -45,7 +45,7 @@ func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceA resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ DesiredCount: 1, - MaxPricePerNodeHour: 1000, + MaxPricePerNodeHour: 1600, Zone: attrs.Location, ImageID: param.Opt[string]{Value: attrs.ImageID}, //this needs to point to a valid image CloudInitUserData: param.Opt[string]{Value: sshKeyCloudInit(attrs.PublicKey)}, // encode ssh key to b64-wrapped cloud-init script