Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions internal/lambdalabs/v1/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
v1 "github.com/brevdev/compute/pkg/v1"
)

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

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

// Firewall Management
v1.CapabilityModifyFirewall, // Firewall rulesets API available

// UNSUPPORTED FEATURES (no API evidence found):
// - v1.CapabilityModifyFirewall // Firewall management is project-level, not instance-level
// - v1.CapabilityStopStartInstance // No stop/start endpoints
// - v1.CapabilityResizeInstanceVolume // No volume resizing endpoints
// - v1.CapabilityMachineImage // No image endpoints
// - v1.CapabilityTags // No tagging endpoints
}
}

// GetCapabilities returns the capabilities of Lambda Labs client
func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
return getLambdaLabsCapabilities(), nil
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we find a way to combine client + credential capabilities these? also is appears lambda does not support modify firewall since it is project level

return capabilities, nil
// GetCapabilities returns the capabilities for Lambda Labs credential
func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) {
return getLambdaLabsCapabilities(), nil
}
63 changes: 61 additions & 2 deletions internal/lambdalabs/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,78 @@ package v1

import (
"context"
"crypto/sha256"
"fmt"
"net/http"

openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs"
v1 "github.com/brevdev/compute/pkg/v1"
)

// LambdaLabsCredential implements the CloudCredential interface for Lambda Labs
type LambdaLabsCredential struct {
RefID string
APIKey string
}

var _ v1.CloudCredential = &LambdaLabsCredential{}

// NewLambdaLabsCredential creates a new Lambda Labs credential
func NewLambdaLabsCredential(refID, apiKey string) *LambdaLabsCredential {
return &LambdaLabsCredential{
RefID: refID,
APIKey: apiKey,
}
}

// GetReferenceID returns the reference ID for this credential
func (c *LambdaLabsCredential) GetReferenceID() string {
return c.RefID
}

// GetAPIType returns the API type for Lambda Labs
func (c *LambdaLabsCredential) GetAPIType() v1.APIType {
return v1.APITypeGlobal
}

// GetCloudProviderID returns the cloud provider ID for Lambda Labs
func (c *LambdaLabsCredential) GetCloudProviderID() v1.CloudProviderID {
return "lambdalabs"
}

// GetTenantID returns the tenant ID for Lambda Labs
func (c *LambdaLabsCredential) GetTenantID() (string, error) {
return fmt.Sprintf("lambdalabs-%x", sha256.Sum256([]byte(c.APIKey))), nil
}

// MakeClient creates a new Lambda Labs client from this credential
func (c *LambdaLabsCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) {
return NewLambdaLabsClient(c.RefID, c.APIKey), nil
}

// LambdaLabsClient implements the CloudClient interface for Lambda Labs
// It embeds NotImplCloudClient to handle unsupported features
type LambdaLabsClient struct {
v1.NotImplCloudClient
refID string
apiKey string
baseURL string
client *openapi.APIClient
}

var _ v1.CloudClient = &LambdaLabsClient{}

// NewLambdaLabsClient creates a new Lambda Labs client
func NewLambdaLabsClient(apiKey string) *LambdaLabsClient {
func NewLambdaLabsClient(refID, apiKey string) *LambdaLabsClient {
config := openapi.NewConfiguration()
config.HTTPClient = http.DefaultClient
client := openapi.NewAPIClient(config)

return &LambdaLabsClient{
refID: refID,
apiKey: apiKey,
baseURL: "https://cloud.lambda.ai/api/v1",
client: client,
}
}

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

// GetReferenceID returns the reference ID for this client
func (c *LambdaLabsClient) GetReferenceID() string {
return "lambdalabs-client"
return c.refID
}

func (c *LambdaLabsClient) makeAuthContext(ctx context.Context) context.Context {
return context.WithValue(ctx, openapi.ContextBasicAuth, openapi.BasicAuth{
UserName: c.apiKey,
})
}
223 changes: 186 additions & 37 deletions internal/lambdalabs/v1/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,223 @@ package v1
import (
"context"
"fmt"
"strings"
"time"

openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs"
v1 "github.com/brevdev/compute/pkg/v1"
)

const lambdaLabsTimeNameFormat = "2006-01-02-15-04-05Z07-00"

// CreateInstance creates a new instance in Lambda Labs
// Supported via: POST /api/v1/instance-operations/launch
func (c *LambdaLabsClient) CreateInstance(_ context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) {
// TODO: Implement Lambda Labs instance creation
// This would typically involve:
// 1. Validating the instance type and location
// 2. Creating the instance via Lambda Labs API
// 3. Waiting for the instance to be ready
// 4. Returning the instance details
func (c *LambdaLabsClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) {
keyPairName := attrs.RefID
if attrs.KeyPairName != nil {
keyPairName = *attrs.KeyPairName
}

return &v1.Instance{
Name: attrs.Name,
RefID: attrs.RefID,
CreatedAt: time.Now(),
CloudID: v1.CloudProviderInstanceID("lambda-instance-id"), // TODO: Get from API response
Location: attrs.Location,
SubLocation: attrs.SubLocation,
InstanceType: attrs.InstanceType,
ImageID: attrs.ImageID,
DiskSize: attrs.DiskSize,
Status: v1.Status{
LifecycleStatus: v1.LifecycleStatusRunning,
},
Tags: attrs.Tags,
}, nil
if attrs.PublicKey != "" {
request := openapi.AddSSHKeyRequest{
Name: keyPairName,
PublicKey: &attrs.PublicKey,
}

_, resp, err := c.client.DefaultAPI.AddSSHKey(c.makeAuthContext(ctx)).AddSSHKeyRequest(request).Execute()
if resp != nil {
defer func() { _ = resp.Body.Close() }()
}
if err != nil && !strings.Contains(err.Error(), "name must be unique") {
return nil, fmt.Errorf("failed to add SSH key: %w", err)
}
}

location := attrs.Location
if location == "" {
location = "us-west-1"
}

quantity := int32(1)
request := openapi.LaunchInstanceRequest{
RegionName: location,
InstanceTypeName: attrs.InstanceType,
SshKeyNames: []string{keyPairName},
Quantity: &quantity,
FileSystemNames: []string{},
}

name := fmt.Sprintf("%s--%s", c.GetReferenceID(), time.Now().UTC().Format(lambdaLabsTimeNameFormat))
if attrs.Name != "" {
name = fmt.Sprintf("%s--%s--%s", c.GetReferenceID(), attrs.Name, time.Now().UTC().Format(lambdaLabsTimeNameFormat))
}
request.Name = *openapi.NewNullableString(&name)

resp, httpResp, err := c.client.DefaultAPI.LaunchInstance(c.makeAuthContext(ctx)).LaunchInstanceRequest(request).Execute()
if httpResp != nil {
defer func() { _ = httpResp.Body.Close() }()
}
if err != nil {
return nil, fmt.Errorf("failed to launch instance: %w", err)
}

if len(resp.Data.InstanceIds) != 1 {
return nil, fmt.Errorf("expected 1 instance ID, got %d", len(resp.Data.InstanceIds))
}

instanceID := v1.CloudProviderInstanceID(resp.Data.InstanceIds[0])
return c.GetInstance(ctx, instanceID)
}

// GetInstance retrieves an instance by ID
// Supported via: GET /api/v1/instances/{id}
func (c *LambdaLabsClient) GetInstance(_ context.Context, _ v1.CloudProviderInstanceID) (*v1.Instance, error) {
// TODO: Implement Lambda Labs instance retrieval
return nil, fmt.Errorf("not implemented")
func (c *LambdaLabsClient) GetInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) (*v1.Instance, error) {
resp, httpResp, err := c.client.DefaultAPI.GetInstance(c.makeAuthContext(ctx), string(instanceID)).Execute()
if httpResp != nil {
defer func() { _ = httpResp.Body.Close() }()
}
if err != nil {
return nil, fmt.Errorf("failed to get instance: %w", err)
}

return convertLambdaLabsInstanceToV1Instance(resp.Data), nil
}

// TerminateInstance terminates an instance
// Supported via: POST /api/v1/instance-operations/terminate
func (c *LambdaLabsClient) TerminateInstance(_ context.Context, _ v1.CloudProviderInstanceID) error {
// TODO: Implement Lambda Labs instance termination
return fmt.Errorf("not implemented")
func (c *LambdaLabsClient) TerminateInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error {
request := openapi.TerminateInstanceRequest{
InstanceIds: []string{string(instanceID)},
}

_, httpResp, err := c.client.DefaultAPI.TerminateInstance(c.makeAuthContext(ctx)).TerminateInstanceRequest(request).Execute()
if httpResp != nil {
defer func() { _ = httpResp.Body.Close() }()
}
if err != nil {
return fmt.Errorf("failed to terminate instance: %w", err)
}

return nil
}

// ListInstances lists all instances
// Supported via: GET /api/v1/instances
func (c *LambdaLabsClient) ListInstances(_ context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) {
// TODO: Implement Lambda Labs instance listing
return nil, fmt.Errorf("not implemented")
func (c *LambdaLabsClient) ListInstances(ctx context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) {
resp, httpResp, err := c.client.DefaultAPI.ListInstances(c.makeAuthContext(ctx)).Execute()
if httpResp != nil {
defer func() { _ = httpResp.Body.Close() }()
}
if err != nil {
return nil, fmt.Errorf("failed to list instances: %w", err)
}

instances := make([]v1.Instance, 0, len(resp.Data))
for _, llInstance := range resp.Data {
instance := convertLambdaLabsInstanceToV1Instance(llInstance)
instances = append(instances, *instance)
}

return instances, nil
}

// RebootInstance reboots an instance
// Supported via: POST /api/v1/instance-operations/restart
func (c *LambdaLabsClient) RebootInstance(_ context.Context, _ v1.CloudProviderInstanceID) error {
// TODO: Implement Lambda Labs instance rebooting
return fmt.Errorf("not implemented")
func (c *LambdaLabsClient) RebootInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error {
request := openapi.RestartInstanceRequest{
InstanceIds: []string{string(instanceID)},
}

_, httpResp, err := c.client.DefaultAPI.RestartInstance(c.makeAuthContext(ctx)).RestartInstanceRequest(request).Execute()
if httpResp != nil {
defer func() { _ = httpResp.Body.Close() }()
}
if err != nil {
return fmt.Errorf("failed to reboot instance: %w", err)
}

return nil
}

// MergeInstanceForUpdate merges instance data for updates
func convertLambdaLabsInstanceToV1Instance(llInstance openapi.Instance) *v1.Instance {
var publicIP, privateIP, hostname, name string

if llInstance.Ip.IsSet() {
publicIP = *llInstance.Ip.Get()
}
if llInstance.PrivateIp.IsSet() {
privateIP = *llInstance.PrivateIp.Get()
}
if llInstance.Hostname.IsSet() {
hostname = *llInstance.Hostname.Get()
}
if llInstance.Name.IsSet() {
name = *llInstance.Name.Get()
}

var cloudCredRefID string
var createdAt time.Time
if name != "" {
parts := strings.Split(name, "--")
if len(parts) > 0 {
cloudCredRefID = parts[0]
}
if len(parts) > 1 {
createdAt, _ = time.Parse("2006-01-02-15-04-05Z07-00", parts[1])
}
}

refID := ""
if len(llInstance.SshKeyNames) > 0 {
refID = llInstance.SshKeyNames[0]
}

return &v1.Instance{
Name: name,
RefID: refID,
CloudCredRefID: cloudCredRefID,
CreatedAt: createdAt,
CloudID: v1.CloudProviderInstanceID(llInstance.Id),
PublicIP: publicIP,
PrivateIP: privateIP,
PublicDNS: publicIP,
Hostname: hostname,
InstanceType: llInstance.InstanceType.Name,
Status: v1.Status{
LifecycleStatus: convertLambdaLabsStatusToV1Status(llInstance.Status),
},
Location: llInstance.Region.Name,
SSHUser: "ubuntu",
SSHPort: 22,
Stoppable: false,
Rebootable: true,
}
}

func convertLambdaLabsStatusToV1Status(status string) v1.LifecycleStatus {
switch status {
case "booting":
return v1.LifecycleStatusPending
case "active":
return v1.LifecycleStatusRunning
case "terminating":
return v1.LifecycleStatusTerminating
case "terminated":
return v1.LifecycleStatusTerminated
case "error":
return v1.LifecycleStatusFailed
case "unhealthy":
return v1.LifecycleStatusRunning
default:
return v1.LifecycleStatusPending
}
}

func (c *LambdaLabsClient) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Instance) v1.Instance {
// TODO: Implement instance merging logic
return newInst
}

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