From 8778ca7104c9d9d310d4bc13336a172dd9f63b28 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:09:25 -0800 Subject: [PATCH 01/13] add missing deprecated bytes values --- v1/providers/nebius/instance.go | 1 + v1/providers/nebius/instancetype.go | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/v1/providers/nebius/instance.go b/v1/providers/nebius/instance.go index 9b07d19e..312f10ed 100644 --- a/v1/providers/nebius/instance.go +++ b/v1/providers/nebius/instance.go @@ -344,6 +344,7 @@ func (c *NebiusClient) convertNebiusInstanceToV1(ctx context.Context, instance * InstanceType: instanceTypeID, // Full instance type ID (e.g., "gpu-h100-sxm.8gpu-128vcpu-1600gb") InstanceTypeID: v1.InstanceTypeID(instanceTypeID), // Same as InstanceType - required for dev-plane lookup ImageID: imageFamily, + DiskSize: units.Base2Bytes(diskSize), DiskSizeBytes: v1.NewBytes(v1.BytesValue(diskSize), v1.Byte), // diskSize is already in bytes from getBootDiskSize Tags: tags, Status: v1.Status{LifecycleStatus: lifecycleStatus}, diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 33712ea7..4a0fcf99 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -180,6 +180,7 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform Location: location.Name, Type: instanceTypeID, // Same as ID - both use dot-separated format VCPU: preset.Resources.VcpuCount, + Memory: units.Base2Bytes(preset.Resources.MemoryGibibytes) * units.Gibibyte, MemoryBytes: v1.NewBytes(v1.BytesValue(preset.Resources.MemoryGibibytes), v1.Gibibyte), // Memory in GiB NetworkPerformance: "standard", // Default network performance IsAvailable: isAvailable, @@ -191,12 +192,14 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform // Add GPU information if available if preset.Resources.GpuCount > 0 && !isCPUOnly { + memory := getGPUMemory(gpuType) gpu := v1.GPU{ Count: preset.Resources.GpuCount, Type: gpuType, Name: gpuName, Manufacturer: v1.ManufacturerNVIDIA, // Nebius currently only supports NVIDIA GPUs - Memory: getGPUMemory(gpuType), // Populate VRAM based on GPU type + Memory: memory, // Populate VRAM based on GPU type + MemoryBytes: v1.NewBytes(v1.BytesValue(int64(memory)/int64(units.Gibibyte)), v1.Gibibyte), } instanceType.SupportedGPUs = []v1.GPU{gpu} } @@ -368,7 +371,9 @@ func (c *NebiusClient) buildSupportedStorage() []v1.Storage { // Nebius supports dynamically allocatable network SSD disks // Minimum: 50GB, Maximum: 2560GB minSize := 50 * units.GiB + minSizeBytes := v1.NewBytes(50, v1.Gibibyte) maxSize := 2560 * units.GiB + maxSizeBytes := v1.NewBytes(2560, v1.Gibibyte) // Pricing is roughly $0.10 per GB-month, which is ~$0.00014 per GB-hour pricePerGBHr, _ := currency.NewAmount("0.00014", "USD") @@ -379,6 +384,8 @@ func (c *NebiusClient) buildSupportedStorage() []v1.Storage { Count: 1, MinSize: &minSize, MaxSize: &maxSize, + MinSizeBytes: &minSizeBytes, + MaxSizeBytes: &maxSizeBytes, IsElastic: true, PricePerGBHr: &pricePerGBHr, }, From 3430015e82496e2ae38078339c3ef8f4bc417e0e Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:10:59 -0800 Subject: [PATCH 02/13] lint --- v1/providers/nebius/scripts/images_test.go | 29 ++++++++++--------- .../nebius/scripts/instancetypes_test.go | 29 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/v1/providers/nebius/scripts/images_test.go b/v1/providers/nebius/scripts/images_test.go index 3bad754d..9c21e7ad 100644 --- a/v1/providers/nebius/scripts/images_test.go +++ b/v1/providers/nebius/scripts/images_test.go @@ -17,10 +17,11 @@ import ( // Test_EnumerateImages enumerates all available images in Nebius // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// export NEBIUS_LOCATION='eu-north1' -// go test -tags scripts -v -run Test_EnumerateImages +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// export NEBIUS_LOCATION='eu-north1' +// go test -tags scripts -v -run Test_EnumerateImages func Test_EnumerateImages(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") @@ -93,7 +94,7 @@ func Test_EnumerateImages(t *testing.T) { t.Fatalf("Error marshaling JSON: %v", err) } - err = os.WriteFile(outputFile, output, 0644) + err = os.WriteFile(outputFile, output, 0o644) if err != nil { t.Fatalf("Error writing to file: %v", err) } @@ -103,9 +104,10 @@ func Test_EnumerateImages(t *testing.T) { // Test_EnumerateImagesAllRegions enumerates images across all Nebius regions // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// go test -tags scripts -v -run Test_EnumerateImagesAllRegions +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// go test -tags scripts -v -run Test_EnumerateImagesAllRegions func Test_EnumerateImagesAllRegions(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") @@ -173,7 +175,7 @@ func Test_EnumerateImagesAllRegions(t *testing.T) { t.Fatalf("Error marshaling JSON: %v", err) } - err = os.WriteFile(outputFile, output, 0644) + err = os.WriteFile(outputFile, output, 0o644) if err != nil { t.Fatalf("Error writing to file: %v", err) } @@ -183,10 +185,11 @@ func Test_EnumerateImagesAllRegions(t *testing.T) { // Test_FilterGPUImages filters images suitable for GPU instances // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// export NEBIUS_LOCATION='eu-north1' -// go test -tags scripts -v -run Test_FilterGPUImages +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// export NEBIUS_LOCATION='eu-north1' +// go test -tags scripts -v -run Test_FilterGPUImages func Test_FilterGPUImages(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") diff --git a/v1/providers/nebius/scripts/instancetypes_test.go b/v1/providers/nebius/scripts/instancetypes_test.go index d787348e..f3f10b30 100644 --- a/v1/providers/nebius/scripts/instancetypes_test.go +++ b/v1/providers/nebius/scripts/instancetypes_test.go @@ -18,9 +18,10 @@ import ( // Test_EnumerateInstanceTypes enumerates all instance types across all Nebius regions // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// go test -tags scripts -v -run Test_EnumerateInstanceTypes +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// go test -tags scripts -v -run Test_EnumerateInstanceTypes func Test_EnumerateInstanceTypes(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") @@ -120,7 +121,7 @@ func Test_EnumerateInstanceTypes(t *testing.T) { t.Fatalf("Error marshaling JSON: %v", err) } - err = os.WriteFile(outputFile, output, 0644) + err = os.WriteFile(outputFile, output, 0o644) if err != nil { t.Fatalf("Error writing to file: %v", err) } @@ -130,10 +131,11 @@ func Test_EnumerateInstanceTypes(t *testing.T) { // Test_EnumerateInstanceTypesSingleRegion enumerates instance types for a specific region // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// export NEBIUS_LOCATION='eu-north1' -// go test -tags scripts -v -run Test_EnumerateInstanceTypesSingleRegion +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// export NEBIUS_LOCATION='eu-north1' +// go test -tags scripts -v -run Test_EnumerateInstanceTypesSingleRegion func Test_EnumerateInstanceTypesSingleRegion(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") @@ -217,7 +219,7 @@ func Test_EnumerateInstanceTypesSingleRegion(t *testing.T) { t.Fatalf("Error marshaling JSON: %v", err) } - err = os.WriteFile(outputFile, output, 0644) + err = os.WriteFile(outputFile, output, 0o644) if err != nil { t.Fatalf("Error writing to file: %v", err) } @@ -227,10 +229,11 @@ func Test_EnumerateInstanceTypesSingleRegion(t *testing.T) { // Test_EnumerateGPUTypes filters and displays only GPU instance types with detailed specs // Usage: -// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' -// export NEBIUS_TENANT_ID='tenant-e00xxx' -// export NEBIUS_LOCATION='eu-north1' -// go test -tags scripts -v -run Test_EnumerateGPUTypes +// +// export NEBIUS_SERVICE_ACCOUNT_JSON='/path/to/service-account.json' +// export NEBIUS_TENANT_ID='tenant-e00xxx' +// export NEBIUS_LOCATION='eu-north1' +// go test -tags scripts -v -run Test_EnumerateGPUTypes func Test_EnumerateGPUTypes(t *testing.T) { serviceAccountJSON := os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") tenantID := os.Getenv("NEBIUS_TENANT_ID") From 7e8860763327f942e74bb0bb0bb9ca2f920097a8 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:36:59 -0800 Subject: [PATCH 03/13] add nebius validation, skip k8s tests for now --- .../aws/validation_kubernetes_test.go | 2 + v1/providers/aws/validation_network_test.go | 2 + .../nebius/validation_kubernetes_test.go | 2 + .../nebius/validation_network_test.go | 2 + v1/providers/nebius/validation_test.go | 48 +++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 v1/providers/nebius/validation_test.go diff --git a/v1/providers/aws/validation_kubernetes_test.go b/v1/providers/aws/validation_kubernetes_test.go index 1d534302..3132fbbf 100644 --- a/v1/providers/aws/validation_kubernetes_test.go +++ b/v1/providers/aws/validation_kubernetes_test.go @@ -11,6 +11,8 @@ import ( ) func TestAWSKubernetesValidation(t *testing.T) { + t.Skip("Skipping AWS Kubernetes validation tests") + if isValidationTest == "" { t.Skip("VALIDATION_TEST is not set, skipping AWS Kubernetes validation tests") } diff --git a/v1/providers/aws/validation_network_test.go b/v1/providers/aws/validation_network_test.go index 5b6932a3..ced029b0 100644 --- a/v1/providers/aws/validation_network_test.go +++ b/v1/providers/aws/validation_network_test.go @@ -16,6 +16,8 @@ var ( ) func TestAWSNetworkValidation(t *testing.T) { + t.Skip("Skipping AWS Network validation tests") + if isValidationTest == "" { t.Skip("VALIDATION_TEST is not set, skipping AWS Network validation tests") } diff --git a/v1/providers/nebius/validation_kubernetes_test.go b/v1/providers/nebius/validation_kubernetes_test.go index 1f445f74..b7b43400 100644 --- a/v1/providers/nebius/validation_kubernetes_test.go +++ b/v1/providers/nebius/validation_kubernetes_test.go @@ -11,6 +11,8 @@ import ( ) func TestKubernetesValidation(t *testing.T) { + t.Skip("Skipping Nebius Kubernetes validation tests") + isValidationTest := os.Getenv("VALIDATION_TEST") if isValidationTest == "" { t.Skip("VALIDATION_TEST is not set, skipping Nebius Kubernetes validation tests") diff --git a/v1/providers/nebius/validation_network_test.go b/v1/providers/nebius/validation_network_test.go index 5744894c..1cc6a636 100644 --- a/v1/providers/nebius/validation_network_test.go +++ b/v1/providers/nebius/validation_network_test.go @@ -16,6 +16,8 @@ var ( ) func TestNetworkValidation(t *testing.T) { + t.Skip("Skipping Nebius Network validation tests") + if isValidationTest == "" { t.Skip("VALIDATION_TEST is not set, skipping Nebius Network validation tests") } diff --git a/v1/providers/nebius/validation_test.go b/v1/providers/nebius/validation_test.go new file mode 100644 index 00000000..be6df965 --- /dev/null +++ b/v1/providers/nebius/validation_test.go @@ -0,0 +1,48 @@ +package v1 + +import ( + "os" + "testing" + + "github.com/brevdev/cloud/internal/validation" + v1 "github.com/brevdev/cloud/v1" +) + +var ( + nebiusIsValidationTest = os.Getenv("VALIDATION_TEST") + nebiusServiceAccountJSON = os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") + nebiusTenantID = os.Getenv("NEBIUS_TENANT_ID") +) + +func TestValidationFunctions(t *testing.T) { + t.Parallel() + checkSkip(t) + + config := validation.ProviderConfig{ + Credential: NewNebiusCredential("validation-test", nebiusServiceAccountJSON, nebiusTenantID), + StableIDs: []v1.InstanceTypeID{"gpu-l40s-a.1gpu-8vcpu-32gb"}, + } + + validation.RunValidationSuite(t, config) +} + +func TestInstanceLifecycleValidation(t *testing.T) { + t.Parallel() + checkSkip(t) + + config := validation.ProviderConfig{ + Credential: NewNebiusCredential("validation-test", nebiusServiceAccountJSON, nebiusTenantID), + } + + validation.RunInstanceLifecycleValidation(t, config) +} + +func checkSkip(t *testing.T) { + if nebiusIsValidationTest == "" { + t.Skip("VALIDATION_TEST is not set, skipping Nebius validation tests") + } + + if nebiusServiceAccountJSON == "" || nebiusTenantID == "" { + t.Fatal("NEBIUS_SERVICE_ACCOUNT_JSON and NEBIUS_TENANT_ID must be set") + } +} From 0e12d4f03cecb6ef716cdccde30f9c0548591e0a Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:45:24 -0800 Subject: [PATCH 04/13] fail if missing envvars --- v1/providers/nebius/validation_test.go | 41 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/v1/providers/nebius/validation_test.go b/v1/providers/nebius/validation_test.go index be6df965..67cc45a6 100644 --- a/v1/providers/nebius/validation_test.go +++ b/v1/providers/nebius/validation_test.go @@ -2,24 +2,27 @@ package v1 import ( "os" + "strings" "testing" "github.com/brevdev/cloud/internal/validation" v1 "github.com/brevdev/cloud/v1" ) -var ( - nebiusIsValidationTest = os.Getenv("VALIDATION_TEST") - nebiusServiceAccountJSON = os.Getenv("NEBIUS_SERVICE_ACCOUNT_JSON") - nebiusTenantID = os.Getenv("NEBIUS_TENANT_ID") +const ( + nebiusIsValidationTestEnvVar = "VALIDATION_TEST" + nebiusServiceAccountJSONEnvVar = "NEBIUS_SERVICE_ACCOUNT_JSON" + nebiusTenantIDEnvVar = "NEBIUS_TENANT_ID" ) func TestValidationFunctions(t *testing.T) { t.Parallel() - checkSkip(t) + if os.Getenv(nebiusIsValidationTestEnvVar) == "" { + t.Skipf("%s is not set, skipping Nebius validation tests", nebiusIsValidationTestEnvVar) + } config := validation.ProviderConfig{ - Credential: NewNebiusCredential("validation-test", nebiusServiceAccountJSON, nebiusTenantID), + Credential: newNebiusCredential(t), StableIDs: []v1.InstanceTypeID{"gpu-l40s-a.1gpu-8vcpu-32gb"}, } @@ -28,21 +31,33 @@ func TestValidationFunctions(t *testing.T) { func TestInstanceLifecycleValidation(t *testing.T) { t.Parallel() - checkSkip(t) + if os.Getenv(nebiusIsValidationTestEnvVar) == "" { + t.Skipf("%s is not set, skipping Nebius validation tests", nebiusIsValidationTestEnvVar) + } config := validation.ProviderConfig{ - Credential: NewNebiusCredential("validation-test", nebiusServiceAccountJSON, nebiusTenantID), + Credential: newNebiusCredential(t), } validation.RunInstanceLifecycleValidation(t, config) } -func checkSkip(t *testing.T) { - if nebiusIsValidationTest == "" { - t.Skip("VALIDATION_TEST is not set, skipping Nebius validation tests") +func newNebiusCredential(t *testing.T) *NebiusCredential { + serviceAccountJSON := os.Getenv(nebiusServiceAccountJSONEnvVar) + tenantID := os.Getenv(nebiusTenantIDEnvVar) + + var fail bool + if serviceAccountJSON == "" { + t.Logf("%s must be set", nebiusServiceAccountJSONEnvVar) + fail = true + } + if tenantID == "" { + t.Logf("%s must be set", nebiusTenantIDEnvVar) + fail = true } - if nebiusServiceAccountJSON == "" || nebiusTenantID == "" { - t.Fatal("NEBIUS_SERVICE_ACCOUNT_JSON and NEBIUS_TENANT_ID must be set") + if fail { + t.Fatal("Missing environment variables") } + return NewNebiusCredential("validation-test", serviceAccountJSON, tenantID) } From 506f29c8faf7b18009573e40b2d136c59683bf05 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:46:16 -0800 Subject: [PATCH 05/13] pass envvars to nebius test --- .github/workflows/validation-nebius.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validation-nebius.yml b/.github/workflows/validation-nebius.yml index b9a40e35..f2894a29 100644 --- a/.github/workflows/validation-nebius.yml +++ b/.github/workflows/validation-nebius.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" - name: Cache Go modules uses: actions/cache@v4 @@ -47,6 +47,8 @@ jobs: NEBIUS_SERVICE_ACCOUNT_ID: ${{ secrets.NEBIUS_SERVICE_ACCOUNT_ID }} NEBIUS_PROJECT_ID: ${{ secrets.NEBIUS_PROJECT_ID }} TEST_USER_PRIVATE_KEY_PEM_BASE64: ${{ secrets.TEST_USER_PRIVATE_KEY_PEM_BASE64 }} + NEBIUS_SERVICE_ACCOUNT_JSON: ${{ secrets.NEBIUS_SERVICE_ACCOUNT_JSON }} + NEBIUS_TENANT_ID: ${{ secrets.NEBIUS_TENANT_ID }} VALIDATION_TEST: true run: | cd v1/providers/nebius From d4ae99b7c95ee2cac4661c5f17e1baecb4c16aa4 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:46:55 -0800 Subject: [PATCH 06/13] lint --- v1/providers/nebius/validation_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v1/providers/nebius/validation_test.go b/v1/providers/nebius/validation_test.go index 67cc45a6..8bcbf304 100644 --- a/v1/providers/nebius/validation_test.go +++ b/v1/providers/nebius/validation_test.go @@ -2,7 +2,6 @@ package v1 import ( "os" - "strings" "testing" "github.com/brevdev/cloud/internal/validation" From 7c37e51472f79b32dd5379e2a97c5607ba6683f6 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 09:53:08 -0800 Subject: [PATCH 07/13] add location to validation test --- v1/providers/nebius/validation_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v1/providers/nebius/validation_test.go b/v1/providers/nebius/validation_test.go index 8bcbf304..8662019f 100644 --- a/v1/providers/nebius/validation_test.go +++ b/v1/providers/nebius/validation_test.go @@ -22,6 +22,7 @@ func TestValidationFunctions(t *testing.T) { config := validation.ProviderConfig{ Credential: newNebiusCredential(t), + Location: "eu-north1", StableIDs: []v1.InstanceTypeID{"gpu-l40s-a.1gpu-8vcpu-32gb"}, } @@ -36,6 +37,7 @@ func TestInstanceLifecycleValidation(t *testing.T) { config := validation.ProviderConfig{ Credential: newNebiusCredential(t), + Location: "eu-north1", } validation.RunInstanceLifecycleValidation(t, config) From 8ac7b22552825ed4a84b2c702ac5d29cd8cae9cf Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 10:24:13 -0800 Subject: [PATCH 08/13] tests --- v1/providers/nebius/client.go | 5 +++++ v1/providers/nebius/instancetype.go | 6 ++++-- v1/providers/nebius/validation_test.go | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/v1/providers/nebius/client.go b/v1/providers/nebius/client.go index 7ef58561..0198f09e 100644 --- a/v1/providers/nebius/client.go +++ b/v1/providers/nebius/client.go @@ -130,6 +130,11 @@ func findProjectForRegion(ctx context.Context, sdk *gosdk.SDK, tenantID, region return "", fmt.Errorf("no projects found in tenant %s", tenantID) } + // TODO: I don't think the following code is correct, as the use of monikers like "default" or "default-project" + // or even the nebius convention of "default-project-{region}" will work with the nebius SDK. The SDK expects + // the project *ID* to be used, not the name. If we get to this part of the code, it likely implies that we will + // not be able to proceed. + // Sort projects by ID for deterministic selection // This ensures CreateInstance and ListInstances always use the same project! sort.Slice(projects, func(i, j int) bool { diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 4a0fcf99..c019c449 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -176,7 +176,6 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform // Convert Nebius platform preset to our InstanceType format instanceType := v1.InstanceType{ - ID: v1.InstanceTypeID(instanceTypeID), // Dot-separated format (e.g., "gpu-h100-sxm.8gpu-128vcpu-1600gb") Location: location.Name, Type: instanceTypeID, // Same as ID - both use dot-separated format VCPU: preset.Resources.VcpuCount, @@ -210,6 +209,9 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform instanceType.BasePrice = pricing } + // Make the instance type ID + instanceType.ID = v1.MakeGenericInstanceTypeID(instanceType) + instanceTypes = append(instanceTypes, instanceType) } } @@ -403,7 +405,7 @@ func (c *NebiusClient) applyInstanceTypeFilters(instanceTypes []v1.InstanceType, if len(args.InstanceTypes) > 0 { found := false for _, requestedType := range args.InstanceTypes { - if string(instanceType.ID) == requestedType { + if instanceType.Type == requestedType { found = true break } diff --git a/v1/providers/nebius/validation_test.go b/v1/providers/nebius/validation_test.go index 8662019f..460ae98a 100644 --- a/v1/providers/nebius/validation_test.go +++ b/v1/providers/nebius/validation_test.go @@ -23,7 +23,7 @@ func TestValidationFunctions(t *testing.T) { config := validation.ProviderConfig{ Credential: newNebiusCredential(t), Location: "eu-north1", - StableIDs: []v1.InstanceTypeID{"gpu-l40s-a.1gpu-8vcpu-32gb"}, + StableIDs: []v1.InstanceTypeID{"eu-north1-noSub-gpu-l40s-a.1gpu-8vcpu-32gb"}, } validation.RunValidationSuite(t, config) From d1869dbc1a5a299f205229ba02826e1be71842ff Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 10:36:33 -0800 Subject: [PATCH 09/13] boot disk default size --- v1/providers/nebius/instance.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v1/providers/nebius/instance.go b/v1/providers/nebius/instance.go index 312f10ed..f46b2537 100644 --- a/v1/providers/nebius/instance.go +++ b/v1/providers/nebius/instance.go @@ -1151,6 +1151,10 @@ func (c *NebiusClient) createBootDisk(ctx context.Context, attrs v1.CreateInstan // buildDiskCreateRequest builds a disk creation request, trying image family first, then image ID func (c *NebiusClient) buildDiskCreateRequest(ctx context.Context, diskName string, attrs v1.CreateInstanceAttrs) (*compute.CreateDiskRequest, error) { + if attrs.DiskSize == 0 { + attrs.DiskSize = 1280 * units.Gibibyte // Defaulted by the Nebius Console + } + baseReq := &compute.CreateDiskRequest{ Metadata: &common.ResourceMetadata{ ParentId: c.projectID, From 5beaaae429d41538aba6d046658498bc9b95b62d Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 11:17:12 -0800 Subject: [PATCH 10/13] tests --- v1/providers/nebius/image.go | 18 ++++++++++++------ v1/providers/nebius/instancetype.go | 19 +++++++++---------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/v1/providers/nebius/image.go b/v1/providers/nebius/image.go index 90e2c901..eda002c7 100644 --- a/v1/providers/nebius/image.go +++ b/v1/providers/nebius/image.go @@ -202,6 +202,13 @@ func getImageDescription(image *compute.Image) string { return "" } +const ( + ArchitectureX86_64 = "x86_64" + ArchitectureArm64 = "arm64" + ArchitectureAMD64 = "amd64" + ArchitectureAArch64 = "aarch64" +) + // extractArchitecture extracts architecture information from image metadata func extractArchitecture(image *compute.Image) string { // Check labels for architecture info @@ -217,16 +224,15 @@ func extractArchitecture(image *compute.Image) string { // Infer from image name if image.Metadata != nil { name := strings.ToLower(image.Metadata.Name) - if strings.Contains(name, "arm64") || strings.Contains(name, "aarch64") { - return "arm64" + if strings.Contains(name, ArchitectureArm64) || strings.Contains(name, ArchitectureAArch64) { + return ArchitectureArm64 } - if strings.Contains(name, "x86_64") || strings.Contains(name, "amd64") { - //nolint:goconst // Architecture string used in detection and returned as default - return "x86_64" + if strings.Contains(name, ArchitectureX86_64) || strings.Contains(name, ArchitectureAMD64) { + return ArchitectureX86_64 } } - return "x86_64" + return ArchitectureX86_64 } // filterImagesByArchitectures filters images by multiple architectures diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index c019c449..103d6655 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -29,7 +29,13 @@ func (c *NebiusClient) GetInstanceTypes(ctx context.Context, args v1.GetInstance // Default behavior: check ALL regions to show all available quota var locations []v1.Location - if len(args.Locations) > 0 && !args.Locations.IsAll() { + if args.Locations.IsAll() { //nolint:gocritic // prefer if statement over switch statement + allLocations, err := c.GetLocations(ctx, v1.GetLocationsArgs{}) + if err != nil { + return nil, errors.WrapAndTrace(err) + } + locations = allLocations + } else if len(args.Locations) > 0 { // User requested specific locations - filter to those allLocations, err := c.GetLocations(ctx, v1.GetLocationsArgs{}) if err == nil { @@ -48,15 +54,8 @@ func (c *NebiusClient) GetInstanceTypes(ctx context.Context, args v1.GetInstance locations = []v1.Location{{Name: c.location}} } } else { - // Default behavior: enumerate ALL regions for quota-aware discovery - // This shows users all instance types they have quota for, regardless of region - allLocations, err := c.GetLocations(ctx, v1.GetLocationsArgs{}) - if err == nil { - locations = allLocations - } else { - // Fallback to client's configured location if we can't get all locations - locations = []v1.Location{{Name: c.location}} - } + // Fallback to client's configured location if we can't get all locations + locations = []v1.Location{{Name: c.location}} } // Get quota information for all regions From a2fd3ad05ea5abd321723109a67d4273afef7b15 Mon Sep 17 00:00:00 2001 From: Drew Malin Date: Wed, 26 Nov 2025 11:41:19 -0800 Subject: [PATCH 11/13] keys for nebius test --- .github/workflows/validation-nebius.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/validation-nebius.yml b/.github/workflows/validation-nebius.yml index f2894a29..781cd3bf 100644 --- a/.github/workflows/validation-nebius.yml +++ b/.github/workflows/validation-nebius.yml @@ -49,6 +49,8 @@ jobs: TEST_USER_PRIVATE_KEY_PEM_BASE64: ${{ secrets.TEST_USER_PRIVATE_KEY_PEM_BASE64 }} NEBIUS_SERVICE_ACCOUNT_JSON: ${{ secrets.NEBIUS_SERVICE_ACCOUNT_JSON }} NEBIUS_TENANT_ID: ${{ secrets.NEBIUS_TENANT_ID }} + TEST_PRIVATE_KEY_BASE64: ${{ secrets.TEST_PRIVATE_KEY_BASE64 }} + TEST_PUBLIC_KEY_BASE64: ${{ secrets.TEST_PUBLIC_KEY_BASE64 }} VALIDATION_TEST: true run: | cd v1/providers/nebius From 08c2c55df75b08f8fb1a9d30aa1c874185db824c Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Thu, 4 Dec 2025 14:53:15 -0800 Subject: [PATCH 12/13] remove ref to ubuntu18 and allow ubuntu 24 --- v1/image.go | 2 +- v1/providers/nebius/instance.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/v1/image.go b/v1/image.go index d781e852..7ec4c493 100644 --- a/v1/image.go +++ b/v1/image.go @@ -112,7 +112,7 @@ func validateOSVersion(ctx context.Context, sshClient *ssh.Client) (string, erro } osVersion := strings.Trim(parts[1], "\"") - ubuntuRegex := regexp.MustCompile(`Ubuntu 20\.04|22\.04`) + ubuntuRegex := regexp.MustCompile(`Ubuntu 20\.04|22\.04|24\.04`) if !ubuntuRegex.MatchString(osVersion) { return "", fmt.Errorf("expected Ubuntu 20.04 or 22.04, got: %s", osVersion) } diff --git a/v1/providers/nebius/instance.go b/v1/providers/nebius/instance.go index f46b2537..5047341a 100644 --- a/v1/providers/nebius/instance.go +++ b/v1/providers/nebius/instance.go @@ -1558,7 +1558,6 @@ func (c *NebiusClient) resolveImageFamily(ctx context.Context, imageID string) ( "mk8s-worker-node-v-1-31-ubuntu24.04-cuda12", "ubuntu22.04", "ubuntu20.04", - "ubuntu18.04", } // Check if ImageID is already a known family name @@ -1605,9 +1604,6 @@ func (c *NebiusClient) resolveImageFamily(ctx context.Context, imageID string) ( if strings.Contains(name, "ubuntu20") || strings.Contains(name, "ubuntu-20") { return "ubuntu20.04", nil } - if strings.Contains(name, "ubuntu18") || strings.Contains(name, "ubuntu-18") { - return "ubuntu18.04", nil - } } // Default fallback - use the original ImageID as family From 138c8e609e7409b3e1f1e5f195d7561ca6b02d27 Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Thu, 4 Dec 2025 15:21:01 -0800 Subject: [PATCH 13/13] only get x86 in image validation --- internal/validation/suite.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/validation/suite.go b/internal/validation/suite.go index e158ad52..dfcf2f9d 100644 --- a/internal/validation/suite.go +++ b/internal/validation/suite.go @@ -65,7 +65,11 @@ func RunInstanceLifecycleValidation(t *testing.T, config ProviderConfig) { capabilities, err := client.GetCapabilities(ctx) require.NoError(t, err) - types, err := client.GetInstanceTypes(ctx, v1.GetInstanceTypeArgs{}) + types, err := client.GetInstanceTypes(ctx, v1.GetInstanceTypeArgs{ + ArchitectureFilter: &v1.ArchitectureFilter{ + IncludeArchitectures: []v1.Architecture{v1.ArchitectureX86_64}, + }, + }) require.NoError(t, err) require.NotEmpty(t, types, "Should have instance types")