diff --git a/go.mod b/go.mod index cfaadd6d..7d5e0ceb 100644 --- a/go.mod +++ b/go.mod @@ -7,26 +7,26 @@ toolchain go1.23.2 require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b github.com/bojanz/currency v1.3.1 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/gliderlabs/ssh v0.3.8 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/jarcoal/httpmock v1.4.0 github.com/nebius/gosdk v0.0.0-20250731090238-d96c0d4a5930 github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.41.0 ) require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20231030212536-12f9cba37c9d.2 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gliderlabs/ssh v0.3.8 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index 949962d3..4717a57c 100644 --- a/go.sum +++ b/go.sum @@ -58,20 +58,14 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/fluidstack/v1/client.go b/internal/fluidstack/v1/client.go index 9b343745..8e9f7c0b 100644 --- a/internal/fluidstack/v1/client.go +++ b/internal/fluidstack/v1/client.go @@ -14,8 +14,8 @@ const CloudProviderID = "fluidstack" // FluidStackCredential implements the CloudCredential interface for FluidStack type FluidStackCredential struct { - RefID string - APIKey string + RefID string `json:"ref_id"` + APIKey string `json:"api_key"` } var _ v1.CloudCredential = &FluidStackCredential{} diff --git a/internal/lambdalabs/v1/client.go b/internal/lambdalabs/v1/client.go index 75ae00ef..38a52c5c 100644 --- a/internal/lambdalabs/v1/client.go +++ b/internal/lambdalabs/v1/client.go @@ -14,8 +14,8 @@ import ( // LambdaLabsCredential implements the CloudCredential interface for Lambda Labs type LambdaLabsCredential struct { - RefID string - APIKey string + RefID string `json:"ref_id"` + APIKey string `json:"api_key"` } var _ v1.CloudCredential = &LambdaLabsCredential{} diff --git a/internal/nebius/v1/client.go b/internal/nebius/v1/client.go index 25d6d203..b3553ef2 100644 --- a/internal/nebius/v1/client.go +++ b/internal/nebius/v1/client.go @@ -9,9 +9,9 @@ import ( ) type NebiusCredential struct { - RefID string - ServiceAccountKey string // JSON service account key - ProjectID string + RefID string `json:"ref_id"` + ServiceAccountKey string `json:"service_account_key"` // JSON service account key + ProjectID string `json:"project_id"` } var _ v1.CloudCredential = &NebiusCredential{} diff --git a/internal/serialization/v1/serialization.go b/internal/serialization/v1/serialization.go new file mode 100644 index 00000000..7075c3ba --- /dev/null +++ b/internal/serialization/v1/serialization.go @@ -0,0 +1,179 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + fluidstackv1 "github.com/brevdev/cloud/internal/fluidstack/v1" + lambdalabsv1 "github.com/brevdev/cloud/internal/lambdalabs/v1" + nebiusv1 "github.com/brevdev/cloud/internal/nebius/v1" + v1 "github.com/brevdev/cloud/pkg/v1" +) + +type SerializedCredential struct { + ProviderID string `json:"provider_id"` + Data json.RawMessage `json:"data"` +} + +type SerializableCredential interface { + v1.CloudCredential + SerializeData() (interface{}, error) +} + +func SerializeCredentialData(providerID string, credData interface{}) ([]byte, error) { + if providerID == "" { + return nil, fmt.Errorf("provider_id cannot be empty") + } + + dataBytes, err := json.Marshal(credData) + if err != nil { + return nil, fmt.Errorf("failed to marshal credential data: %w", err) + } + + serialized := SerializedCredential{ + ProviderID: providerID, + Data: dataBytes, + } + + return json.Marshal(serialized) +} + +func SerializeCredentialDataToString(providerID string, credData interface{}) (string, error) { + bytes, err := SerializeCredentialData(providerID, credData) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func SerializeCredentialDataToJSON(providerID string, credData interface{}) ([]byte, error) { + return SerializeCredentialData(providerID, credData) +} + +func SerializeCredential(cred v1.CloudCredential) ([]byte, error) { + if cred == nil { + return nil, fmt.Errorf("credential cannot be nil") + } + + serializableCred, ok := cred.(SerializableCredential) + if !ok { + return nil, fmt.Errorf("credential does not implement SerializableCredential interface") + } + + providerID := string(cred.GetCloudProviderID()) + if providerID == "" { + return nil, fmt.Errorf("credential must have a valid provider ID") + } + + credData, err := serializableCred.SerializeData() + if err != nil { + return nil, fmt.Errorf("failed to serialize credential data: %w", err) + } + + return SerializeCredentialData(providerID, credData) +} + +func SerializeCredentialToString(cred v1.CloudCredential) (string, error) { + bytes, err := SerializeCredential(cred) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func SerializeCredentialToJSON(cred v1.CloudCredential) ([]byte, error) { + return SerializeCredential(cred) +} + +func DeserializeCredential(data []byte) (v1.CloudCredential, error) { + if len(data) == 0 { + return nil, fmt.Errorf("data cannot be empty") + } + + var serialized SerializedCredential + if err := json.Unmarshal(data, &serialized); err != nil { + return nil, fmt.Errorf("failed to unmarshal serialized credential: %w", err) + } + + if serialized.ProviderID == "" { + return nil, fmt.Errorf("provider_id cannot be empty") + } + + return DeserializeCredentialByProvider(serialized.ProviderID, serialized.Data) +} + +func DeserializeCredentialFromString(data string) (v1.CloudCredential, error) { + return DeserializeCredential([]byte(data)) +} + +func DeserializeCredentialFromJSON(data []byte) (v1.CloudCredential, error) { + return DeserializeCredential(data) +} + +func DeserializeCredentialByProvider(providerID string, data json.RawMessage) (v1.CloudCredential, error) { + switch providerID { + case lambdalabsv1.CloudProviderID: + return DeserializeLambdaLabsCredential(data) + case fluidstackv1.CloudProviderID: + return DeserializeFluidStackCredential(data) + case "nebius": + return DeserializeNebiusCredential(data) + default: + return nil, fmt.Errorf("unsupported provider: %s", providerID) + } +} + +func DeserializeLambdaLabsCredential(data json.RawMessage) (v1.CloudCredential, error) { + var credData lambdalabsv1.LambdaLabsCredential + if err := json.Unmarshal(data, &credData); err != nil { + return nil, fmt.Errorf("failed to unmarshal lambda labs credential data: %w", err) + } + + if credData.RefID == "" { + return nil, fmt.Errorf("lambda labs credential must have a reference ID") + } + + if credData.APIKey == "" { + return nil, fmt.Errorf("lambda labs credential must have an API key") + } + + return &credData, nil +} + +func DeserializeFluidStackCredential(data json.RawMessage) (v1.CloudCredential, error) { + var credData fluidstackv1.FluidStackCredential + if err := json.Unmarshal(data, &credData); err != nil { + return nil, fmt.Errorf("failed to unmarshal fluidstack credential data: %w", err) + } + + if credData.RefID == "" { + return nil, fmt.Errorf("fluidstack credential must have a reference ID") + } + + if credData.APIKey == "" { + return nil, fmt.Errorf("fluidstack credential must have an API key") + } + + return &credData, nil +} + +func DeserializeNebiusCredential(data json.RawMessage) (v1.CloudCredential, error) { + var credData nebiusv1.NebiusCredential + if err := json.Unmarshal(data, &credData); err != nil { + return nil, fmt.Errorf("failed to unmarshal nebius credential data: %w", err) + } + + if credData.RefID == "" { + return nil, fmt.Errorf("nebius credential must have a reference ID") + } + + if credData.ServiceAccountKey == "" { + return nil, fmt.Errorf("nebius credential must have a service account key") + } + + if credData.ProjectID == "" { + return nil, fmt.Errorf("nebius credential must have a project ID") + } + + return &credData, nil +} diff --git a/internal/serialization/v1/serialization_test.go b/internal/serialization/v1/serialization_test.go new file mode 100644 index 00000000..784a4015 --- /dev/null +++ b/internal/serialization/v1/serialization_test.go @@ -0,0 +1,405 @@ +package v1 + +import ( + "context" + "encoding/json" + "testing" + + fluidstackv1 "github.com/brevdev/cloud/internal/fluidstack/v1" + lambdalabsv1 "github.com/brevdev/cloud/internal/lambdalabs/v1" + nebiusv1 "github.com/brevdev/cloud/internal/nebius/v1" + v1 "github.com/brevdev/cloud/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockCredential struct { + providerID v1.CloudProviderID + refID string + tenantID string +} + +func (m *MockCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) { + return nil, v1.ErrNotImplemented +} + +func (m *MockCredential) GetTenantID() (string, error) { + return m.tenantID, nil +} + +func (m *MockCredential) GetReferenceID() string { + return m.refID +} + +func (m *MockCredential) GetAPIType() v1.APIType { + return v1.APITypeGlobal +} + +func (m *MockCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return nil, v1.ErrNotImplemented +} + +func (m *MockCredential) GetCloudProviderID() v1.CloudProviderID { + return m.providerID +} + +func TestSerializedCredential_Structure(t *testing.T) { + serialized := SerializedCredential{ + ProviderID: "test-provider", + Data: json.RawMessage(`{"test": "data"}`), + } + + bytes, err := json.Marshal(serialized) + require.NoError(t, err) + + var unmarshaled SerializedCredential + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err) + + assert.Equal(t, "test-provider", unmarshaled.ProviderID) + assert.Equal(t, json.RawMessage(`{"test":"data"}`), unmarshaled.Data) +} + +func TestCredentialDataStructures_JSONTags(t *testing.T) { + t.Run("LambdaLabsCredential", func(t *testing.T) { + data := lambdalabsv1.LambdaLabsCredential{ + RefID: "test-ref", + APIKey: "test-key", + } + + bytes, err := json.Marshal(data) + require.NoError(t, err) + + expected := `{"ref_id":"test-ref","api_key":"test-key"}` + assert.JSONEq(t, expected, string(bytes)) + + var unmarshaled lambdalabsv1.LambdaLabsCredential + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err) + assert.Equal(t, data, unmarshaled) + }) + + t.Run("FluidStackCredential", func(t *testing.T) { + data := fluidstackv1.FluidStackCredential{ + RefID: "test-ref", + APIKey: "test-key", + } + + bytes, err := json.Marshal(data) + require.NoError(t, err) + + expected := `{"ref_id":"test-ref","api_key":"test-key"}` + assert.JSONEq(t, expected, string(bytes)) + + var unmarshaled fluidstackv1.FluidStackCredential + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err) + assert.Equal(t, data, unmarshaled) + }) + + t.Run("NebiusCredential", func(t *testing.T) { + data := nebiusv1.NebiusCredential{ + RefID: "test-ref", + ServiceAccountKey: "test-key", + ProjectID: "test-project", + } + + bytes, err := json.Marshal(data) + require.NoError(t, err) + + expected := `{"ref_id":"test-ref","service_account_key":"test-key","project_id":"test-project"}` + assert.JSONEq(t, expected, string(bytes)) + + var unmarshaled nebiusv1.NebiusCredential + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err) + assert.Equal(t, data, unmarshaled) + }) +} + +type MockSerializableCredential struct { + MockCredential + data interface{} +} + +func (m *MockSerializableCredential) SerializeData() (interface{}, error) { + return m.data, nil +} + +func TestSerializeCredentialData(t *testing.T) { + t.Run("valid data", func(t *testing.T) { + credData := lambdalabsv1.LambdaLabsCredential{ + RefID: "test-ref", + APIKey: "test-key", + } + + bytes, err := SerializeCredentialData(lambdalabsv1.CloudProviderID, credData) + require.NoError(t, err) + + var serialized SerializedCredential + err = json.Unmarshal(bytes, &serialized) + require.NoError(t, err) + + assert.Equal(t, lambdalabsv1.CloudProviderID, serialized.ProviderID) + + var unmarshaled lambdalabsv1.LambdaLabsCredential + err = json.Unmarshal(serialized.Data, &unmarshaled) + require.NoError(t, err) + assert.Equal(t, credData, unmarshaled) + }) + + t.Run("empty provider ID", func(t *testing.T) { + _, err := SerializeCredentialData("", map[string]string{"test": "data"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "provider_id cannot be empty") + }) +} + +func TestSerializeCredentialDataToString(t *testing.T) { + credData := fluidstackv1.FluidStackCredential{ + RefID: "test-ref", + APIKey: "test-key", + } + + str, err := SerializeCredentialDataToString(fluidstackv1.CloudProviderID, credData) + require.NoError(t, err) + assert.NotEmpty(t, str) + + var serialized SerializedCredential + err = json.Unmarshal([]byte(str), &serialized) + require.NoError(t, err) + assert.Equal(t, fluidstackv1.CloudProviderID, serialized.ProviderID) +} + +func TestSerializeCredential_ErrorCases(t *testing.T) { + t.Run("nil credential", func(t *testing.T) { + _, err := SerializeCredential(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "credential cannot be nil") + }) + + t.Run("non-serializable credential", func(t *testing.T) { + cred := &MockCredential{ + providerID: lambdalabsv1.CloudProviderID, + refID: "test-ref", + } + + _, err := SerializeCredential(cred) + assert.Error(t, err) + assert.Contains(t, err.Error(), "does not implement SerializableCredential") + }) + + t.Run("empty provider ID", func(t *testing.T) { + cred := &MockSerializableCredential{ + MockCredential: MockCredential{ + providerID: "", + refID: "test-ref", + }, + data: map[string]string{"test": "data"}, + } + + _, err := SerializeCredential(cred) + assert.Error(t, err) + assert.Contains(t, err.Error(), "credential must have a valid provider ID") + }) + + t.Run("valid serializable credential", func(t *testing.T) { + cred := &MockSerializableCredential{ + MockCredential: MockCredential{ + providerID: lambdalabsv1.CloudProviderID, + refID: "test-ref", + }, + data: lambdalabsv1.LambdaLabsCredential{ + RefID: "test-ref", + APIKey: "test-key", + }, + } + + bytes, err := SerializeCredential(cred) + require.NoError(t, err) + assert.NotEmpty(t, bytes) + + var serialized SerializedCredential + err = json.Unmarshal(bytes, &serialized) + require.NoError(t, err) + assert.Equal(t, lambdalabsv1.CloudProviderID, serialized.ProviderID) + }) +} + +func TestDeserializeCredential_ErrorCases(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + _, err := DeserializeCredential([]byte{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "data cannot be empty") + }) + + t.Run("invalid JSON", func(t *testing.T) { + _, err := DeserializeCredential([]byte("invalid json")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal serialized credential") + }) + + t.Run("empty provider ID", func(t *testing.T) { + serialized := SerializedCredential{ + ProviderID: "", + Data: json.RawMessage(`{"test": "data"}`), + } + bytes, _ := json.Marshal(serialized) + + _, err := DeserializeCredential(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "provider_id cannot be empty") + }) + + t.Run("unsupported provider", func(t *testing.T) { + serialized := SerializedCredential{ + ProviderID: "unsupported-provider", + Data: json.RawMessage(`{"test": "data"}`), + } + bytes, _ := json.Marshal(serialized) + + _, err := DeserializeCredential(bytes) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported provider: unsupported-provider") + }) + + t.Run("supported provider with valid data", func(t *testing.T) { + serialized := SerializedCredential{ + ProviderID: lambdalabsv1.CloudProviderID, + Data: json.RawMessage(`{"ref_id": "test", "api_key": "key"}`), + } + bytes, _ := json.Marshal(serialized) + + cred, err := DeserializeCredential(bytes) + assert.NoError(t, err) + assert.NotNil(t, cred) + assert.Equal(t, lambdalabsv1.CloudProviderID, string(cred.GetCloudProviderID())) + }) +} + +func TestDeserializeCredentialFromString(t *testing.T) { + _, err := DeserializeCredentialFromString("") + assert.Error(t, err) + assert.Contains(t, err.Error(), "data cannot be empty") +} + +func TestDeserializeCredentialByProvider(t *testing.T) { + t.Run("lambda-labs with valid data", func(t *testing.T) { + data := json.RawMessage(`{"ref_id": "test-ref", "api_key": "test-key"}`) + cred, err := DeserializeCredentialByProvider(lambdalabsv1.CloudProviderID, data) + assert.NoError(t, err) + assert.NotNil(t, cred) + assert.Equal(t, lambdalabsv1.CloudProviderID, string(cred.GetCloudProviderID())) + }) + + t.Run("lambda-labs with missing ref_id", func(t *testing.T) { + data := json.RawMessage(`{"api_key": "test-key"}`) + _, err := DeserializeCredentialByProvider(lambdalabsv1.CloudProviderID, data) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must have a reference ID") + }) + + t.Run("lambda-labs with missing api_key", func(t *testing.T) { + data := json.RawMessage(`{"ref_id": "test-ref"}`) + _, err := DeserializeCredentialByProvider(lambdalabsv1.CloudProviderID, data) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must have an API key") + }) + + t.Run("fluidstack with valid data", func(t *testing.T) { + data := json.RawMessage(`{"ref_id": "test-ref", "api_key": "test-key"}`) + cred, err := DeserializeCredentialByProvider(fluidstackv1.CloudProviderID, data) + assert.NoError(t, err) + assert.NotNil(t, cred) + assert.Equal(t, fluidstackv1.CloudProviderID, string(cred.GetCloudProviderID())) + }) + + t.Run("nebius with valid data", func(t *testing.T) { + data := json.RawMessage(`{"ref_id": "test-ref", "service_account_key": "test-key", "project_id": "test-project"}`) + cred, err := DeserializeCredentialByProvider("nebius", data) + assert.NoError(t, err) + assert.NotNil(t, cred) + assert.Equal(t, "nebius", string(cred.GetCloudProviderID())) + }) + + t.Run("nebius with missing project_id", func(t *testing.T) { + data := json.RawMessage(`{"ref_id": "test-ref", "service_account_key": "test-key"}`) + _, err := DeserializeCredentialByProvider("nebius", data) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must have a project ID") + }) + + t.Run("invalid JSON", func(t *testing.T) { + data := json.RawMessage(`invalid json`) + _, err := DeserializeCredentialByProvider(lambdalabsv1.CloudProviderID, data) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to unmarshal") + }) +} + +func TestRoundTripSerialization(t *testing.T) { + t.Run("credential data structures", func(t *testing.T) { + testCases := []struct { + name string + providerID string + data interface{} + }{ + { + name: "lambda-labs", + providerID: lambdalabsv1.CloudProviderID, + data: lambdalabsv1.LambdaLabsCredential{ + RefID: "test-ref", + APIKey: "test-key", + }, + }, + { + name: "fluidstack", + providerID: fluidstackv1.CloudProviderID, + data: fluidstackv1.FluidStackCredential{ + RefID: "test-ref", + APIKey: "test-key", + }, + }, + { + name: "nebius", + providerID: "nebius", + data: nebiusv1.NebiusCredential{ + RefID: "test-ref", + ServiceAccountKey: "test-key", + ProjectID: "test-project", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + serialized, err := SerializeCredentialData(tc.providerID, tc.data) + require.NoError(t, err) + + var wrapper SerializedCredential + err = json.Unmarshal(serialized, &wrapper) + require.NoError(t, err) + + assert.Equal(t, tc.providerID, wrapper.ProviderID) + + switch tc.providerID { + case lambdalabsv1.CloudProviderID: + var data lambdalabsv1.LambdaLabsCredential + err = json.Unmarshal(wrapper.Data, &data) + require.NoError(t, err) + assert.Equal(t, tc.data, data) + case fluidstackv1.CloudProviderID: + var data fluidstackv1.FluidStackCredential + err = json.Unmarshal(wrapper.Data, &data) + require.NoError(t, err) + assert.Equal(t, tc.data, data) + case "nebius": + var data nebiusv1.NebiusCredential + err = json.Unmarshal(wrapper.Data, &data) + require.NoError(t, err) + assert.Equal(t, tc.data, data) + } + }) + } + }) +}