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
59 changes: 0 additions & 59 deletions .github/workflows/validation-lambdalabs.yml

This file was deleted.

2 changes: 0 additions & 2 deletions .golangci.bck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,3 @@ issues:
exclude-use-default: false
exclude:
- composites
exclude-dirs:
- internal/lambdalabs/gen
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ linters:
- path: (.+)\.go$
text: composites
paths:
- internal/lambdalabs/gen
- third_party$
- builtin$
- examples$
Expand All @@ -104,7 +103,6 @@ formatters:
exclusions:
generated: lax
paths:
- internal/lambdalabs/gen
- third_party$
- builtin$
- examples$
76 changes: 28 additions & 48 deletions docs/CloudManual.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ We need an API endpoint that returns your available instance types. For each typ

**Example: Converting Provider Data to `v1.InstanceType`**

From Lambda Labs implementation (`cloud/v1/providers/lambdalabs/instancetype.go`):
Example conversion:

```go
it := v1.InstanceType{
Expand Down Expand Up @@ -195,19 +195,23 @@ When implementing the Cloud SDK, you declare how Brev's control plane should que

**You handle the mapping internally.** The SDK doesn't call your API directly—your implementation does. Whether your cloud's native API is regional, global, or something else entirely, you write the conversion logic in `GetInstanceTypes()`.

**Example: Global API (Lambda Labs)**
Lambda Labs' API returns all instance types with regional availability embedded. The SDK implementation fetches once and expands to per-region `v1.InstanceType` entries:
**Example: Global API (Launchpad)**
Launchpad's catalog returns instance types with per-region capacity. The SDK implementation fetches the catalog once and expands available entries into per-region `v1.InstanceType` values:

```go
// Simplified from cloud/v1/providers/lambdalabs/instancetype.go
func (c *LambdaLabsClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) {
resp, _ := c.client.InstanceTypes(ctx) // Single API call returns all types

// Expand each type to all its available regions
for _, instType := range resp.Data {
for _, region := range locations {
isAvailable := slices.Contains(instType.RegionsWithCapacityAvailable, region.Name)
instanceTypes = append(instanceTypes, convertToV1(region.Name, instType, isAvailable))
// Simplified from cloud/v1/providers/launchpad/instancetype.go
func (c *LaunchpadClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) {
catalog, _ := c.paginateInstanceTypes(ctx, 100)

for _, nativeType := range catalog {
for region, capacity := range nativeType.Capacity {
if capacity == 0 {
continue
}
instanceType, _ := launchpadInstanceTypeToInstanceType(nativeType, region)
if v1.IsSelectedByArgs(*instanceType, args) {
instanceTypes = append(instanceTypes, *instanceType)
}
}
}
return instanceTypes, nil
Expand Down Expand Up @@ -248,7 +252,7 @@ When we ingest your instance types, we normalize them to the `v1.InstanceType` s
| Field | Type | Description |
|-------|------|-------------|
| `ID` | `InstanceTypeID` | Stable, unique identifier (you define the format—see below) |
| `Cloud` | `string` | Your cloud identifier (e.g., `"lambdalabs"`, `"crusoe"`) |
| `Cloud` | `string` | Your cloud identifier (e.g., `"nebius"`, `"shadeform"`) |
| `Provider` | `string` | Provider identifier (often same as `Cloud`) |
| `Type` | `string` | Your native type name |
| `Location` | `string` | Primary region identifier |
Expand Down Expand Up @@ -506,18 +510,6 @@ type GPU struct {

### Provider Examples

**Lambda Labs** (`cloud/v1/providers/lambdalabs/instancetype.go:parseGPUFromDescription`)

Parses `"8x A100 (40 GB SXM4)"` using regex:

```go
gpu.Count = int32(count) // from (\d+)x
gpu.Name = nameStr // from x (.*?) \(
gpu.MemoryBytes = v1.NewBytes(v1.BytesValue(memoryGiB), v1.Gibibyte)
gpu.NetworkDetails = networkDetails // remainder after "GB"
gpu.Manufacturer = "NVIDIA"
```

**Launchpad** (`cloud/v1/providers/launchpad/instancetype.go:launchpadGpusToGpus`)

Maps structured API fields:
Expand Down Expand Up @@ -590,7 +582,6 @@ Providers define their own credential struct with whatever fields they need. The

| Provider | Struct Fields | JSON Fields |
|----------|---------------|-------------|
| **Lambda Labs** | `APIKey string` | `api_key` |
| **Shadeform** | `APIKey string` | `api_key` |
| **FluidStack** | `APIKey string` | `api_key` |
| **AWS** | `AccessKeyID`, `SecretAccessKey` | `access_key_id`, `secret_access_key` |
Expand Down Expand Up @@ -698,24 +689,21 @@ The SDK defines these states in `LifecycleStatus` (from `cloud/v1/instance.go`):
| `SSHPort` | Always | SSH port (typically `22`) |
| `RefID` | Always | Echo back the input `RefID` |

**Example flow (from Lambda Labs implementation):**
**Example flow:**

```go
// 1. Register the SSH key with your API
keyPairResp, err := c.addSSHKey(ctx, openapi.AddSSHKeyRequest{
Name: attrs.RefID,
PublicKey: &attrs.PublicKey,
})
keyID, err := c.ensureSSHKey(ctx, attrs.RefID, attrs.PublicKey)

// 2. Launch the instance with the key
resp, err := c.launchInstance(ctx, openapi.LaunchInstanceRequest{
RegionName: attrs.Location,
InstanceTypeName: attrs.InstanceType,
SshKeyNames: []string{keyPairName},
instanceID, err := c.launchInstance(ctx, providerLaunchRequest{
Region: attrs.Location,
InstanceType: attrs.InstanceType,
SSHKeyID: keyID,
})

// 3. Return instance details
return c.GetInstance(ctx, v1.CloudProviderInstanceID(resp.Data.InstanceIds[0]))
return c.GetInstance(ctx, v1.CloudProviderInstanceID(instanceID))
```

### Terminate Instance (Required)
Expand All @@ -740,7 +728,7 @@ return c.GetInstance(ctx, v1.CloudProviderInstanceID(resp.Data.InstanceIds[0]))
- Return `nil` once the stop operation is initiated.
- Instance should transition: `running` → `stopping` → `stopped`

**When to implement:** Only if your platform supports instances that can stop and preserve storage. Lambda Labs does not support this, but Nebius does.
**When to implement:** Only if your platform supports instances that can stop and preserve storage. Nebius is an example that supports this capability.

### Start Instance (Optional)

Expand Down Expand Up @@ -783,7 +771,7 @@ instance := v1.Instance{
}
```

**Example - Lambda Labs (no stop/start support):**
**Example - provider without stop/start support:**
```go
// In GetCapabilities()
// CapabilityStopStartInstance NOT included
Expand Down Expand Up @@ -1091,7 +1079,7 @@ cloudCredRefID := tags["cloudCredRefID"]

If your API doesn't support tags, you **still must** persist and return `RefID` and `CloudCredRefID`. Use creative alternatives:

**Example (Lambda Labs without tags):**
**Example (provider without tags):**
```go
// At creation - encode CloudCredRefID in instance name
name := fmt.Sprintf("%s--%s", c.GetReferenceID(), time.Now().UTC().Format(timeFormat))
Expand Down Expand Up @@ -1165,14 +1153,6 @@ if shadeformErrorResponse.ErrorCode == outOfStockErrorCode {
}
```

**Example from Lambda Labs provider** ([`v1/providers/lambdalabs/errors.go`](v1/providers/lambdalabs/errors.go)):

```go
if strings.Contains(e.Error(), "Not enough capacity") || strings.Contains(e.Error(), "insufficient-capacity") {
return v1.ErrInsufficientResources
}
```

---

## 12. Billing and Pricing
Expand Down Expand Up @@ -1242,7 +1222,7 @@ You don't need to do anything special—just ensure the SSH public key from `Cre

To begin integration:

1. **Follow the Integration Guide and copy the template** — Start with the [Integration Guide](IntegrationGuide.md), which walks through the v1 interfaces, directory layout, and a copy/paste scaffold. Use the Lambda Labs provider as your canonical reference.
1. **Follow the Integration Guide and copy the template** — Start with the [Integration Guide](IntegrationGuide.md), which walks through the v1 interfaces, directory layout, and a copy/paste scaffold. Use the existing provider whose API model most closely matches yours as a reference.
2. **Implement your Cloud provider** — Build out instance lifecycle, instance types, capabilities, and security conformance under `internal/{provider}/v1/`. Embed `NotImplCloudClient` for any unsupported operations.
3. **Run the local Validation Tests** — Wire up `validation_test.go` using real credentials and run `make test-validation` locally. This exercises instance create/get/list/terminate, instance types, and capability checks against your live API.
4. **Provide Brev with a test account** — Give Brev access to run validation independently. This typically means a console account or provided API credentials, but exact requirements vary by provider.
Expand Down
48 changes: 22 additions & 26 deletions docs/IntegrationGuide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# How to Add a Cloud Provider

Practical guide to implementing a new cloud provider in the Brev Cloud SDK (v1). The Lambda Labs provider is the best working, well-tested example—use it as your canonical reference.
Practical guide to implementing a new cloud provider in the Brev Cloud SDK (v1). Use the existing providers as references, choosing the one whose API model most closely matches your integration.

For deeper context on the SDK’s design, interfaces, and provider expectations, see the [Cloud Manual](CloudManual.md).

Expand All @@ -11,9 +11,9 @@ Background:
- v1 design notes: ../pkg/v1/V1_DESIGN_NOTES.md

Provider examples:
- Lambda Labs (canonical): ../internal/lambdalabs/v1/README.md
- Nebius (in progress): ../internal/nebius/v1/README.md
- Fluidstack (in progress): ../internal/fluidstack/v1/README.md
- Nebius: ../v1/providers/nebius/README.md
- TestKube: ../v1/providers/testkube/README.md
- FluidStack: ../v1/providers/fluidstack/README.md

---

Expand Down Expand Up @@ -63,10 +63,10 @@ Create a new provider folder:
- networking.go, image.go, storage.go, tags.go, quota.go, location.go (as applicable)
- validation_test.go (validation suite entry point)

Use Lambda Labs as the pattern:
- ../internal/lambdalabs/v1/client.go
- ../internal/lambdalabs/v1/instance.go
- ../internal/lambdalabs/v1/capabilities.go
For a complete implementation, see:
- ../v1/providers/nebius/client.go
- ../v1/providers/nebius/instance.go
- ../v1/providers/nebius/capabilities.go

---

Expand Down Expand Up @@ -100,7 +100,7 @@ func New{Provider}Credential(refID string /* auth fields */) *{Provider}Credenti
func (c *{Provider}Credential) GetReferenceID() string { return c.RefID }
func (c *{Provider}Credential) GetAPIType() v1.APIType { return v1.APITypeLocational /* or v1.APITypeGlobal */ }
func (c *{Provider}Credential) GetCloudProviderID() v1.CloudProviderID {
return "{provider-id}" // e.g., "lambdalabs"
return "{provider-id}" // e.g., "nebius"
}
func (c *{Provider}Credential) GetTenantID() (string, error) {
// Derive stable tenant ID for quota/account scoping if possible
Expand Down Expand Up @@ -215,10 +215,10 @@ func (c *{Provider}Client) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Inst
func (c *{Provider}Client) MergeInstanceTypeForUpdate(_ v1.InstanceType, newIt v1.InstanceType) v1.Type { return newIt }
```

See the canonical mapping and conversion logic in Lambda Labs:
- Create/terminate/list/reboot: ../internal/lambdalabs/v1/instance.go
- Capabilities: ../internal/lambdalabs/v1/capabilities.go
- Client/credential + NotImpl: ../internal/lambdalabs/v1/client.go
See the Nebius implementation for mapping and conversion examples:
- Create/terminate/list/reboot: ../v1/providers/nebius/instance.go
- Capabilities: ../v1/providers/nebius/capabilities.go
- Client/credential + NotImpl: ../v1/providers/nebius/client.go

Implement instance types in internal/{provider}/v1/instancetype.go:

Expand All @@ -238,9 +238,7 @@ Implement instance types in internal/{provider}/v1/instancetype.go:
The SDK uses a three-level capability system to accurately represent what operations are supported:

### 1. Provider-Level Capabilities
These are high-level features that your cloud provider's API supports, declared in your `GetCapabilities()` method. Capability flags live in ../pkg/v1/capabilities.go. Only include capabilities your API actually supports. For example, Lambda Labs supports:
- Create/terminate/reboot instance (`CapabilityCreateInstance`, `CapabilityTerminateInstance`, `CapabilityRebootInstance`)
- Does not (currently) support stop/start, resize volume, machine image, tags
These are high-level features that your cloud provider's API supports, declared in your `GetCapabilities()` method. Capability flags live in ../pkg/v1/capabilities.go. Only include capabilities your API actually supports. For example, Nebius advertises create, terminate, reboot, stop/start, volume resize, machine image, and tag capabilities.

### 2. Instance Type Capabilities
These are hardware-specific features that vary by instance configuration, expressed as boolean fields on the `InstanceType` struct:
Expand All @@ -257,7 +255,7 @@ These are capability boolean fields replicated on individual `Instance` objects,
These fields must be kept accurate and in sync with the corresponding InstanceType capabilities, even though they appear redundant. They can also reflect runtime state-dependent variations - for example, a running instance might support certain operations that a stopped instance cannot, based on the current `LifecycleStatus`.

Reference:
- Lambda capabilities: ../internal/lambdalabs/v1/capabilities.go
- Nebius capabilities: ../v1/providers/nebius/capabilities.go

---

Expand All @@ -270,9 +268,7 @@ All providers must conform to ../docs/SECURITY.md:
- If your provider’s firewall model is global/project-scoped rather than per-instance, document limitations in internal/{provider}/SECURITY.md and reflect that by omitting CapabilityModifyFirewall if applicable.

Provider-specific security doc examples:
- Lambda Labs: ../internal/lambdalabs/SECURITY.md
- Nebius: ../internal/nebius/SECURITY.md
- Fluidstack: ../internal/fluidstack/v1/SECURITY.md
- FluidStack: ../v1/providers/fluidstack/SECURITY.md

---

Expand Down Expand Up @@ -319,7 +315,7 @@ func TestValidationFunctions(t *testing.T) {
- make test-all # runs everything

3) CI workflow (recommended):
- Add .github/workflows/validation-{provider}.yml (copy Lambda Labs workflow if available or follow VALIDATION_TESTING.md).
- Add .github/workflows/validation-{provider}.yml following VALIDATION_TESTING.md.
- Store secrets in GitHub Actions (e.g., YOUR_PROVIDER_API_KEY).

---
Expand All @@ -346,8 +342,8 @@ func TestValidationFunctions(t *testing.T) {
- Capabilities: ../pkg/v1/capabilities.go
- Instance lifecycle and validations: ../pkg/v1/instance.go
- Instance types and validations: ../pkg/v1/instancetype.go
- Lambda Labs example:
- Client/Credential: ../internal/lambdalabs/v1/client.go
- Capabilities: ../internal/lambdalabs/v1/capabilities.go
- Instance operations: ../internal/lambdalabs/v1/instance.go
- Provider README: ../internal/lambdalabs/v1/README.md
- Nebius example:
- Client/Credential: ../v1/providers/nebius/client.go
- Capabilities: ../v1/providers/nebius/capabilities.go
- Instance operations: ../v1/providers/nebius/instance.go
- Provider README: ../v1/providers/nebius/README.md
Loading
Loading