Skip to content

Commit a3b7a5d

Browse files
Merge pull request #1 from sfcompute/sfc
merge in SFC client code
2 parents 13327cb + 7fc00b0 commit a3b7a5d

5 files changed

Lines changed: 258 additions & 0 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
golang.org/x/text v0.29.0
2727
google.golang.org/grpc v1.75.0
2828
gopkg.in/validator.v2 v2.0.1
29+
github.com/sfcompute/nodes-go v0.1.0-alpha.3
2930
gopkg.in/yaml.v3 v3.0.1
3031
k8s.io/api v0.34.1
3132
k8s.io/apimachinery v0.34.1

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D
160160
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
161161
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
162162
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
163+
github.com/sfcompute/nodes-go v0.1.0-alpha.3/go.mod h1:dF3O8MCxLz3FTVYhjCa876Z9O3EAM8E8fONivDpfmkM=
163164
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
164165
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
165166
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
6+
v1 "github.com/brevdev/cloud/v1"
7+
)
8+
9+
func getSFCCapabilities() v1.Capabilities {
10+
return v1.Capabilities{
11+
v1.CapabilityCreateInstance,
12+
v1.CapabilityTerminateInstance,
13+
v1.CapabilityCreateTerminateInstance,
14+
// add others supported by your provider: reboot, stop/start, machine-image, tags, resize-volume, modify-firewall, etc.
15+
}
16+
}
17+
18+
func (c *SFCClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
19+
return getSFCCapabilities(), nil
20+
}
21+
22+
func (c *SFCCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
23+
return getSFCCapabilities(), nil
24+
}

v1/providers/sfcompute/client.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
6+
v1 "github.com/brevdev/cloud/v1"
7+
"github.com/sfcompute/nodes-go/option"
8+
9+
sfcnodes "github.com/sfcompute/nodes-go"
10+
)
11+
12+
type SFCCredential struct {
13+
RefID string
14+
apiKey string `json:"api_key"`
15+
}
16+
17+
var _ v1.CloudCredential = &SFCCredential{}
18+
19+
func NewSFCCredential(refID string, apiKey string /* auth fields */) *SFCCredential {
20+
return &SFCCredential{
21+
RefID: refID,
22+
apiKey: apiKey,
23+
// ...
24+
}
25+
}
26+
27+
func (c *SFCCredential) GetReferenceID() string { return c.RefID }
28+
func (c *SFCCredential) GetAPIType() v1.APIType { return v1.APITypeLocational /* or v1.APITypeGlobal */ }
29+
func (c *SFCCredential) GetCloudProviderID() v1.CloudProviderID {
30+
return "sfcompute" // e.g., "lambdalabs"
31+
}
32+
func (c *SFCCredential) GetTenantID() (string, error) {
33+
// sfc does not have a tenant system, return empty string
34+
return "", nil
35+
}
36+
37+
func (c *SFCCredential) MakeClient(ctx context.Context, location string) (v1.CloudClient, error) {
38+
// Create a client configured for a given location if locational API
39+
return NewSFCClient(c.RefID, c.apiKey /* auth fields */).MakeClient(ctx, location)
40+
}
41+
42+
// ---------------- Client ----------------
43+
44+
type SFCClient struct {
45+
v1.NotImplCloudClient
46+
refID string
47+
location string
48+
apiKey string
49+
client sfcnodes.Client // Add this field
50+
// add http/sdk client fields, base URLs, etc.
51+
}
52+
53+
var _ v1.CloudClient = &SFCClient{}
54+
55+
func NewSFCClient(refID string, apiKey string /* auth fields */) *SFCClient {
56+
return &SFCClient{
57+
refID: refID,
58+
apiKey: apiKey,
59+
client: sfcnodes.NewClient(
60+
option.WithBearerToken(apiKey)),
61+
// init http/sdk clients here
62+
}
63+
}
64+
65+
func (c *SFCClient) GetAPIType() v1.APIType { return v1.APITypeGlobal /* or Global */ }
66+
func (c *SFCClient) GetCloudProviderID() v1.CloudProviderID { return "sfcompute" }
67+
func (c *SFCClient) GetReferenceID() string { return c.refID }
68+
func (c *SFCClient) GetTenantID() (string, error) { return "", nil }
69+
70+
func (c *SFCClient) MakeClient(_ context.Context, location string) (v1.CloudClient, error) {
71+
c.location = location
72+
return c, nil
73+
}

v1/providers/sfcompute/instance.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
v1 "github.com/brevdev/cloud/v1"
11+
sfcnodes "github.com/sfcompute/nodes-go"
12+
"github.com/sfcompute/nodes-go/packages/param"
13+
)
14+
15+
// define function to convert string to b64
16+
func toBase64(s string) string {
17+
return base64.StdEncoding.EncodeToString([]byte(s))
18+
}
19+
20+
// define function to add ssh key to cloud init
21+
func sshKeyCloudInit(sshKey string) string {
22+
return toBase64(fmt.Sprintf("#cloud-config\nssh_authorized_keys:\n - %s", sshKey))
23+
}
24+
25+
func mapSFCStatus(s string) v1.LifecycleStatus {
26+
switch strings.ToLower(s) {
27+
case "pending", "nodefailure", "unspecified", "awaitingcapacity", "unknown", "failed":
28+
return v1.LifecycleStatusPending
29+
case "running":
30+
return v1.LifecycleStatusRunning
31+
// case "stopping":
32+
//return v1.LifecycleStatusStopping
33+
case "stopped":
34+
return v1.LifecycleStatusStopped
35+
case "terminating", "released":
36+
return v1.LifecycleStatusTerminating
37+
case "destroyed", "deleted":
38+
return v1.LifecycleStatusTerminated
39+
default:
40+
return v1.LifecycleStatusPending
41+
}
42+
}
43+
44+
func (c *SFCClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) {
45+
resp, err := c.client.Nodes.New(ctx, sfcnodes.NodeNewParams{
46+
CreateNodesRequest: sfcnodes.CreateNodesRequestParam{
47+
DesiredCount: 1,
48+
MaxPricePerNodeHour: 1000,
49+
Zone: attrs.Location,
50+
ImageID: param.Opt[string]{Value: attrs.ImageID}, //this needs to point to a valid image
51+
CloudInitUserData: param.Opt[string]{Value: sshKeyCloudInit(attrs.PublicKey)}, // encode ssh key to b64-wrapped cloud-init script
52+
},
53+
})
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
if len(resp.Data) == 0 {
59+
return nil, fmt.Errorf("no nodes returned")
60+
}
61+
node := resp.Data[0]
62+
63+
inst := &v1.Instance{
64+
Name: attrs.Name,
65+
RefID: attrs.RefID,
66+
CloudCredRefID: c.refID,
67+
CloudID: v1.CloudProviderInstanceID(node.ID), // SFC ID
68+
ImageID: attrs.ImageID,
69+
InstanceType: attrs.InstanceType,
70+
Location: attrs.Location,
71+
CreatedAt: time.Now(),
72+
Status: v1.Status{LifecycleStatus: mapSFCStatus(fmt.Sprint(node.Status))}, // map SDK status to our lifecycle
73+
InstanceTypeID: v1.InstanceTypeID(node.GPUType),
74+
SSHPort: 2222, // we use 2222/tcp for all of our SSH ports
75+
}
76+
77+
return inst, nil
78+
}
79+
80+
func (c *SFCClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) {
81+
node, err := c.client.Nodes.Get(ctx, string(id))
82+
if err != nil {
83+
panic(err.Error())
84+
}
85+
var vmID string
86+
if len(node.VMs.Data) > 0 {
87+
vmID = node.VMs.Data[0].ID
88+
fmt.Println(vmID)
89+
}
90+
91+
ssh, err := c.client.VMs.SSH(ctx, sfcnodes.VMSSHParams{VMID: vmID})
92+
if err != nil {
93+
panic(err.Error())
94+
}
95+
96+
inst := &v1.Instance{
97+
Name: node.Name,
98+
RefID: c.refID,
99+
CloudCredRefID: c.refID,
100+
CloudID: v1.CloudProviderInstanceID(node.ID), // SFC ID
101+
PublicIP: ssh.SSHHostname,
102+
CreatedAt: time.Unix(node.CreatedAt, 0),
103+
Status: v1.Status{LifecycleStatus: mapSFCStatus(fmt.Sprint(node.Status))}, // map SDK status to our lifecycle
104+
InstanceTypeID: v1.InstanceTypeID(node.GPUType),
105+
}
106+
return inst, nil
107+
}
108+
109+
func (c *SFCClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) {
110+
resp, err := c.client.Nodes.List(ctx, sfcnodes.NodeListParams{})
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
var instances []v1.Instance
116+
for _, node := range resp.Data {
117+
inst, err := c.GetInstance(ctx, v1.CloudProviderInstanceID(node.ID))
118+
if err != nil {
119+
return nil, err
120+
}
121+
if inst != nil {
122+
instances = append(instances, *inst)
123+
}
124+
}
125+
return instances, nil
126+
}
127+
128+
func (c *SFCClient) TerminateInstance(ctx context.Context, id v1.CloudProviderInstanceID) error {
129+
// release the node first
130+
_, errRelease := c.client.Nodes.Release(ctx, string(id))
131+
if errRelease != nil {
132+
panic(errRelease.Error())
133+
}
134+
// then delete the node
135+
errDelete := c.client.Nodes.Delete(ctx, string(id))
136+
if errDelete != nil {
137+
panic(errDelete.Error())
138+
}
139+
return nil
140+
}
141+
142+
// Optional if supported:
143+
func (c *SFCClient) RebootInstance(ctx context.Context, id v1.CloudProviderInstanceID) error {
144+
return fmt.Errorf("not implemented")
145+
}
146+
func (c *SFCClient) StopInstance(ctx context.Context, id v1.CloudProviderInstanceID) error {
147+
return fmt.Errorf("not implemented")
148+
}
149+
func (c *SFCClient) StartInstance(ctx context.Context, id v1.CloudProviderInstanceID) error {
150+
return fmt.Errorf("not implemented")
151+
}
152+
153+
// Merge strategies (pass-through is acceptable baseline).
154+
func (c *SFCClient) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Instance) v1.Instance {
155+
return newInst
156+
}
157+
func (c *SFCClient) MergeInstanceTypeForUpdate(_ v1.InstanceType, newIt v1.InstanceType) v1.InstanceType {
158+
return newIt
159+
}

0 commit comments

Comments
 (0)