From 63b1ad83b7dd8f4ba649ba922e4231961678667e Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 08:54:24 -0700 Subject: [PATCH 1/9] set preemptible to false --- v1/providers/verda/client_test.go | 1 - v1/providers/verda/instancetype.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/v1/providers/verda/client_test.go b/v1/providers/verda/client_test.go index 7488a21..c4d3a2f 100644 --- a/v1/providers/verda/client_test.go +++ b/v1/providers/verda/client_test.go @@ -163,7 +163,6 @@ func TestGetInstanceTypesAndLocations(t *testing.T) { //nolint:funlen // One cat assert.Equal(t, "H100 80GB", h100.SupportedGPUs[0].Type) assert.Equal(t, "NVMe", h100.SupportedStorage[0].Type) assert.Equal(t, v1.Tebibyte, h100.SupportedStorage[0].SizeBytes.Unit()) - assert.True(t, h100.Preemptible) assert.True(t, h100.ElasticRootVolume) assert.Equal(t, CloudProviderID, h100.Provider) assert.Equal(t, []v1.Architecture{v1.ArchitectureX86_64}, h100.SupportedArchitectures) diff --git a/v1/providers/verda/instancetype.go b/v1/providers/verda/instancetype.go index 63b70f0..5c045d1 100644 --- a/v1/providers/verda/instancetype.go +++ b/v1/providers/verda/instancetype.go @@ -91,8 +91,6 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio SupportedUsageClasses: usageClasses, Stoppable: true, Rebootable: false, - Preemptible: verdaType.SpotPrice > 0, - VariablePrice: verdaType.DynamicPrice > 0, IsAvailable: true, BasePrice: &basePrice, Provider: CloudProviderID, From ed57ace5707d754babbe96fcb803d8f1710abc40 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 09:20:31 -0700 Subject: [PATCH 2/9] add supported storage --- v1/providers/verda/instancetype.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/v1/providers/verda/instancetype.go b/v1/providers/verda/instancetype.go index 5c045d1..46a38b0 100644 --- a/v1/providers/verda/instancetype.go +++ b/v1/providers/verda/instancetype.go @@ -86,7 +86,7 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio MemoryBytes: memoryBytes, VCPU: int32(verdaType.CPU.NumberOfCores), //nolint:gosec // ok SupportedArchitectures: []v1.Architecture{verdaArchitecture(verdaType.Model)}, - SupportedStorage: storageDescriptionToStorage(verdaType.Storage.Description), + SupportedStorage: buildSupportedStorage(), ElasticRootVolume: true, SupportedUsageClasses: usageClasses, Stoppable: true, @@ -115,6 +115,28 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio return instanceType, nil } +func buildSupportedStorage() []v1.Storage { + minSize := 50 * units.GiB + minSizeBytes := v1.NewBytes(50, v1.Gibibyte) + maxSize := 10 * units.TiB + maxSizeBytes := v1.NewBytes(10, v1.Tebibyte) + + pricePerGBHr, _ := currency.NewAmount("0.0002740", "USD") + + return []v1.Storage{ + { + Type: "nvme", + Count: 1, + MinSize: &minSize, + MaxSize: &maxSize, + MinSizeBytes: &minSizeBytes, + MaxSizeBytes: &maxSizeBytes, + IsElastic: true, + PricePerGBHr: &pricePerGBHr, + }, + } +} + func verdaGPUName(model string) string { return gpuMemorySuffixPattern.ReplaceAllString(model, "") } From 398f2bc6e671d7545420f2f4ff8a743c35820c65 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 09:34:31 -0700 Subject: [PATCH 3/9] remove redundant cloud --- v1/providers/verda/instancetype.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v1/providers/verda/instancetype.go b/v1/providers/verda/instancetype.go index 46a38b0..1a49b13 100644 --- a/v1/providers/verda/instancetype.go +++ b/v1/providers/verda/instancetype.go @@ -94,7 +94,6 @@ func verdaInstanceTypeToInstanceType(verdaType verdago.InstanceTypeInfo, locatio IsAvailable: true, BasePrice: &basePrice, Provider: CloudProviderID, - Cloud: CloudProviderID, } if verdaType.GPU.NumberOfGPUs > 0 { From 337e645b388e24df5877087e85c1eb9b55fbf930 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 09:48:49 -0700 Subject: [PATCH 4/9] use ubuntu instead of root --- v1/providers/verda/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/verda/instance.go b/v1/providers/verda/instance.go index 2de21a1..3b63615 100644 --- a/v1/providers/verda/instance.go +++ b/v1/providers/verda/instance.go @@ -22,7 +22,7 @@ const ( instanceIdentitySeparator = "_" firewallResourceNamePrefix = "brev-firewall" sshKeyResourceNamePrefix = "brev-key" - defaultSSHUser = "root" + defaultSSHUser = "ubuntu" defaultSSHPort = 22 ) From 5175efe8dd3ef7810b33b2a5c6c800a5fccb90f9 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 10:04:55 -0700 Subject: [PATCH 5/9] create brev user in startup script --- v1/providers/verda/bootstrap.go | 16 +++++++++++++++- v1/providers/verda/client_test.go | 6 +++++- v1/providers/verda/instance.go | 15 +++++++-------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/v1/providers/verda/bootstrap.go b/v1/providers/verda/bootstrap.go index a3d91fb..42c468c 100644 --- a/v1/providers/verda/bootstrap.go +++ b/v1/providers/verda/bootstrap.go @@ -13,7 +13,7 @@ const ( dockerFirewallDropInPath = "/etc/systemd/system/docker.service.d/10-brev-firewall.conf" ) -func buildStartupScript(rules v1.FirewallRules) (string, error) { +func buildStartupScript(rules v1.FirewallRules, publicKey string) (string, error) { ufwRules, dockerRules, err := firewallRuleCommands(rules.IngressRules) if err != nil { return "", err @@ -23,6 +23,20 @@ func buildStartupScript(rules v1.FirewallRules) (string, error) { script.WriteString(`#!/bin/bash set -u +if ! id brev >/dev/null 2>&1; then + useradd --create-home --shell /bin/bash brev +fi +install -d -m 0700 -o brev -g brev /home/brev/.ssh +cat > /home/brev/.ssh/authorized_keys <<'BREV_AUTHORIZED_KEYS' +`) + script.WriteString(publicKey) + script.WriteString(` +BREV_AUTHORIZED_KEYS +chown brev:brev /home/brev/.ssh/authorized_keys +chmod 0600 /home/brev/.ssh/authorized_keys +echo 'brev ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/brev +chmod 0440 /etc/sudoers.d/brev + if ! command -v ufw >/dev/null 2>&1; then apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y ufw iptables diff --git a/v1/providers/verda/client_test.go b/v1/providers/verda/client_test.go index c4d3a2f..d34d120 100644 --- a/v1/providers/verda/client_test.go +++ b/v1/providers/verda/client_test.go @@ -332,6 +332,7 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur assert.Equal(t, "ref-123", instance.RefID) assert.Equal(t, "credential-ref", instance.CloudCredRefID) assert.Equal(t, "ubuntu-24.04-cuda-12.8-open-docker", instance.ImageID) + assert.Equal(t, "brev", instance.SSHUser) assert.Equal(t, v1.LifecycleStatusPending, instance.Status.LifecycleStatus) assert.Equal(t, v1.InstanceTypeID("FIN-03-noSub-1H100.80S.22V"), instance.InstanceTypeID) assertLegacyBytesMatch(t, instance.DiskSize, instance.DiskSizeBytes) @@ -341,6 +342,9 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur assert.Equal(t, "brev-key-ref-123", createdSSHKey.Name) assert.Equal(t, authorizedKey, createdSSHKey.PublicKey) assert.Equal(t, "brev-firewall-ref-123", createdScript.Name) + assert.Contains(t, createdScript.Script, "useradd --create-home --shell /bin/bash brev") + assert.Contains(t, createdScript.Script, authorizedKey) + assert.Contains(t, createdScript.Script, "brev ALL=(ALL) NOPASSWD:ALL") assert.Equal(t, []string{"ssh-key-1"}, createdRequest.SSHKeyIDs) require.NotNil(t, createdRequest.OSVolume) assert.Equal(t, 100, createdRequest.OSVolume.Size) @@ -418,7 +422,7 @@ func TestBuildStartupScriptRejectsUnsafeRules(t *testing.T) { ToPort: 9999, IPRanges: []string{"not-a-cidr"}, }}, - }) + }, "public-key") require.Error(t, err) } diff --git a/v1/providers/verda/instance.go b/v1/providers/verda/instance.go index 3b63615..2daf182 100644 --- a/v1/providers/verda/instance.go +++ b/v1/providers/verda/instance.go @@ -22,7 +22,7 @@ const ( instanceIdentitySeparator = "_" firewallResourceNamePrefix = "brev-firewall" sshKeyResourceNamePrefix = "brev-key" - defaultSSHUser = "ubuntu" + defaultSSHUser = "brev" defaultSSHPort = 22 ) @@ -68,13 +68,17 @@ func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanc } // SSH keys are independent resources, so must be created and cleaned up separately - sshKeyID, err := c.ensureSSHKey(ctx, attrs.PublicKey, attrs.RefID) + publicKey, err := normalizeSSHPublicKey(attrs.PublicKey) + if err != nil { + return nil, err + } + sshKeyID, err := c.ensureSSHKey(ctx, publicKey, attrs.RefID) if err != nil { return nil, err } // Startup scripts are independent resources, so must be created and cleaned up separately - startupScript, err := buildStartupScript(attrs.FirewallRules) + startupScript, err := buildStartupScript(attrs.FirewallRules, publicKey) if err != nil { return nil, errors.Join(err, c.cleanupManagedResources(ctx, attrs.RefID)) } @@ -223,11 +227,6 @@ func (c *VerdaClient) selectImage(ctx context.Context, instanceType string, requ } func (c *VerdaClient) ensureSSHKey(ctx context.Context, publicKey string, refID string) (string, error) { - publicKey, err := normalizeSSHPublicKey(publicKey) - if err != nil { - return "", err - } - keys, err := c.client.SSHKeys.GetAllSSHKeys(ctx) if err != nil { return "", wrapVerdaError(err) From 578e0294590d8adb5a290ca3adce7f8244f4c25c Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 12:21:50 -0700 Subject: [PATCH 6/9] request live storage size --- v1/providers/verda/instance.go | 35 ++++++++++++---- v1/providers/verda/instancetype.go | 59 +-------------------------- v1/providers/verda/validation_test.go | 12 ++++++ 3 files changed, 39 insertions(+), 67 deletions(-) diff --git a/v1/providers/verda/instance.go b/v1/providers/verda/instance.go index 2daf182..301f68d 100644 --- a/v1/providers/verda/instance.go +++ b/v1/providers/verda/instance.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/alecthomas/units" v1 "github.com/brevdev/cloud/v1" verdago "github.com/verda-cloud/verdacloud-sdk-go/pkg/verda" "golang.org/x/crypto/ssh" @@ -114,7 +115,11 @@ func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanc if err != nil { return nil, errors.Join(wrapVerdaError(err), c.cleanupManagedResources(ctx, attrs.RefID)) } - return c.verdaInstanceToInstance(verdaInstance), nil + instance, err := c.verdaInstanceToInstance(ctx, verdaInstance) + if err != nil { + return nil, errors.Join(err, c.cleanupManagedResources(ctx, attrs.RefID)) + } + return instance, nil } func (c *VerdaClient) GetInstance(ctx context.Context, id v1.CloudProviderInstanceID) (*v1.Instance, error) { @@ -122,7 +127,11 @@ func (c *VerdaClient) GetInstance(ctx context.Context, id v1.CloudProviderInstan if err != nil { return nil, wrapVerdaError(err) } - return c.verdaInstanceToInstance(verdaInstance), nil + instance, err := c.verdaInstanceToInstance(ctx, verdaInstance) + if err != nil { + return nil, err + } + return instance, nil } func (c *VerdaClient) ListInstances(ctx context.Context, args v1.ListInstancesArgs) ([]v1.Instance, error) { @@ -133,7 +142,10 @@ func (c *VerdaClient) ListInstances(ctx context.Context, args v1.ListInstancesAr instances := make([]v1.Instance, 0, len(verdaInstances)) for i := range verdaInstances { - instance := c.verdaInstanceToInstance(&verdaInstances[i]) + instance, err := c.verdaInstanceToInstance(ctx, &verdaInstances[i]) + if err != nil { + return nil, err + } if len(args.InstanceIDs) > 0 && !slices.Contains(args.InstanceIDs, instance.CloudID) { continue } @@ -310,7 +322,7 @@ func (c *VerdaClient) cleanupManagedResources(ctx context.Context, refID string) return errors.Join(cleanupErrors...) } -func (c *VerdaClient) verdaInstanceToInstance(verdaInstance *verdago.Instance) *v1.Instance { +func (c *VerdaClient) verdaInstanceToInstance(ctx context.Context, verdaInstance *verdago.Instance) (*v1.Instance, error) { refID, cloudCredRefID := parseInstanceDescription(verdaInstance.Description) if cloudCredRefID == "" { cloudCredRefID = c.refID @@ -344,12 +356,17 @@ func (c *VerdaClient) verdaInstanceToInstance(verdaInstance *verdago.Instance) * } instance.InstanceTypeID = v1.MakeGenericInstanceTypeIDFromInstance(*instance) - if storage := storageDescriptionToStorage(verdaInstance.Storage.Description); len(storage) > 0 { - instance.DiskSize = storage[0].Size - instance.DiskSizeBytes = storage[0].SizeBytes - instance.VolumeType = storage[0].Type + if verdaInstance.OSVolumeID != nil { + volume, err := c.client.Volumes.GetVolume(ctx, *verdaInstance.OSVolumeID) + if err != nil { + return nil, wrapVerdaError(err) + } + instance.VolumeType = "nvme" + instance.DiskSize = units.Base2Bytes(volume.Size) + instance.DiskSizeBytes = v1.NewBytes(v1.BytesValue(volume.Size), v1.Gibibyte) } - return instance + + return instance, nil } func verdaStatusToLifecycleStatus(status string) v1.LifecycleStatus { diff --git a/v1/providers/verda/instancetype.go b/v1/providers/verda/instancetype.go index 1a49b13..e912b63 100644 --- a/v1/providers/verda/instancetype.go +++ b/v1/providers/verda/instancetype.go @@ -18,10 +18,7 @@ import ( const defaultCurrency = "usd" -var ( - storageSizePattern = regexp.MustCompile(`(?i)(\d+)\s*(TiB|TB|GiB|GB|MiB|MB)`) - gpuMemorySuffixPattern = regexp.MustCompile(`(?i)\s+[0-9]+GB$`) -) +var gpuMemorySuffixPattern = regexp.MustCompile(`(?i)\s+[0-9]+GB$`) func (c *VerdaClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { verdaTypes, err := c.client.InstanceTypes.Get(ctx, defaultCurrency) @@ -156,60 +153,6 @@ func currencyCode(code string) string { return "USD" } -func storageDescriptionToStorage(description string) []v1.Storage { - match := storageSizePattern.FindStringSubmatch(description) - if len(match) != 3 { - return nil - } - - size, err := strconv.ParseInt(match[1], 10, 64) - if err != nil { - return nil - } - byteUnit, ok := storageByteUnit(match[2]) - if !ok { - return nil - } - legacySize, sizeBytes := byteSizes(size, byteUnit) - - storageType := strings.TrimSpace(description) - upperDescription := strings.ToUpper(description) - switch { - case strings.Contains(upperDescription, "NVME"): - storageType = "NVMe" - case strings.Contains(upperDescription, "SSD"): - storageType = "SSD" - case strings.Contains(upperDescription, "HDD"): - storageType = "HDD" - } - - return []v1.Storage{{ - Count: 1, - Size: legacySize, - SizeBytes: sizeBytes, - Type: storageType, - }} -} - -func storageByteUnit(unit string) (v1.BytesUnit, bool) { - switch strings.ToUpper(unit) { - case "MB": - return v1.Megabyte, true - case "MIB": - return v1.Mebibyte, true - case "GB": - return v1.Gigabyte, true - case "GIB": - return v1.Gibibyte, true - case "TB": - return v1.Terabyte, true - case "TIB": - return v1.Tebibyte, true - default: - return v1.BytesUnit{}, false - } -} - func byteSizes(value int64, unit v1.BytesUnit) (units.Base2Bytes, v1.Bytes) { size := v1.NewBytes(v1.BytesValue(value), unit) return units.Base2Bytes(size.ByteCount().Int64()), size diff --git a/v1/providers/verda/validation_test.go b/v1/providers/verda/validation_test.go index a8f7cb1..29588f1 100644 --- a/v1/providers/verda/validation_test.go +++ b/v1/providers/verda/validation_test.go @@ -21,6 +21,18 @@ func TestValidationFunctions(t *testing.T) { }) } +func TestGetInstance(t *testing.T) { + checkValidationCredentials(t) + credential := validationCredential() + + client, err := credential.MakeClient(context.Background(), "") + require.NoError(t, err) + + instance, err := client.GetInstance(context.Background(), v1.CloudProviderInstanceID("b395e9e7-9a21-4ff1-a4b5-fb06d9652942")) + require.NoError(t, err) + require.NotNil(t, instance) +} + func TestInstanceLifecycleValidation(t *testing.T) { checkValidationCredentials(t) From 11531fa2194b3f18f2e24f841ff964c4f1d8f907 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 12:40:35 -0700 Subject: [PATCH 7/9] lint --- v1/providers/verda/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v1/providers/verda/instance.go b/v1/providers/verda/instance.go index 301f68d..b1a7ea6 100644 --- a/v1/providers/verda/instance.go +++ b/v1/providers/verda/instance.go @@ -29,7 +29,7 @@ const ( var resourceNameInvalidCharacters = regexp.MustCompile(`[^a-z0-9-]+`) -func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo // complexity is acceptable for a single func +func (c *VerdaClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo,funlen // complexity is acceptable for a single func location := attrs.Location if location == "" { location = c.location From f577ce7b2d67b9629a31546dd8cd1a5318546548 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 12:48:53 -0700 Subject: [PATCH 8/9] test --- v1/providers/verda/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v1/providers/verda/client_test.go b/v1/providers/verda/client_test.go index d34d120..5d29635 100644 --- a/v1/providers/verda/client_test.go +++ b/v1/providers/verda/client_test.go @@ -161,8 +161,8 @@ func TestGetInstanceTypesAndLocations(t *testing.T) { //nolint:funlen // One cat assert.Equal(t, int32(1), h100.SupportedGPUs[0].Count) assert.Equal(t, "H100", h100.SupportedGPUs[0].Name) assert.Equal(t, "H100 80GB", h100.SupportedGPUs[0].Type) - assert.Equal(t, "NVMe", h100.SupportedStorage[0].Type) - assert.Equal(t, v1.Tebibyte, h100.SupportedStorage[0].SizeBytes.Unit()) + assert.Equal(t, "nvme", h100.SupportedStorage[0].Type) + assert.Equal(t, v1.Tebibyte, h100.SupportedStorage[0].MaxSizeBytes.Unit()) assert.True(t, h100.ElasticRootVolume) assert.Equal(t, CloudProviderID, h100.Provider) assert.Equal(t, []v1.Architecture{v1.ArchitectureX86_64}, h100.SupportedArchitectures) From 541b8350eb3bdf6478f6409d786c0bfa8a9826f9 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Thu, 13 Aug 2026 13:01:51 -0700 Subject: [PATCH 9/9] test --- v1/providers/verda/client_test.go | 9 +++++++++ v1/providers/verda/instance.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/v1/providers/verda/client_test.go b/v1/providers/verda/client_test.go index 5d29635..0405a07 100644 --- a/v1/providers/verda/client_test.go +++ b/v1/providers/verda/client_test.go @@ -296,6 +296,13 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur writeJSON(t, w, createdInstance) case r.Method == http.MethodGet && r.URL.Path == "/instances/instance-1": writeJSON(t, w, createdInstance) + case r.Method == http.MethodGet && r.URL.Path == "/volumes/os-volume-1": + writeJSON(t, w, verdago.Volume{ + ID: "os-volume-1", + Size: 100, + Type: verdago.VolumeTypeNVMe, + IsOSVolume: true, + }) case r.Method == http.MethodPut && r.URL.Path == "/instances": require.NoError(t, json.NewDecoder(r.Body).Decode(&actionRequest)) w.WriteHeader(http.StatusAccepted) @@ -335,6 +342,8 @@ func TestInstanceLifecycle(t *testing.T) { //nolint:gocyclo,funlen // One fixtur assert.Equal(t, "brev", instance.SSHUser) assert.Equal(t, v1.LifecycleStatusPending, instance.Status.LifecycleStatus) assert.Equal(t, v1.InstanceTypeID("FIN-03-noSub-1H100.80S.22V"), instance.InstanceTypeID) + assert.Equal(t, "nvme", instance.VolumeType) + assert.Equal(t, v1.NewBytes(100, v1.Gibibyte), instance.DiskSizeBytes) assertLegacyBytesMatch(t, instance.DiskSize, instance.DiskSizeBytes) assert.Equal(t, "ref-123_credential-ref", createdRequest.Description) diff --git a/v1/providers/verda/instance.go b/v1/providers/verda/instance.go index b1a7ea6..3370426 100644 --- a/v1/providers/verda/instance.go +++ b/v1/providers/verda/instance.go @@ -362,7 +362,7 @@ func (c *VerdaClient) verdaInstanceToInstance(ctx context.Context, verdaInstance return nil, wrapVerdaError(err) } instance.VolumeType = "nvme" - instance.DiskSize = units.Base2Bytes(volume.Size) + instance.DiskSize = units.Base2Bytes(volume.Size) * units.GiB instance.DiskSizeBytes = v1.NewBytes(v1.BytesValue(volume.Size), v1.Gibibyte) }