Skip to content

Commit f27baae

Browse files
authored
Merge pull request #4 from brevdev/devin/1754356044-implement-lambdalabs-provider
Implement Lambda Labs provider methods
2 parents 07ecce6 + d8c2d9d commit f27baae

File tree

4 files changed

+424
-72
lines changed

4 files changed

+424
-72
lines changed

internal/lambdalabs/v1/capabilities.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
v1 "github.com/brevdev/compute/pkg/v1"
77
)
88

9-
// GetCapabilities returns the capabilities of Lambda Labs
9+
// getLambdaLabsCapabilities returns the unified capabilities for Lambda Labs
1010
// Based on API documentation at https://cloud.lambda.ai/api/v1/openapi.json
11-
func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
12-
capabilities := v1.Capabilities{
11+
func getLambdaLabsCapabilities() v1.Capabilities {
12+
return v1.Capabilities{
1313
// SUPPORTED FEATURES (with API evidence):
1414

1515
// Instance Management
@@ -18,15 +18,21 @@ func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities,
1818
v1.CapabilityCreateTerminateInstance, // Combined create/terminate capability
1919
v1.CapabilityRebootInstance, // POST /api/v1/instance-operations/restart
2020

21-
// Firewall Management
22-
v1.CapabilityModifyFirewall, // Firewall rulesets API available
23-
2421
// UNSUPPORTED FEATURES (no API evidence found):
22+
// - v1.CapabilityModifyFirewall // Firewall management is project-level, not instance-level
2523
// - v1.CapabilityStopStartInstance // No stop/start endpoints
2624
// - v1.CapabilityResizeInstanceVolume // No volume resizing endpoints
2725
// - v1.CapabilityMachineImage // No image endpoints
2826
// - v1.CapabilityTags // No tagging endpoints
2927
}
28+
}
29+
30+
// GetCapabilities returns the capabilities of Lambda Labs client
31+
func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
32+
return getLambdaLabsCapabilities(), nil
33+
}
3034

31-
return capabilities, nil
35+
// GetCapabilities returns the capabilities for Lambda Labs credential
36+
func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
37+
return getLambdaLabsCapabilities(), nil
3238
}

internal/lambdalabs/v1/client.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,78 @@ package v1
22

33
import (
44
"context"
5+
"crypto/sha256"
6+
"fmt"
7+
"net/http"
58

9+
openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs"
610
v1 "github.com/brevdev/compute/pkg/v1"
711
)
812

13+
// LambdaLabsCredential implements the CloudCredential interface for Lambda Labs
14+
type LambdaLabsCredential struct {
15+
RefID string
16+
APIKey string
17+
}
18+
19+
var _ v1.CloudCredential = &LambdaLabsCredential{}
20+
21+
// NewLambdaLabsCredential creates a new Lambda Labs credential
22+
func NewLambdaLabsCredential(refID, apiKey string) *LambdaLabsCredential {
23+
return &LambdaLabsCredential{
24+
RefID: refID,
25+
APIKey: apiKey,
26+
}
27+
}
28+
29+
// GetReferenceID returns the reference ID for this credential
30+
func (c *LambdaLabsCredential) GetReferenceID() string {
31+
return c.RefID
32+
}
33+
34+
// GetAPIType returns the API type for Lambda Labs
35+
func (c *LambdaLabsCredential) GetAPIType() v1.APIType {
36+
return v1.APITypeGlobal
37+
}
38+
39+
// GetCloudProviderID returns the cloud provider ID for Lambda Labs
40+
func (c *LambdaLabsCredential) GetCloudProviderID() v1.CloudProviderID {
41+
return "lambdalabs"
42+
}
43+
44+
// GetTenantID returns the tenant ID for Lambda Labs
45+
func (c *LambdaLabsCredential) GetTenantID() (string, error) {
46+
return fmt.Sprintf("lambdalabs-%x", sha256.Sum256([]byte(c.APIKey))), nil
47+
}
48+
49+
// MakeClient creates a new Lambda Labs client from this credential
50+
func (c *LambdaLabsCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) {
51+
return NewLambdaLabsClient(c.RefID, c.APIKey), nil
52+
}
53+
954
// LambdaLabsClient implements the CloudClient interface for Lambda Labs
1055
// It embeds NotImplCloudClient to handle unsupported features
1156
type LambdaLabsClient struct {
1257
v1.NotImplCloudClient
58+
refID string
1359
apiKey string
1460
baseURL string
61+
client *openapi.APIClient
1562
}
1663

1764
var _ v1.CloudClient = &LambdaLabsClient{}
1865

1966
// NewLambdaLabsClient creates a new Lambda Labs client
20-
func NewLambdaLabsClient(apiKey string) *LambdaLabsClient {
67+
func NewLambdaLabsClient(refID, apiKey string) *LambdaLabsClient {
68+
config := openapi.NewConfiguration()
69+
config.HTTPClient = http.DefaultClient
70+
client := openapi.NewAPIClient(config)
71+
2172
return &LambdaLabsClient{
73+
refID: refID,
2274
apiKey: apiKey,
2375
baseURL: "https://cloud.lambda.ai/api/v1",
76+
client: client,
2477
}
2578
}
2679

@@ -49,5 +102,11 @@ func (c *LambdaLabsClient) GetTenantID() (string, error) {
49102

50103
// GetReferenceID returns the reference ID for this client
51104
func (c *LambdaLabsClient) GetReferenceID() string {
52-
return "lambdalabs-client"
105+
return c.refID
106+
}
107+
108+
func (c *LambdaLabsClient) makeAuthContext(ctx context.Context) context.Context {
109+
return context.WithValue(ctx, openapi.ContextBasicAuth, openapi.BasicAuth{
110+
UserName: c.apiKey,
111+
})
53112
}

internal/lambdalabs/v1/instance.go

Lines changed: 186 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,223 @@ package v1
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"time"
78

9+
openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs"
810
v1 "github.com/brevdev/compute/pkg/v1"
911
)
1012

13+
const lambdaLabsTimeNameFormat = "2006-01-02-15-04-05Z07-00"
14+
1115
// CreateInstance creates a new instance in Lambda Labs
1216
// Supported via: POST /api/v1/instance-operations/launch
13-
func (c *LambdaLabsClient) CreateInstance(_ context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) {
14-
// TODO: Implement Lambda Labs instance creation
15-
// This would typically involve:
16-
// 1. Validating the instance type and location
17-
// 2. Creating the instance via Lambda Labs API
18-
// 3. Waiting for the instance to be ready
19-
// 4. Returning the instance details
17+
func (c *LambdaLabsClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) {
18+
keyPairName := attrs.RefID
19+
if attrs.KeyPairName != nil {
20+
keyPairName = *attrs.KeyPairName
21+
}
2022

21-
return &v1.Instance{
22-
Name: attrs.Name,
23-
RefID: attrs.RefID,
24-
CreatedAt: time.Now(),
25-
CloudID: v1.CloudProviderInstanceID("lambda-instance-id"), // TODO: Get from API response
26-
Location: attrs.Location,
27-
SubLocation: attrs.SubLocation,
28-
InstanceType: attrs.InstanceType,
29-
ImageID: attrs.ImageID,
30-
DiskSize: attrs.DiskSize,
31-
Status: v1.Status{
32-
LifecycleStatus: v1.LifecycleStatusRunning,
33-
},
34-
Tags: attrs.Tags,
35-
}, nil
23+
if attrs.PublicKey != "" {
24+
request := openapi.AddSSHKeyRequest{
25+
Name: keyPairName,
26+
PublicKey: &attrs.PublicKey,
27+
}
28+
29+
_, resp, err := c.client.DefaultAPI.AddSSHKey(c.makeAuthContext(ctx)).AddSSHKeyRequest(request).Execute()
30+
if resp != nil {
31+
defer func() { _ = resp.Body.Close() }()
32+
}
33+
if err != nil && !strings.Contains(err.Error(), "name must be unique") {
34+
return nil, fmt.Errorf("failed to add SSH key: %w", err)
35+
}
36+
}
37+
38+
location := attrs.Location
39+
if location == "" {
40+
location = "us-west-1"
41+
}
42+
43+
quantity := int32(1)
44+
request := openapi.LaunchInstanceRequest{
45+
RegionName: location,
46+
InstanceTypeName: attrs.InstanceType,
47+
SshKeyNames: []string{keyPairName},
48+
Quantity: &quantity,
49+
FileSystemNames: []string{},
50+
}
51+
52+
name := fmt.Sprintf("%s--%s", c.GetReferenceID(), time.Now().UTC().Format(lambdaLabsTimeNameFormat))
53+
if attrs.Name != "" {
54+
name = fmt.Sprintf("%s--%s--%s", c.GetReferenceID(), attrs.Name, time.Now().UTC().Format(lambdaLabsTimeNameFormat))
55+
}
56+
request.Name = *openapi.NewNullableString(&name)
57+
58+
resp, httpResp, err := c.client.DefaultAPI.LaunchInstance(c.makeAuthContext(ctx)).LaunchInstanceRequest(request).Execute()
59+
if httpResp != nil {
60+
defer func() { _ = httpResp.Body.Close() }()
61+
}
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to launch instance: %w", err)
64+
}
65+
66+
if len(resp.Data.InstanceIds) != 1 {
67+
return nil, fmt.Errorf("expected 1 instance ID, got %d", len(resp.Data.InstanceIds))
68+
}
69+
70+
instanceID := v1.CloudProviderInstanceID(resp.Data.InstanceIds[0])
71+
return c.GetInstance(ctx, instanceID)
3672
}
3773

3874
// GetInstance retrieves an instance by ID
3975
// Supported via: GET /api/v1/instances/{id}
40-
func (c *LambdaLabsClient) GetInstance(_ context.Context, _ v1.CloudProviderInstanceID) (*v1.Instance, error) {
41-
// TODO: Implement Lambda Labs instance retrieval
42-
return nil, fmt.Errorf("not implemented")
76+
func (c *LambdaLabsClient) GetInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) (*v1.Instance, error) {
77+
resp, httpResp, err := c.client.DefaultAPI.GetInstance(c.makeAuthContext(ctx), string(instanceID)).Execute()
78+
if httpResp != nil {
79+
defer func() { _ = httpResp.Body.Close() }()
80+
}
81+
if err != nil {
82+
return nil, fmt.Errorf("failed to get instance: %w", err)
83+
}
84+
85+
return convertLambdaLabsInstanceToV1Instance(resp.Data), nil
4386
}
4487

4588
// TerminateInstance terminates an instance
4689
// Supported via: POST /api/v1/instance-operations/terminate
47-
func (c *LambdaLabsClient) TerminateInstance(_ context.Context, _ v1.CloudProviderInstanceID) error {
48-
// TODO: Implement Lambda Labs instance termination
49-
return fmt.Errorf("not implemented")
90+
func (c *LambdaLabsClient) TerminateInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error {
91+
request := openapi.TerminateInstanceRequest{
92+
InstanceIds: []string{string(instanceID)},
93+
}
94+
95+
_, httpResp, err := c.client.DefaultAPI.TerminateInstance(c.makeAuthContext(ctx)).TerminateInstanceRequest(request).Execute()
96+
if httpResp != nil {
97+
defer func() { _ = httpResp.Body.Close() }()
98+
}
99+
if err != nil {
100+
return fmt.Errorf("failed to terminate instance: %w", err)
101+
}
102+
103+
return nil
50104
}
51105

52106
// ListInstances lists all instances
53107
// Supported via: GET /api/v1/instances
54-
func (c *LambdaLabsClient) ListInstances(_ context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) {
55-
// TODO: Implement Lambda Labs instance listing
56-
return nil, fmt.Errorf("not implemented")
108+
func (c *LambdaLabsClient) ListInstances(ctx context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) {
109+
resp, httpResp, err := c.client.DefaultAPI.ListInstances(c.makeAuthContext(ctx)).Execute()
110+
if httpResp != nil {
111+
defer func() { _ = httpResp.Body.Close() }()
112+
}
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to list instances: %w", err)
115+
}
116+
117+
instances := make([]v1.Instance, 0, len(resp.Data))
118+
for _, llInstance := range resp.Data {
119+
instance := convertLambdaLabsInstanceToV1Instance(llInstance)
120+
instances = append(instances, *instance)
121+
}
122+
123+
return instances, nil
57124
}
58125

59126
// RebootInstance reboots an instance
60127
// Supported via: POST /api/v1/instance-operations/restart
61-
func (c *LambdaLabsClient) RebootInstance(_ context.Context, _ v1.CloudProviderInstanceID) error {
62-
// TODO: Implement Lambda Labs instance rebooting
63-
return fmt.Errorf("not implemented")
128+
func (c *LambdaLabsClient) RebootInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error {
129+
request := openapi.RestartInstanceRequest{
130+
InstanceIds: []string{string(instanceID)},
131+
}
132+
133+
_, httpResp, err := c.client.DefaultAPI.RestartInstance(c.makeAuthContext(ctx)).RestartInstanceRequest(request).Execute()
134+
if httpResp != nil {
135+
defer func() { _ = httpResp.Body.Close() }()
136+
}
137+
if err != nil {
138+
return fmt.Errorf("failed to reboot instance: %w", err)
139+
}
140+
141+
return nil
64142
}
65143

66144
// MergeInstanceForUpdate merges instance data for updates
145+
func convertLambdaLabsInstanceToV1Instance(llInstance openapi.Instance) *v1.Instance {
146+
var publicIP, privateIP, hostname, name string
147+
148+
if llInstance.Ip.IsSet() {
149+
publicIP = *llInstance.Ip.Get()
150+
}
151+
if llInstance.PrivateIp.IsSet() {
152+
privateIP = *llInstance.PrivateIp.Get()
153+
}
154+
if llInstance.Hostname.IsSet() {
155+
hostname = *llInstance.Hostname.Get()
156+
}
157+
if llInstance.Name.IsSet() {
158+
name = *llInstance.Name.Get()
159+
}
160+
161+
var cloudCredRefID string
162+
var createdAt time.Time
163+
if name != "" {
164+
parts := strings.Split(name, "--")
165+
if len(parts) > 0 {
166+
cloudCredRefID = parts[0]
167+
}
168+
if len(parts) > 1 {
169+
createdAt, _ = time.Parse("2006-01-02-15-04-05Z07-00", parts[1])
170+
}
171+
}
172+
173+
refID := ""
174+
if len(llInstance.SshKeyNames) > 0 {
175+
refID = llInstance.SshKeyNames[0]
176+
}
177+
178+
return &v1.Instance{
179+
Name: name,
180+
RefID: refID,
181+
CloudCredRefID: cloudCredRefID,
182+
CreatedAt: createdAt,
183+
CloudID: v1.CloudProviderInstanceID(llInstance.Id),
184+
PublicIP: publicIP,
185+
PrivateIP: privateIP,
186+
PublicDNS: publicIP,
187+
Hostname: hostname,
188+
InstanceType: llInstance.InstanceType.Name,
189+
Status: v1.Status{
190+
LifecycleStatus: convertLambdaLabsStatusToV1Status(llInstance.Status),
191+
},
192+
Location: llInstance.Region.Name,
193+
SSHUser: "ubuntu",
194+
SSHPort: 22,
195+
Stoppable: false,
196+
Rebootable: true,
197+
}
198+
}
199+
200+
func convertLambdaLabsStatusToV1Status(status string) v1.LifecycleStatus {
201+
switch status {
202+
case "booting":
203+
return v1.LifecycleStatusPending
204+
case "active":
205+
return v1.LifecycleStatusRunning
206+
case "terminating":
207+
return v1.LifecycleStatusTerminating
208+
case "terminated":
209+
return v1.LifecycleStatusTerminated
210+
case "error":
211+
return v1.LifecycleStatusFailed
212+
case "unhealthy":
213+
return v1.LifecycleStatusRunning
214+
default:
215+
return v1.LifecycleStatusPending
216+
}
217+
}
218+
67219
func (c *LambdaLabsClient) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Instance) v1.Instance {
68-
// TODO: Implement instance merging logic
69220
return newInst
70221
}
71222

72-
// MergeInstanceTypeForUpdate merges instance type data for updates
73223
func (c *LambdaLabsClient) MergeInstanceTypeForUpdate(_ v1.InstanceType, newIt v1.InstanceType) v1.InstanceType {
74-
// TODO: Implement instance type merging logic
75224
return newIt
76225
}

0 commit comments

Comments
 (0)