Skip to content

Commit 12ac7f9

Browse files
drewmalinandreaanezclaude
authored
feat: add SFC V2 cloud provider integration (#116) (#119)
* feat: add SFC V2 cloud provider integration (#116) * feat: add SFC V2 cloud provider integration Introduces the sfcomputev2 provider package that talks to the V2 SFC API via github.com/sfcompute/sfc-go. Uses capacity-based slot tracking to report availability and native tags for instance metadata instead of name encoding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sfcomputev2): consolidate Brev-specific constants into brev_constants.go Moves tag keys, SSH defaults, and adds production capacity/image IDs to a dedicated file to separate Brev-specific config from generated SDK usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(sfcomputev2): clean up provider for correctness and consistency - Store user tags from attrs.Tags as real SFC V2 instance tags; populate v1.Instance.Tags on read, filtering internal Brev metadata keys - Remove CapacityID/ImageID from SFCCredentialV2 — pull from constants instead, with a TODO to source from env vars - Replace procurementTarget (Procurements.List) with currentCapacityAllocation (Capacities.Fetch + AllocationSchedule.Total) for available slot counting - Count all non-terminated instances (including failed) against capacity - Fix sfcInstanceToBrevInstance to use sfcLocation constant instead of c.location - Add CapabilityTags to declared capabilities - Remove legacy sfcNameToBrevData fallback logic (V1 and V2 are mutually exclusive) - Remove speculative stageTesting/stageProduction normalization; pass stage through raw - Add validation_test.go following the same pattern as the V1 provider Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: swap sfc-go local replace for tagged release v0.1.0-preview Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * use v2 sfc * use capacity ID * update tests to use env var * add sfcompute_workspace envvar * organization * lint * bump version and consume fixes * stable capacity, no cron for sfc * merge deedee branch * fmt * fetch ssh details dynamically * no parallel to avoid conflicting capacity test * use v2 in provider id * back to sfcompute --------- Co-authored-by: Andrea <andrea@sfcompute.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b2a8416 commit 12ac7f9

10 files changed

Lines changed: 778 additions & 6 deletions

File tree

.github/workflows/validation-sfcompute.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
name: SFCompute Validation Tests
22

33
on:
4-
schedule:
5-
# Run daily at 2 AM UTC
6-
- cron: '0 2 * * *'
74
workflow_dispatch:
85
# Allow manual triggering
96
pull_request:
107
paths:
11-
- 'v1/providers/sfcompute/**'
8+
- 'v1/providers/sfcomputev2/**'
129
- 'internal/validation/**'
1310
- 'v1/**'
1411
branches: [ main ]
@@ -45,8 +42,11 @@ jobs:
4542
SFCOMPUTE_API_KEY: ${{ secrets.SFCOMPUTE_API_KEY }}
4643
TEST_PRIVATE_KEY_BASE64: ${{ secrets.TEST_PRIVATE_KEY_BASE64 }}
4744
TEST_PUBLIC_KEY_BASE64: ${{ secrets.TEST_PUBLIC_KEY_BASE64 }}
45+
SFCOMPUTE_ORGANIZATION: ${{ secrets.SFCOMPUTE_ORGANIZATION }}
46+
SFCOMPUTE_WORKSPACE: ${{ secrets.SFCOMPUTE_WORKSPACE }}
47+
VALIDATION_TEST: true
4848
run: |
49-
cd v1/providers/sfcompute
49+
cd v1/providers/sfcomputev2
5050
go test -v -short=false -timeout=30m ./...
5151
5252
- name: Upload test results
@@ -55,4 +55,4 @@ jobs:
5555
with:
5656
name: sfcompute-validation-results
5757
path: |
58-
v1/providers/sfcompute/coverage.out
58+
v1/providers/sfcomputev2/coverage.out

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/nebius/gosdk v0.0.0-20250826102719-940ad1dfb5de
2323
github.com/pkg/errors v0.9.1
2424
github.com/sfcompute/nodes-go v0.1.0-alpha.4
25+
github.com/sfcompute/sfc-go v0.1.0-preview.2
2526
github.com/stretchr/testify v1.11.1
2627
golang.org/x/crypto v0.52.0
2728
golang.org/x/text v0.37.0
@@ -85,6 +86,7 @@ require (
8586
github.com/sirupsen/logrus v1.9.3 // indirect
8687
github.com/spf13/afero v1.15.0 // indirect
8788
github.com/spf13/pflag v1.0.10 // indirect
89+
github.com/spyzhov/ajson v0.8.0 // indirect
8890
github.com/tidwall/gjson v1.18.0 // indirect
8991
github.com/tidwall/match v1.1.1 // indirect
9092
github.com/tidwall/pretty v1.2.1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,16 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
162162
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
163163
github.com/sfcompute/nodes-go v0.1.0-alpha.4 h1:oFBWcMPSpqLYm/NDs5I1jTvzgx9rsXDL9Ghsm30Hc0Q=
164164
github.com/sfcompute/nodes-go v0.1.0-alpha.4/go.mod h1:nUviHgK+Fgt2hDFcRL3M8VoyiypC8fc0dsY8C30QU8M=
165+
github.com/sfcompute/sfc-go v0.1.0-preview.2 h1:IzLovv15bnfh+nwgPK1/rKidCNome9rq06j0vnrzKf0=
166+
github.com/sfcompute/sfc-go v0.1.0-preview.2/go.mod h1:vhUpRpAHKitZzzWPg87RjreC+pzK57PGe4ZuSIQSk94=
165167
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
166168
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
167169
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
168170
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
169171
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
170172
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
173+
github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc=
174+
github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
171175
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
172176
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
173177
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

v1/instance.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ func makeDebuggableName(name string) (string, error) {
166166
if err != nil {
167167
return "", err
168168
}
169+
if name == "" {
170+
name = "test"
171+
}
169172
return fmt.Sprintf("%s-%s", name, time.Now().In(pt).Format("2006-01-02-15-04-05")), nil
170173
}
171174

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package v2
2+
3+
import "fmt"
4+
5+
// Package-internal constants — SSH defaults and internal tag keys.
6+
const (
7+
defaultSSHUsername = "ubuntu"
8+
9+
// Internal tag keys written to every SFCompute V2 instance. These are stripped from
10+
// v1.Instance.Tags on read so they don't surface as user-facing tags.
11+
tagKeyCloudCredRefID = "brev-cloud-cred-ref-id" //nolint:gosec // not a secret
12+
tagKeyRefID = "brev-ref-id"
13+
14+
// Brev environment config for SFCompute V2.
15+
brevDefaultImageResourcePath = "sfc:image:sfcompute:public:ubuntu-24.04.4-cuda-12.8"
16+
)
17+
18+
func (c *SFCClientV2) GetDefaultCapacityResourcePath() string {
19+
return fmt.Sprintf("sfc:capacity:%s:%s:default", c.organization, c.workspace)
20+
}
21+
22+
func (c *SFCClientV2) GetWorkspaceResourcePath() string {
23+
return fmt.Sprintf("sfc:workspace:%s:%s", c.organization, c.workspace)
24+
}
25+
26+
func (c *SFCClientV2) GetDefaultImageResourcePath() string {
27+
return brevDefaultImageResourcePath
28+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
6+
v1 "github.com/brevdev/cloud/v1"
7+
)
8+
9+
func getSFCCapabilitiesV2() v1.Capabilities {
10+
return v1.Capabilities{
11+
v1.CapabilityCreateInstance,
12+
v1.CapabilityTerminateInstance,
13+
v1.CapabilityCreateTerminateInstance,
14+
v1.CapabilityTags,
15+
}
16+
}
17+
18+
func (c *SFCClientV2) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
19+
return getSFCCapabilitiesV2(), nil
20+
}
21+
22+
func (c *SFCCredentialV2) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
23+
return getSFCCapabilitiesV2(), nil
24+
}

v1/providers/sfcomputev2/client.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
6+
v1 "github.com/brevdev/cloud/v1"
7+
sfc "github.com/sfcompute/sfc-go"
8+
)
9+
10+
const CloudProviderID = "sfcompute"
11+
12+
// SFCCredentialV2 holds authentication details for a Brev-managed SFCompute V2 account.
13+
type SFCCredentialV2 struct {
14+
RefID string
15+
APIKey string `json:"api_key"`
16+
Organization string `json:"organization"`
17+
Workspace string `json:"workspace"`
18+
}
19+
20+
var _ v1.CloudCredential = &SFCCredentialV2{}
21+
22+
func NewSFCCredentialV2(refID string, apiKey string, organization string, workspace string) *SFCCredentialV2 {
23+
return &SFCCredentialV2{
24+
RefID: refID,
25+
APIKey: apiKey,
26+
Organization: organization,
27+
Workspace: workspace,
28+
}
29+
}
30+
31+
func (c *SFCCredentialV2) GetReferenceID() string {
32+
return c.RefID
33+
}
34+
35+
func (c *SFCCredentialV2) GetAPIType() v1.APIType {
36+
return v1.APITypeGlobal
37+
}
38+
39+
func (c *SFCCredentialV2) GetCloudProviderID() v1.CloudProviderID {
40+
return CloudProviderID
41+
}
42+
43+
func (c *SFCCredentialV2) GetTenantID() (string, error) {
44+
return "", nil
45+
}
46+
47+
type SFCClientV2 struct {
48+
v1.NotImplCloudClient
49+
refID string
50+
organization string
51+
workspace string
52+
location string
53+
client *sfc.SDK
54+
logger v1.Logger
55+
}
56+
57+
var _ v1.CloudClient = &SFCClientV2{}
58+
59+
type SFCClientV2Option func(c *SFCClientV2)
60+
61+
func WithLogger(logger v1.Logger) SFCClientV2Option {
62+
return func(c *SFCClientV2) {
63+
c.logger = logger
64+
}
65+
}
66+
67+
func (c *SFCCredentialV2) MakeClientWithOptions(_ context.Context, location string, opts ...SFCClientV2Option) (v1.CloudClient, error) {
68+
sfcClient := &SFCClientV2{
69+
refID: c.RefID,
70+
organization: c.Organization,
71+
workspace: c.Workspace,
72+
location: location,
73+
client: sfc.New(sfc.WithSecurity(c.APIKey)),
74+
logger: &v1.NoopLogger{},
75+
}
76+
77+
for _, opt := range opts {
78+
opt(sfcClient)
79+
}
80+
81+
return sfcClient, nil
82+
}
83+
84+
func (c *SFCCredentialV2) MakeClient(ctx context.Context, location string) (v1.CloudClient, error) {
85+
return c.MakeClientWithOptions(ctx, location)
86+
}
87+
88+
func (c *SFCClientV2) GetAPIType() v1.APIType {
89+
return v1.APITypeGlobal
90+
}
91+
92+
func (c *SFCClientV2) GetCloudProviderID() v1.CloudProviderID {
93+
return CloudProviderID
94+
}
95+
96+
func (c *SFCClientV2) GetOrganization() string {
97+
return c.organization
98+
}
99+
100+
func (c *SFCClientV2) GetWorkspace() string {
101+
return c.workspace
102+
}
103+
104+
func (c *SFCClientV2) GetReferenceID() string {
105+
return c.refID
106+
}
107+
108+
func (c *SFCClientV2) GetTenantID() (string, error) {
109+
return "", nil
110+
}
111+
112+
func (c *SFCClientV2) MakeClient(_ context.Context, location string) (v1.CloudClient, error) {
113+
c.location = location
114+
return c, nil
115+
}

0 commit comments

Comments
 (0)