diff --git a/api/v1alpha2/config/config.go b/api/v1alpha2/config/config.go new file mode 100644 index 000000000..87c827bcd --- /dev/null +++ b/api/v1alpha2/config/config.go @@ -0,0 +1,77 @@ +package v1alpha2 + +import ( + "maps" + + "github.com/windsorcli/cli/api/v1alpha2/config/providers" + "github.com/windsorcli/cli/api/v1alpha2/config/secrets" + "github.com/windsorcli/cli/api/v1alpha2/config/terraform" + "github.com/windsorcli/cli/api/v1alpha2/config/workstation" +) + +// Config represents the entire configuration +type Config struct { + Version string `yaml:"version"` + Workstation *workstation.WorkstationConfig `yaml:"workstation,omitempty"` + Environment map[string]string `yaml:"environment,omitempty"` + Secrets *secrets.SecretsConfig `yaml:"secrets,omitempty"` + Providers *providers.ProvidersConfig `yaml:"providers,omitempty"` + Terraform *terraform.TerraformConfig `yaml:"terraform,omitempty"` +} + +// Merge performs a deep merge of the current Config with another Config. +func (base *Config) Merge(overlay *Config) { + if overlay == nil { + return + } + if overlay.Environment != nil { + if base.Environment == nil { + base.Environment = make(map[string]string) + } + maps.Copy(base.Environment, overlay.Environment) + } + if overlay.Secrets != nil { + if base.Secrets == nil { + base.Secrets = &secrets.SecretsConfig{} + } + base.Secrets.Merge(overlay.Secrets) + } + if overlay.Providers != nil { + if base.Providers == nil { + base.Providers = &providers.ProvidersConfig{} + } + base.Providers.Merge(overlay.Providers) + } + if overlay.Terraform != nil { + if base.Terraform == nil { + base.Terraform = &terraform.TerraformConfig{} + } + base.Terraform.Merge(overlay.Terraform) + } + if overlay.Workstation != nil { + if base.Workstation == nil { + base.Workstation = &workstation.WorkstationConfig{} + } + base.Workstation.Merge(overlay.Workstation) + } +} + +// DeepCopy creates a deep copy of the Config object +func (c *Config) DeepCopy() *Config { + if c == nil { + return nil + } + var environmentCopy map[string]string + if c.Environment != nil { + environmentCopy = make(map[string]string, len(c.Environment)) + maps.Copy(environmentCopy, c.Environment) + } + return &Config{ + Version: c.Version, + Workstation: c.Workstation.DeepCopy(), + Environment: environmentCopy, + Secrets: c.Secrets.DeepCopy(), + Providers: c.Providers.DeepCopy(), + Terraform: c.Terraform.DeepCopy(), + } +} diff --git a/api/v1alpha2/config/config_test.go b/api/v1alpha2/config/config_test.go new file mode 100644 index 000000000..53a675430 --- /dev/null +++ b/api/v1alpha2/config/config_test.go @@ -0,0 +1,236 @@ +package v1alpha2 + +import ( + "testing" + + "github.com/windsorcli/cli/api/v1alpha2/config/providers" + "github.com/windsorcli/cli/api/v1alpha2/config/secrets" + "github.com/windsorcli/cli/api/v1alpha2/config/terraform" + "github.com/windsorcli/cli/api/v1alpha2/config/workstation" +) + +// TestConfig_Merge tests the Merge functionality of the Config struct +func TestConfig_Merge(t *testing.T) { + t.Run("HandleNilOverlayGracefully", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + Environment: map[string]string{ + "key1": "value1", + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Version != original.Version { + t.Errorf("expected version to remain unchanged, got %s", base.Version) + } + if base.Environment["key1"] != original.Environment["key1"] { + t.Errorf("expected environment to remain unchanged") + } + }) + + t.Run("MergeEnvironmentVariables", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + Environment: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + overlay := &Config{ + Environment: map[string]string{ + "key2": "newvalue2", + "key3": "value3", + }, + } + + base.Merge(overlay) + + if base.Environment["key1"] != "value1" { + t.Errorf("expected key1 to remain unchanged, got %s", base.Environment["key1"]) + } + if base.Environment["key2"] != "newvalue2" { + t.Errorf("expected key2 to be updated, got %s", base.Environment["key2"]) + } + if base.Environment["key3"] != "value3" { + t.Errorf("expected key3 to be added, got %s", base.Environment["key3"]) + } + }) + + t.Run("InitializeEnvironmentMapWhenNil", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + } + overlay := &Config{ + Environment: map[string]string{ + "key1": "value1", + }, + } + + base.Merge(overlay) + + if base.Environment == nil { + t.Error("expected environment map to be initialized") + } + if base.Environment["key1"] != "value1" { + t.Errorf("expected key1 to be set, got %s", base.Environment["key1"]) + } + }) + + t.Run("MergeSecretsConfiguration", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + } + overlay := &Config{ + Secrets: &secrets.SecretsConfig{}, + } + + base.Merge(overlay) + + if base.Secrets == nil { + t.Error("expected secrets to be initialized") + } + }) + + t.Run("MergeProvidersConfiguration", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + } + overlay := &Config{ + Providers: &providers.ProvidersConfig{}, + } + + base.Merge(overlay) + + if base.Providers == nil { + t.Error("expected Providers config to be initialized") + } + }) + + t.Run("MergeTerraformConfiguration", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + } + overlay := &Config{ + Terraform: &terraform.TerraformConfig{}, + } + + base.Merge(overlay) + + if base.Terraform == nil { + t.Error("expected Terraform config to be initialized") + } + }) + + t.Run("MergeWorkstationConfiguration", func(t *testing.T) { + base := &Config{ + Version: "v1alpha2", + } + overlay := &Config{ + Workstation: &workstation.WorkstationConfig{}, + } + + base.Merge(overlay) + + if base.Workstation == nil { + t.Error("expected Workstation config to be initialized") + } + }) +} + +// TestConfig_Copy tests the Copy functionality of the Config struct +func TestConfig_Copy(t *testing.T) { + t.Run("ReturnNilForNilConfig", func(t *testing.T) { + var config *Config + result := config.DeepCopy() + + if result != nil { + t.Error("expected nil result for nil config") + } + }) + + t.Run("CreateDeepCopyOfConfig", func(t *testing.T) { + original := &Config{ + Version: "v1alpha2", + Environment: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + + copy := original.DeepCopy() + + if copy == nil { + t.Fatal("expected non-nil copy") + } + if copy == original { + t.Error("expected different pointer") + } + if copy.Version != original.Version { + t.Errorf("expected version to be copied, got %s", copy.Version) + } + if copy.Environment == nil { + t.Error("expected environment to be copied") + } + if copy.Environment["key1"] != original.Environment["key1"] { + t.Errorf("expected environment values to be copied") + } + }) + + t.Run("HandleNilEnvironmentInCopy", func(t *testing.T) { + original := &Config{ + Version: "v1alpha2", + } + + copy := original.DeepCopy() + + if copy.Environment != nil { + t.Error("expected nil environment in copy") + } + }) + + t.Run("CopyAllConfigSections", func(t *testing.T) { + original := &Config{ + Version: "v1alpha2", + Environment: map[string]string{ + "key1": "value1", + }, + Secrets: &secrets.SecretsConfig{}, + Providers: &providers.ProvidersConfig{}, + Terraform: &terraform.TerraformConfig{}, + Workstation: &workstation.WorkstationConfig{}, + } + + copy := original.DeepCopy() + + if copy.Secrets == nil { + t.Error("expected secrets to be copied") + } + if copy.Providers == nil { + t.Error("expected Providers config to be copied") + } + if copy.Terraform == nil { + t.Error("expected Terraform config to be copied") + } + if copy.Workstation == nil { + t.Error("expected Workstation config to be copied") + } + }) + + t.Run("CreateIndependentEnvironmentMap", func(t *testing.T) { + original := &Config{ + Version: "v1alpha2", + Environment: map[string]string{ + "key1": "value1", + }, + } + + copy := original.DeepCopy() + copy.Environment["key1"] = "modified" + + if original.Environment["key1"] == "modified" { + t.Error("expected original environment to remain unchanged") + } + }) +} diff --git a/api/v1alpha2/config/providers/aws/aws.go b/api/v1alpha2/config/providers/aws/aws.go new file mode 100644 index 000000000..505e073c4 --- /dev/null +++ b/api/v1alpha2/config/providers/aws/aws.go @@ -0,0 +1,69 @@ +package aws + +// AWSConfig represents the AWS configuration +type AWSConfig struct { + // Enabled indicates whether AWS integration is enabled. + Enabled *bool `yaml:"enabled,omitempty"` + + // AWSEndpointURL specifies the custom endpoint URL for AWS services. + AWSEndpointURL *string `yaml:"aws_endpoint_url,omitempty"` + + // AWSProfile defines the AWS CLI profile to use for authentication. + AWSProfile *string `yaml:"aws_profile,omitempty"` + + // S3Hostname sets the custom hostname for the S3 service. + S3Hostname *string `yaml:"s3_hostname,omitempty"` + + // MWAAEndpoint specifies the endpoint for Managed Workflows for Apache Airflow. + MWAAEndpoint *string `yaml:"mwaa_endpoint,omitempty"` +} + +// Merge performs a deep merge of the current AWSConfig with another AWSConfig. +func (base *AWSConfig) Merge(overlay *AWSConfig) { + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.AWSEndpointURL != nil { + base.AWSEndpointURL = overlay.AWSEndpointURL + } + if overlay.AWSProfile != nil { + base.AWSProfile = overlay.AWSProfile + } + if overlay.S3Hostname != nil { + base.S3Hostname = overlay.S3Hostname + } + if overlay.MWAAEndpoint != nil { + base.MWAAEndpoint = overlay.MWAAEndpoint + } +} + +// DeepCopy creates a deep copy of the AWSConfig object +func (c *AWSConfig) DeepCopy() *AWSConfig { + if c == nil { + return nil + } + copied := &AWSConfig{} + + if c.Enabled != nil { + enabledCopy := *c.Enabled + copied.Enabled = &enabledCopy + } + if c.AWSEndpointURL != nil { + urlCopy := *c.AWSEndpointURL + copied.AWSEndpointURL = &urlCopy + } + if c.AWSProfile != nil { + profileCopy := *c.AWSProfile + copied.AWSProfile = &profileCopy + } + if c.S3Hostname != nil { + hostnameCopy := *c.S3Hostname + copied.S3Hostname = &hostnameCopy + } + if c.MWAAEndpoint != nil { + endpointCopy := *c.MWAAEndpoint + copied.MWAAEndpoint = &endpointCopy + } + + return copied +} diff --git a/api/v1alpha2/config/providers/aws/aws_test.go b/api/v1alpha2/config/providers/aws/aws_test.go new file mode 100644 index 000000000..90a77f622 --- /dev/null +++ b/api/v1alpha2/config/providers/aws/aws_test.go @@ -0,0 +1,132 @@ +package aws + +import ( + "testing" +) + +func TestAWSConfig_Merge(t *testing.T) { + t.Run("MergeWithNoNils", func(t *testing.T) { + base := &AWSConfig{ + Enabled: ptrBool(true), + AWSEndpointURL: ptrString("https://base.aws.endpoint"), + AWSProfile: ptrString("base-profile"), + S3Hostname: ptrString("base-s3-hostname"), + MWAAEndpoint: ptrString("base-mwaa-endpoint"), + } + + overlay := &AWSConfig{ + Enabled: ptrBool(false), + AWSEndpointURL: ptrString("https://overlay.aws.endpoint"), + AWSProfile: ptrString("overlay-profile"), + S3Hostname: ptrString("overlay-s3-hostname"), + MWAAEndpoint: ptrString("overlay-mwaa-endpoint"), + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != false { + t.Errorf("Enabled mismatch: expected false, got %v", *base.Enabled) + } + if base.AWSEndpointURL == nil || *base.AWSEndpointURL != "https://overlay.aws.endpoint" { + t.Errorf("AWSEndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWSEndpointURL) + } + if base.AWSProfile == nil || *base.AWSProfile != "overlay-profile" { + t.Errorf("AWSProfile mismatch: expected 'overlay-profile', got '%s'", *base.AWSProfile) + } + if base.S3Hostname == nil || *base.S3Hostname != "overlay-s3-hostname" { + t.Errorf("S3Hostname mismatch: expected 'overlay-s3-hostname', got '%s'", *base.S3Hostname) + } + if base.MWAAEndpoint == nil || *base.MWAAEndpoint != "overlay-mwaa-endpoint" { + t.Errorf("MWAAEndpoint mismatch: expected 'overlay-mwaa-endpoint', got '%s'", *base.MWAAEndpoint) + } + }) + + t.Run("MergeWithAllNils", func(t *testing.T) { + base := &AWSConfig{ + Enabled: nil, + AWSEndpointURL: nil, + AWSProfile: nil, + S3Hostname: nil, + MWAAEndpoint: nil, + } + + overlay := &AWSConfig{ + Enabled: nil, + AWSEndpointURL: nil, + AWSProfile: nil, + S3Hostname: nil, + MWAAEndpoint: nil, + } + + base.Merge(overlay) + + if base.Enabled != nil { + t.Errorf("Enabled mismatch: expected nil, got %v", base.Enabled) + } + if base.AWSEndpointURL != nil { + t.Errorf("AWSEndpointURL mismatch: expected nil, got '%s'", *base.AWSEndpointURL) + } + if base.AWSProfile != nil { + t.Errorf("AWSProfile mismatch: expected nil, got '%s'", *base.AWSProfile) + } + if base.S3Hostname != nil { + t.Errorf("S3Hostname mismatch: expected nil, got '%s'", *base.S3Hostname) + } + if base.MWAAEndpoint != nil { + t.Errorf("MWAAEndpoint mismatch: expected nil, got '%s'", *base.MWAAEndpoint) + } + }) +} + +func TestAWSConfig_Copy(t *testing.T) { + t.Run("CopyWithNonNilValues", func(t *testing.T) { + original := &AWSConfig{ + Enabled: ptrBool(true), + AWSEndpointURL: ptrString("https://original.aws.endpoint"), + AWSProfile: ptrString("original-profile"), + S3Hostname: ptrString("original-s3-hostname"), + MWAAEndpoint: ptrString("original-mwaa-endpoint"), + } + + copy := original.DeepCopy() + + if original.Enabled == nil || copy.Enabled == nil || *original.Enabled != *copy.Enabled { + t.Errorf("Enabled mismatch: expected %v, got %v", *original.Enabled, *copy.Enabled) + } + if original.AWSEndpointURL == nil || copy.AWSEndpointURL == nil || *original.AWSEndpointURL != *copy.AWSEndpointURL { + t.Errorf("AWSEndpointURL mismatch: expected %v, got %v", *original.AWSEndpointURL, *copy.AWSEndpointURL) + } + if original.AWSProfile == nil || copy.AWSProfile == nil || *original.AWSProfile != *copy.AWSProfile { + t.Errorf("AWSProfile mismatch: expected %v, got %v", *original.AWSProfile, *copy.AWSProfile) + } + if original.S3Hostname == nil || copy.S3Hostname == nil || *original.S3Hostname != *copy.S3Hostname { + t.Errorf("S3Hostname mismatch: expected %v, got %v", *original.S3Hostname, *copy.S3Hostname) + } + if original.MWAAEndpoint == nil || copy.MWAAEndpoint == nil || *original.MWAAEndpoint != *copy.MWAAEndpoint { + t.Errorf("MWAAEndpoint mismatch: expected %v, got %v", *original.MWAAEndpoint, *copy.MWAAEndpoint) + } + + // Modify the copy and ensure original is unchanged + copy.Enabled = ptrBool(false) + if original.Enabled == nil || *original.Enabled == *copy.Enabled { + t.Errorf("Original Enabled was modified: expected %v, got %v", true, *copy.Enabled) + } + }) + + t.Run("CopyNil", func(t *testing.T) { + var original *AWSConfig = nil + mockCopy := original.DeepCopy() + if mockCopy != nil { + t.Errorf("Mock copy should be nil, got %v", mockCopy) + } + }) +} + +// Helper functions to create pointers for basic types +func ptrString(s string) *string { + return &s +} + +func ptrBool(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/providers/azure/azure.go b/api/v1alpha2/config/providers/azure/azure.go new file mode 100644 index 000000000..1d6d57f17 --- /dev/null +++ b/api/v1alpha2/config/providers/azure/azure.go @@ -0,0 +1,62 @@ +package azure + +// AzureConfig represents the Azure configuration +type AzureConfig struct { + // Enabled indicates whether Azure integration is enabled. + Enabled *bool `yaml:"enabled,omitempty"` + + // SubscriptionID is the Azure subscription identifier + SubscriptionID *string `yaml:"subscription_id,omitempty"` + + // TenantID is the Azure tenant identifier + TenantID *string `yaml:"tenant_id,omitempty"` + + // Environment specifies the Azure cloud environment (e.g. "public", "usgovernment") + Environment *string `yaml:"environment,omitempty"` +} + +// Merge performs a deep merge of the current AzureConfig with another AzureConfig. +func (base *AzureConfig) Merge(overlay *AzureConfig) { + if overlay == nil { + return + } + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.SubscriptionID != nil { + base.SubscriptionID = overlay.SubscriptionID + } + if overlay.TenantID != nil { + base.TenantID = overlay.TenantID + } + if overlay.Environment != nil { + base.Environment = overlay.Environment + } +} + +// DeepCopy creates a deep copy of the AzureConfig object +func (c *AzureConfig) DeepCopy() *AzureConfig { + if c == nil { + return nil + } + copied := &AzureConfig{} + + if c.Enabled != nil { + enabledCopy := *c.Enabled + copied.Enabled = &enabledCopy + } + if c.SubscriptionID != nil { + subscriptionCopy := *c.SubscriptionID + copied.SubscriptionID = &subscriptionCopy + } + if c.TenantID != nil { + tenantCopy := *c.TenantID + copied.TenantID = &tenantCopy + } + if c.Environment != nil { + environmentCopy := *c.Environment + copied.Environment = &environmentCopy + } + + return copied +} diff --git a/api/v1alpha2/config/providers/azure/azure_test.go b/api/v1alpha2/config/providers/azure/azure_test.go new file mode 100644 index 000000000..825fea62c --- /dev/null +++ b/api/v1alpha2/config/providers/azure/azure_test.go @@ -0,0 +1,164 @@ +package azure + +import ( + "testing" +) + +// TestAzureConfig_Merge tests the Merge method of AzureConfig +func TestAzureConfig_Merge(t *testing.T) { + t.Run("MergeAllFields", func(t *testing.T) { + base := &AzureConfig{ + Enabled: boolPtr(false), + SubscriptionID: stringPtr("old-sub"), + TenantID: stringPtr("old-tenant"), + Environment: stringPtr("old-env"), + } + overlay := &AzureConfig{ + Enabled: boolPtr(true), + SubscriptionID: stringPtr("new-sub"), + TenantID: stringPtr("new-tenant"), + Environment: stringPtr("new-env"), + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", base.Enabled) + } + if base.SubscriptionID == nil || *base.SubscriptionID != "new-sub" { + t.Errorf("Expected SubscriptionID to be 'new-sub', got %s", *base.SubscriptionID) + } + if base.TenantID == nil || *base.TenantID != "new-tenant" { + t.Errorf("Expected TenantID to be 'new-tenant', got %s", *base.TenantID) + } + if base.Environment == nil || *base.Environment != "new-env" { + t.Errorf("Expected Environment to be 'new-env', got %s", *base.Environment) + } + }) + + t.Run("MergePartialOverlay", func(t *testing.T) { + base := &AzureConfig{ + Enabled: boolPtr(false), + SubscriptionID: stringPtr("old-sub"), + TenantID: stringPtr("old-tenant"), + Environment: stringPtr("old-env"), + } + overlay := &AzureConfig{ + Enabled: boolPtr(true), + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", base.Enabled) + } + if base.SubscriptionID == nil || *base.SubscriptionID != "old-sub" { + t.Errorf("Expected SubscriptionID to remain 'old-sub', got %s", *base.SubscriptionID) + } + if base.TenantID == nil || *base.TenantID != "old-tenant" { + t.Errorf("Expected TenantID to remain 'old-tenant', got %s", *base.TenantID) + } + if base.Environment == nil || *base.Environment != "old-env" { + t.Errorf("Expected Environment to remain 'old-env', got %s", *base.Environment) + } + }) + + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &AzureConfig{ + Enabled: boolPtr(false), + SubscriptionID: stringPtr("old-sub"), + TenantID: stringPtr("old-tenant"), + Environment: stringPtr("old-env"), + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Enabled == nil || *base.Enabled != *original.Enabled { + t.Errorf("Expected Enabled to remain unchanged") + } + if base.SubscriptionID == nil || *base.SubscriptionID != *original.SubscriptionID { + t.Errorf("Expected SubscriptionID to remain unchanged") + } + if base.TenantID == nil || *base.TenantID != *original.TenantID { + t.Errorf("Expected TenantID to remain unchanged") + } + if base.Environment == nil || *base.Environment != *original.Environment { + t.Errorf("Expected Environment to remain unchanged") + } + }) +} + +// TestAzureConfig_Copy tests the Copy method of AzureConfig +func TestAzureConfig_Copy(t *testing.T) { + t.Run("CopyAllFields", func(t *testing.T) { + original := &AzureConfig{ + Enabled: boolPtr(true), + SubscriptionID: stringPtr("sub"), + TenantID: stringPtr("tenant"), + Environment: stringPtr("env"), + } + + copy := original.DeepCopy() + + if copy == nil { + t.Fatal("Expected non-nil copy") + } + if copy == original { + t.Error("Expected copy to be a new instance") + } + if copy.Enabled == nil || *copy.Enabled != *original.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copy.SubscriptionID == nil || *copy.SubscriptionID != *original.SubscriptionID { + t.Errorf("Expected SubscriptionID to be copied correctly") + } + if copy.TenantID == nil || *copy.TenantID != *original.TenantID { + t.Errorf("Expected TenantID to be copied correctly") + } + if copy.Environment == nil || *copy.Environment != *original.Environment { + t.Errorf("Expected Environment to be copied correctly") + } + }) + + t.Run("CopySomeFields", func(t *testing.T) { + original := &AzureConfig{ + Enabled: boolPtr(true), + } + + copy := original.DeepCopy() + + if copy == nil { + t.Fatal("Expected non-nil copy") + } + if copy.Enabled == nil || *copy.Enabled != *original.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copy.SubscriptionID != nil { + t.Error("Expected SubscriptionID to be nil") + } + if copy.TenantID != nil { + t.Error("Expected TenantID to be nil") + } + if copy.Environment != nil { + t.Error("Expected Environment to be nil") + } + }) + + t.Run("CopyNilConfig", func(t *testing.T) { + var original *AzureConfig + copy := original.DeepCopy() + + if copy != nil { + t.Error("Expected nil copy for nil original") + } + }) +} + +func boolPtr(b bool) *bool { + return &b +} + +func stringPtr(s string) *string { + return &s +} diff --git a/api/v1alpha2/config/providers/providers.go b/api/v1alpha2/config/providers/providers.go new file mode 100644 index 000000000..3da6557fc --- /dev/null +++ b/api/v1alpha2/config/providers/providers.go @@ -0,0 +1,48 @@ +package providers + +import ( + "github.com/windsorcli/cli/api/v1alpha2/config/providers/aws" + "github.com/windsorcli/cli/api/v1alpha2/config/providers/azure" +) + +// ProvidersConfig represents the configuration for all cloud providers +type ProvidersConfig struct { + AWS *aws.AWSConfig `yaml:"aws,omitempty"` + Azure *azure.AzureConfig `yaml:"azure,omitempty"` +} + +// Merge performs a deep merge of the current ProvidersConfig with another ProvidersConfig. +func (base *ProvidersConfig) Merge(overlay *ProvidersConfig) { + if overlay == nil { + return + } + if overlay.AWS != nil { + if base.AWS == nil { + base.AWS = &aws.AWSConfig{} + } + base.AWS.Merge(overlay.AWS) + } + if overlay.Azure != nil { + if base.Azure == nil { + base.Azure = &azure.AzureConfig{} + } + base.Azure.Merge(overlay.Azure) + } +} + +// DeepCopy creates a deep copy of the ProvidersConfig object +func (c *ProvidersConfig) DeepCopy() *ProvidersConfig { + if c == nil { + return nil + } + copied := &ProvidersConfig{} + + if c.AWS != nil { + copied.AWS = c.AWS.DeepCopy() + } + if c.Azure != nil { + copied.Azure = c.Azure.DeepCopy() + } + + return copied +} diff --git a/api/v1alpha2/config/providers/providers_test.go b/api/v1alpha2/config/providers/providers_test.go new file mode 100644 index 000000000..854f65b69 --- /dev/null +++ b/api/v1alpha2/config/providers/providers_test.go @@ -0,0 +1,317 @@ +package providers + +import ( + "testing" + + "github.com/windsorcli/cli/api/v1alpha2/config/providers/aws" + "github.com/windsorcli/cli/api/v1alpha2/config/providers/azure" +) + +// TestProvidersConfig_Merge tests the Merge method of ProvidersConfig +func TestProvidersConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.AWS == nil || *base.AWS.Enabled != *original.AWS.Enabled { + t.Errorf("Expected AWS config to remain unchanged") + } + if base.Azure == nil || *base.Azure.Enabled != *original.Azure.Enabled { + t.Errorf("Expected Azure config to remain unchanged") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + overlay := &ProvidersConfig{} + + base.Merge(overlay) + + if base.AWS == nil || !*base.AWS.Enabled { + t.Errorf("Expected AWS config to remain enabled") + } + if base.Azure == nil || *base.Azure.Enabled { + t.Errorf("Expected Azure config to remain disabled") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + overlay := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(false), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(true), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || *base.AWS.Enabled { + t.Errorf("Expected AWS config to be disabled after merge") + } + if base.Azure == nil || !*base.Azure.Enabled { + t.Errorf("Expected Azure config to be enabled after merge") + } + }) + + t.Run("MergeWithOnlyAWS", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + overlay := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(false), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || *base.AWS.Enabled { + t.Errorf("Expected AWS config to be disabled after merge") + } + if base.Azure == nil || *base.Azure.Enabled { + t.Errorf("Expected Azure config to remain disabled") + } + }) + + t.Run("MergeWithOnlyAzure", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + overlay := &ProvidersConfig{ + Azure: &azure.AzureConfig{ + Enabled: boolPtr(true), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || !*base.AWS.Enabled { + t.Errorf("Expected AWS config to remain enabled") + } + if base.Azure == nil || !*base.Azure.Enabled { + t.Errorf("Expected Azure config to be enabled after merge") + } + }) + + t.Run("MergeWithNilBaseFields", func(t *testing.T) { + base := &ProvidersConfig{} + overlay := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || !*base.AWS.Enabled { + t.Errorf("Expected AWS config to be initialized and enabled") + } + if base.Azure == nil || *base.Azure.Enabled { + t.Errorf("Expected Azure config to be initialized and disabled") + } + }) + + t.Run("MergeWithNilBaseAWS", func(t *testing.T) { + base := &ProvidersConfig{ + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + overlay := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || !*base.AWS.Enabled { + t.Errorf("Expected AWS config to be initialized and enabled") + } + if base.Azure == nil || *base.Azure.Enabled { + t.Errorf("Expected Azure config to remain disabled") + } + }) + + t.Run("MergeWithNilBaseAzure", func(t *testing.T) { + base := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + } + overlay := &ProvidersConfig{ + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + + base.Merge(overlay) + + if base.AWS == nil || !*base.AWS.Enabled { + t.Errorf("Expected AWS config to remain enabled") + } + if base.Azure == nil || *base.Azure.Enabled { + t.Errorf("Expected Azure config to be initialized and disabled") + } + }) +} + +// TestProvidersConfig_Copy tests the Copy method of ProvidersConfig +func TestProvidersConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *ProvidersConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &ProvidersConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.AWS != nil { + t.Error("Expected AWS to be nil in copy") + } + if copied.Azure != nil { + t.Error("Expected Azure to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.AWS == nil || *copied.AWS.Enabled != *config.AWS.Enabled { + t.Errorf("Expected AWS config to be copied correctly") + } + if copied.Azure == nil || *copied.Azure.Enabled != *config.Azure.Enabled { + t.Errorf("Expected Azure config to be copied correctly") + } + }) + + t.Run("CopyWithPartialFields", func(t *testing.T) { + config := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.AWS == nil || *copied.AWS.Enabled != *config.AWS.Enabled { + t.Errorf("Expected AWS config to be copied correctly") + } + if copied.Azure != nil { + t.Error("Expected Azure to be nil in copy") + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + Azure: &azure.AzureConfig{ + Enabled: boolPtr(false), + }, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.AWS.Enabled = false + *config.Azure.Enabled = true + + if *copied.AWS.Enabled != true { + t.Error("Expected copied AWS config to remain independent") + } + if *copied.Azure.Enabled != false { + t.Error("Expected copied Azure config to remain independent") + } + }) + + t.Run("CopyWithSingleField", func(t *testing.T) { + config := &ProvidersConfig{ + AWS: &aws.AWSConfig{ + Enabled: boolPtr(true), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.AWS == nil || *copied.AWS.Enabled != *config.AWS.Enabled { + t.Errorf("Expected AWS config to be copied correctly") + } + if copied.Azure != nil { + t.Error("Expected Azure to be nil in copy") + } + }) +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/secrets/onepassword/onepassword.go b/api/v1alpha2/config/secrets/onepassword/onepassword.go new file mode 100644 index 000000000..9fdb27121 --- /dev/null +++ b/api/v1alpha2/config/secrets/onepassword/onepassword.go @@ -0,0 +1,57 @@ +package onepassword + +// OnePasswordConfig represents the OnePassword configuration +type OnePasswordConfig struct { + Vaults map[string]OnePasswordVault `yaml:"vaults,omitempty"` +} + +type OnePasswordVault struct { + ID string `yaml:"id,omitempty"` + URL string `yaml:"url,omitempty"` + Name string `yaml:"name,omitempty"` +} + +// Merge performs a deep merge of the current OnePasswordConfig with another OnePasswordConfig. +func (base *OnePasswordConfig) Merge(overlay *OnePasswordConfig) { + if overlay == nil { + return + } + + if base.Vaults == nil { + base.Vaults = make(map[string]OnePasswordVault) + } + + for key, overlayVault := range overlay.Vaults { + if baseVault, exists := base.Vaults[key]; exists { + if overlayVault.URL != "" { + baseVault.URL = overlayVault.URL + } + if overlayVault.ID != "" { + baseVault.ID = overlayVault.ID + } + if overlayVault.Name != "" { + baseVault.Name = overlayVault.Name + } + base.Vaults[key] = baseVault + } else { + base.Vaults[key] = overlayVault + } + } +} + +// DeepCopy creates a deep copy of the OnePasswordConfig object +func (c *OnePasswordConfig) DeepCopy() *OnePasswordConfig { + if c == nil { + return nil + } + + copied := &OnePasswordConfig{ + Vaults: make(map[string]OnePasswordVault), + } + + for key, vault := range c.Vaults { + copied.Vaults[key] = vault + } + + return copied +} diff --git a/api/v1alpha2/config/secrets/onepassword/onepassword_test.go b/api/v1alpha2/config/secrets/onepassword/onepassword_test.go new file mode 100644 index 000000000..bad9d14b3 --- /dev/null +++ b/api/v1alpha2/config/secrets/onepassword/onepassword_test.go @@ -0,0 +1,237 @@ +package onepassword + +import ( + "testing" +) + +// TestOnePasswordConfig_Merge tests the Merge method of OnePasswordConfig +func TestOnePasswordConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if len(base.Vaults) != len(original.Vaults) { + t.Errorf("Expected vaults to remain unchanged") + } + if base.Vaults["vault1"].ID != original.Vaults["vault1"].ID { + t.Errorf("Expected vault ID to remain unchanged") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + overlay := &OnePasswordConfig{} + + base.Merge(overlay) + + if len(base.Vaults) != 1 { + t.Errorf("Expected vaults to remain unchanged") + } + if base.Vaults["vault1"].ID != "id1" { + t.Errorf("Expected vault ID to remain 'id1'") + } + }) + + t.Run("MergeWithNewVault", func(t *testing.T) { + base := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + overlay := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault2": {ID: "id2", URL: "url2", Name: "Vault 2"}, + }, + } + + base.Merge(overlay) + + if len(base.Vaults) != 2 { + t.Errorf("Expected 2 vaults, got %d", len(base.Vaults)) + } + if base.Vaults["vault1"].ID != "id1" { + t.Errorf("Expected vault1 ID to remain 'id1'") + } + if base.Vaults["vault2"].ID != "id2" { + t.Errorf("Expected vault2 ID to be 'id2'") + } + }) + + t.Run("MergeWithExistingVault", func(t *testing.T) { + base := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + overlay := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1-new", URL: "url1-new", Name: "Vault 1 Updated"}, + }, + } + + base.Merge(overlay) + + if len(base.Vaults) != 1 { + t.Errorf("Expected 1 vault, got %d", len(base.Vaults)) + } + if base.Vaults["vault1"].ID != "id1-new" { + t.Errorf("Expected vault1 ID to be 'id1-new', got %s", base.Vaults["vault1"].ID) + } + if base.Vaults["vault1"].URL != "url1-new" { + t.Errorf("Expected vault1 URL to be 'url1-new', got %s", base.Vaults["vault1"].URL) + } + if base.Vaults["vault1"].Name != "Vault 1 Updated" { + t.Errorf("Expected vault1 Name to be 'Vault 1 Updated', got %s", base.Vaults["vault1"].Name) + } + }) + + t.Run("MergeWithPartialVaultUpdate", func(t *testing.T) { + base := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + overlay := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1-new", URL: "", Name: ""}, + }, + } + + base.Merge(overlay) + + if base.Vaults["vault1"].ID != "id1-new" { + t.Errorf("Expected vault1 ID to be 'id1-new', got %s", base.Vaults["vault1"].ID) + } + if base.Vaults["vault1"].URL != "url1" { + t.Errorf("Expected vault1 URL to remain 'url1', got %s", base.Vaults["vault1"].URL) + } + if base.Vaults["vault1"].Name != "Vault 1" { + t.Errorf("Expected vault1 Name to remain 'Vault 1', got %s", base.Vaults["vault1"].Name) + } + }) + + t.Run("MergeWithNilBaseVaults", func(t *testing.T) { + base := &OnePasswordConfig{} + overlay := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + + base.Merge(overlay) + + if len(base.Vaults) != 1 { + t.Errorf("Expected 1 vault, got %d", len(base.Vaults)) + } + if base.Vaults["vault1"].ID != "id1" { + t.Errorf("Expected vault1 ID to be 'id1'") + } + }) +} + +// TestOnePasswordConfig_Copy tests the Copy method of OnePasswordConfig +func TestOnePasswordConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *OnePasswordConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &OnePasswordConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.Vaults == nil { + t.Error("Expected vaults map to be initialized") + } + if len(copied.Vaults) != 0 { + t.Error("Expected empty vaults map") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + "vault2": {ID: "id2", URL: "url2", Name: "Vault 2"}, + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if len(copied.Vaults) != len(config.Vaults) { + t.Errorf("Expected same number of vaults, got %d vs %d", len(copied.Vaults), len(config.Vaults)) + } + if copied.Vaults["vault1"].ID != config.Vaults["vault1"].ID { + t.Errorf("Expected vault1 ID to be copied correctly") + } + if copied.Vaults["vault2"].ID != config.Vaults["vault2"].ID { + t.Errorf("Expected vault2 ID to be copied correctly") + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + config.Vaults["vault1"] = OnePasswordVault{ID: "id1-modified", URL: "url1-modified", Name: "Vault 1 Modified"} + + if copied.Vaults["vault1"].ID != "id1" { + t.Error("Expected copied vault1 ID to remain independent") + } + if copied.Vaults["vault1"].URL != "url1" { + t.Error("Expected copied vault1 URL to remain independent") + } + if copied.Vaults["vault1"].Name != "Vault 1" { + t.Error("Expected copied vault1 Name to remain independent") + } + }) + + t.Run("CopyWithSingleVault", func(t *testing.T) { + config := &OnePasswordConfig{ + Vaults: map[string]OnePasswordVault{ + "vault1": {ID: "id1", URL: "url1", Name: "Vault 1"}, + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if len(copied.Vaults) != 1 { + t.Errorf("Expected 1 vault, got %d", len(copied.Vaults)) + } + if copied.Vaults["vault1"].ID != config.Vaults["vault1"].ID { + t.Errorf("Expected vault1 ID to be copied correctly") + } + }) +} diff --git a/api/v1alpha2/config/secrets/secrets.go b/api/v1alpha2/config/secrets/secrets.go new file mode 100644 index 000000000..675641962 --- /dev/null +++ b/api/v1alpha2/config/secrets/secrets.go @@ -0,0 +1,33 @@ +package secrets + +import ( + "github.com/windsorcli/cli/api/v1alpha2/config/secrets/onepassword" +) + +// SecretsConfig represents the Secrets configuration +type SecretsConfig struct { + OnePassword *onepassword.OnePasswordConfig `yaml:"onepassword,omitempty"` +} + +// Merge performs a deep merge of the current SecretsConfig with another SecretsConfig. +func (base *SecretsConfig) Merge(overlay *SecretsConfig) { + if overlay == nil { + return + } + if overlay.OnePassword != nil { + if base.OnePassword == nil { + base.OnePassword = &onepassword.OnePasswordConfig{} + } + base.OnePassword.Merge(overlay.OnePassword) + } +} + +// DeepCopy creates a deep copy of the SecretsConfig object +func (c *SecretsConfig) DeepCopy() *SecretsConfig { + if c == nil { + return nil + } + return &SecretsConfig{ + OnePassword: c.OnePassword.DeepCopy(), + } +} diff --git a/api/v1alpha2/config/secrets/secrets_test.go b/api/v1alpha2/config/secrets/secrets_test.go new file mode 100644 index 000000000..4f21e482c --- /dev/null +++ b/api/v1alpha2/config/secrets/secrets_test.go @@ -0,0 +1,116 @@ +package secrets + +import ( + "reflect" + "testing" + + "github.com/windsorcli/cli/api/v1alpha2/config/secrets/onepassword" +) + +func TestSecretsConfig_Merge(t *testing.T) { + t.Run("MergeWithNonNilOverlay", func(t *testing.T) { + base := &SecretsConfig{ + OnePassword: &onepassword.OnePasswordConfig{ + Vaults: map[string]onepassword.OnePasswordVault{ + "vault1": {URL: "https://old-url.com", Name: "Old Vault"}, + }, + }, + } + overlay := &SecretsConfig{ + OnePassword: &onepassword.OnePasswordConfig{ + Vaults: map[string]onepassword.OnePasswordVault{ + "vault1": {URL: "https://new-url.com", Name: "New Vault"}, + }, + }, + } + + base.Merge(overlay) + + if base.OnePassword.Vaults["vault1"].URL != "https://new-url.com" { + t.Errorf("URL mismatch: expected %v, got %v", "https://new-url.com", base.OnePassword.Vaults["vault1"].URL) + } + if base.OnePassword.Vaults["vault1"].Name != "New Vault" { + t.Errorf("Name mismatch: expected %v, got %v", "New Vault", base.OnePassword.Vaults["vault1"].Name) + } + }) + + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &SecretsConfig{ + OnePassword: &onepassword.OnePasswordConfig{ + Vaults: map[string]onepassword.OnePasswordVault{ + "vault1": {URL: "https://old-url.com", Name: "Old Vault"}, + }, + }, + } + var overlay *SecretsConfig = nil + + base.Merge(overlay) + + if base.OnePassword.Vaults["vault1"].URL != "https://old-url.com" { + t.Errorf("URL mismatch: expected %v, got %v", "https://old-url.com", base.OnePassword.Vaults["vault1"].URL) + } + if base.OnePassword.Vaults["vault1"].Name != "Old Vault" { + t.Errorf("Name mismatch: expected %v, got %v", "Old Vault", base.OnePassword.Vaults["vault1"].Name) + } + }) + + t.Run("MergeWithNilBaseOnePassword", func(t *testing.T) { + base := &SecretsConfig{ + OnePassword: nil, + } + overlay := &SecretsConfig{ + OnePassword: &onepassword.OnePasswordConfig{ + Vaults: map[string]onepassword.OnePasswordVault{ + "vault1": {URL: "https://new-url.com", Name: "New Vault"}, + }, + }, + } + + base.Merge(overlay) + + if base.OnePassword == nil { + t.Errorf("Base OnePassword should not be nil after merge") + } + if base.OnePassword.Vaults["vault1"].URL != "https://new-url.com" { + t.Errorf("URL mismatch: expected %v, got %v", "https://new-url.com", base.OnePassword.Vaults["vault1"].URL) + } + if base.OnePassword.Vaults["vault1"].Name != "New Vault" { + t.Errorf("Name mismatch: expected %v, got %v", "New Vault", base.OnePassword.Vaults["vault1"].Name) + } + }) +} + +func TestSecretsConfig_Copy(t *testing.T) { + t.Run("CopyWithNonNilValues", func(t *testing.T) { + original := &SecretsConfig{ + OnePassword: &onepassword.OnePasswordConfig{ + Vaults: map[string]onepassword.OnePasswordVault{ + "vault1": {URL: "https://url.com", Name: "Vault"}, + }, + }, + } + + copy := original.DeepCopy() + + if !reflect.DeepEqual(original, copy) { + t.Errorf("Copy mismatch: expected %v, got %v", original, copy) + } + + // Modify the copy and ensure original is unchanged + copy.OnePassword.Vaults["vault1"] = onepassword.OnePasswordVault{URL: "https://new-url.com", Name: "New Vault"} + if original.OnePassword.Vaults["vault1"].URL == copy.OnePassword.Vaults["vault1"].URL { + t.Errorf("Original URL was modified: expected %v, got %v", "https://url.com", copy.OnePassword.Vaults["vault1"].URL) + } + }) + + t.Run("CopyWithNilSecretsConfig", func(t *testing.T) { + var original *SecretsConfig = nil + + copy := original.DeepCopy() + + // Ensure the copy is nil + if copy != nil { + t.Errorf("Copy is not nil, expected a nil copy") + } + }) +} diff --git a/api/v1alpha2/config/terraform/terraform.go b/api/v1alpha2/config/terraform/terraform.go new file mode 100644 index 000000000..f3356653e --- /dev/null +++ b/api/v1alpha2/config/terraform/terraform.go @@ -0,0 +1,48 @@ +package terraform + +// TerraformConfig represents the Terraform configuration +type TerraformConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + Driver *string `yaml:"driver,omitempty"` + Backend *BackendConfig `yaml:"backend,omitempty"` +} + +type BackendConfig struct { + Type string `yaml:"type"` + Prefix *string `yaml:"prefix,omitempty"` +} + +// Merge performs a simple merge of the current TerraformConfig with another TerraformConfig. +func (base *TerraformConfig) Merge(overlay *TerraformConfig) { + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.Driver != nil { + base.Driver = overlay.Driver + } + if overlay.Backend != nil { + base.Backend = overlay.Backend + } +} + +// DeepCopy creates a deep copy of the TerraformConfig object +func (c *TerraformConfig) DeepCopy() *TerraformConfig { + if c == nil { + return nil + } + copied := &TerraformConfig{} + + if c.Enabled != nil { + enabledCopy := *c.Enabled + copied.Enabled = &enabledCopy + } + if c.Driver != nil { + driverCopy := *c.Driver + copied.Driver = &driverCopy + } + if c.Backend != nil { + copied.Backend = c.Backend + } + + return copied +} diff --git a/api/v1alpha2/config/terraform/terraform_test.go b/api/v1alpha2/config/terraform/terraform_test.go new file mode 100644 index 000000000..9fbe5e77e --- /dev/null +++ b/api/v1alpha2/config/terraform/terraform_test.go @@ -0,0 +1,148 @@ +package terraform + +import ( + "reflect" + "testing" +) + +func TestTerraformConfig_Merge(t *testing.T) { + t.Run("MergeWithNonNilValues", func(t *testing.T) { + base := &TerraformConfig{ + Enabled: nil, + Driver: nil, + Backend: nil, + } + overlay := &TerraformConfig{ + Enabled: ptrBool(true), + Driver: stringPtr("docker"), + Backend: &BackendConfig{Type: "s3", Prefix: stringPtr("mock-prefix")}, + } + base.Merge(overlay) + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Enabled mismatch: expected %v, got %v", true, *base.Enabled) + } + if base.Driver == nil || *base.Driver != "docker" { + t.Errorf("Driver mismatch: expected %v, got %v", "docker", *base.Driver) + } + if base.Backend == nil || base.Backend.Type != "s3" { + t.Errorf("Backend mismatch: expected %v, got %v", "s3", base.Backend.Type) + } + if base.Backend.Prefix == nil || *base.Backend.Prefix != "mock-prefix" { + t.Errorf("Prefix mismatch: expected %v, got %v", "mock-prefix", base.Backend.Prefix) + } + }) + + t.Run("MergeWithNilValues", func(t *testing.T) { + base := &TerraformConfig{ + Enabled: ptrBool(false), + Driver: stringPtr("local"), + Backend: &BackendConfig{Type: "s3", Prefix: stringPtr("base-prefix")}, + } + overlay := &TerraformConfig{ + Enabled: nil, + Driver: nil, + Backend: nil, + } + base.Merge(overlay) + if base.Enabled == nil || *base.Enabled != false { + t.Errorf("Enabled mismatch: expected %v, got %v", false, *base.Enabled) + } + if base.Driver == nil || *base.Driver != "local" { + t.Errorf("Driver mismatch: expected %v, got %v", "local", *base.Driver) + } + if base.Backend == nil || base.Backend.Type != "s3" { + t.Errorf("Backend mismatch: expected %v, got %v", "s3", base.Backend.Type) + } + if base.Backend.Prefix == nil || *base.Backend.Prefix != "base-prefix" { + t.Errorf("Prefix mismatch: expected %v, got %v", "base-prefix", base.Backend.Prefix) + } + }) + + t.Run("MergeWithOnlyDriver", func(t *testing.T) { + base := &TerraformConfig{ + Enabled: ptrBool(false), + Driver: stringPtr("local"), + Backend: &BackendConfig{Type: "s3", Prefix: stringPtr("base-prefix")}, + } + overlay := &TerraformConfig{ + Driver: stringPtr("docker"), + } + base.Merge(overlay) + if base.Enabled == nil || *base.Enabled != false { + t.Errorf("Enabled mismatch: expected %v, got %v", false, *base.Enabled) + } + if base.Driver == nil || *base.Driver != "docker" { + t.Errorf("Driver mismatch: expected %v, got %v", "docker", *base.Driver) + } + if base.Backend == nil || base.Backend.Type != "s3" { + t.Errorf("Backend mismatch: expected %v, got %v", "s3", base.Backend.Type) + } + if base.Backend.Prefix == nil || *base.Backend.Prefix != "base-prefix" { + t.Errorf("Prefix mismatch: expected %v, got %v", "base-prefix", base.Backend.Prefix) + } + }) +} + +func TestTerraformConfig_Copy(t *testing.T) { + t.Run("CopyWithNonNilValues", func(t *testing.T) { + original := &TerraformConfig{ + Enabled: ptrBool(true), + Driver: stringPtr("docker"), + Backend: &BackendConfig{Type: "s3", Prefix: stringPtr("original-prefix")}, + } + + copy := original.DeepCopy() + + if !reflect.DeepEqual(original, copy) { + t.Errorf("Copy mismatch: expected %v, got %v", original, copy) + } + + // Modify the copy and ensure original is unchanged + copy.Enabled = ptrBool(false) + if original.Enabled == nil || *original.Enabled == *copy.Enabled { + t.Errorf("Original Enabled was modified: expected %v, got %v", true, *copy.Enabled) + } + copy.Driver = stringPtr("local") + if original.Driver == nil || *original.Driver == *copy.Driver { + t.Errorf("Original Driver was modified: expected %v, got %v", "docker", *copy.Driver) + } + copy.Backend = &BackendConfig{Type: "local", Prefix: stringPtr("copy-prefix")} + if original.Backend == nil || original.Backend.Type == copy.Backend.Type { + t.Errorf("Original Backend was modified: expected %v, got %v", "s3", copy.Backend.Type) + } + if original.Backend.Prefix == nil || *original.Backend.Prefix == *copy.Backend.Prefix { + t.Errorf("Original Prefix was modified: expected %v, got %v", "original-prefix", *copy.Backend.Prefix) + } + }) + + t.Run("CopyWithNilValues", func(t *testing.T) { + original := &TerraformConfig{ + Enabled: nil, + Driver: nil, + Backend: nil, + } + + copy := original.DeepCopy() + + if copy.Enabled != nil || copy.Driver != nil || copy.Backend != nil { + t.Errorf("Copy mismatch: expected nil values, got Enabled: %v, Driver: %v, Backend: %v", copy.Enabled, copy.Driver, copy.Backend) + } + }) + + t.Run("CopyNil", func(t *testing.T) { + var original *TerraformConfig = nil + mockCopy := original.DeepCopy() + if mockCopy != nil { + t.Errorf("Mock copy should be nil, got %v", mockCopy) + } + }) +} + +// Helper functions to create pointers for basic types +func ptrBool(b bool) *bool { + return &b +} + +func stringPtr(s string) *string { + return &s +} diff --git a/api/v1alpha2/config/workstation/cluster/cluster.go b/api/v1alpha2/config/workstation/cluster/cluster.go new file mode 100644 index 000000000..0ab0289df --- /dev/null +++ b/api/v1alpha2/config/workstation/cluster/cluster.go @@ -0,0 +1,301 @@ +package workstation + +// ClusterConfig represents the cluster configuration +type ClusterConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + Platform *string `yaml:"platform,omitempty"` + Driver *string `yaml:"driver,omitempty"` + Endpoint *string `yaml:"endpoint,omitempty"` + Image *string `yaml:"image,omitempty"` + ControlPlanes NodeGroupConfig `yaml:"controlplanes,omitempty"` + Workers NodeGroupConfig `yaml:"workers,omitempty"` +} + +// NodeConfig represents the node configuration +type NodeConfig struct { + Hostname *string `yaml:"hostname,omitempty"` + Node *string `yaml:"node,omitempty"` + Endpoint *string `yaml:"endpoint,omitempty"` + Image *string `yaml:"image,omitempty"` + HostPorts []string `yaml:"hostports,omitempty"` +} + +// NodeGroupConfig represents the configuration for a group of nodes +type NodeGroupConfig struct { + Count *int `yaml:"count,omitempty"` + CPU *int `yaml:"cpu,omitempty"` + Memory *int `yaml:"memory,omitempty"` + Image *string `yaml:"image,omitempty"` + Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + HostPorts []string `yaml:"hostports,omitempty"` + Volumes []string `yaml:"volumes,omitempty"` +} + +// Merge performs a deep merge of the current ClusterConfig with another ClusterConfig. +func (base *ClusterConfig) Merge(overlay *ClusterConfig) { + if overlay == nil { + return + } + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.Platform != nil { + base.Platform = overlay.Platform + } + if overlay.Driver != nil { + base.Driver = overlay.Driver + } + if overlay.Endpoint != nil { + base.Endpoint = overlay.Endpoint + } + if overlay.Image != nil { + base.Image = overlay.Image + } + if overlay.ControlPlanes.Count != nil { + base.ControlPlanes.Count = overlay.ControlPlanes.Count + } + if overlay.ControlPlanes.CPU != nil { + base.ControlPlanes.CPU = overlay.ControlPlanes.CPU + } + if overlay.ControlPlanes.Memory != nil { + base.ControlPlanes.Memory = overlay.ControlPlanes.Memory + } + if overlay.ControlPlanes.Image != nil { + base.ControlPlanes.Image = overlay.ControlPlanes.Image + } + if overlay.ControlPlanes.Nodes != nil { + base.ControlPlanes.Nodes = make(map[string]NodeConfig, len(overlay.ControlPlanes.Nodes)) + for key, node := range overlay.ControlPlanes.Nodes { + base.ControlPlanes.Nodes[key] = node + } + } + if overlay.ControlPlanes.HostPorts != nil { + base.ControlPlanes.HostPorts = make([]string, len(overlay.ControlPlanes.HostPorts)) + copy(base.ControlPlanes.HostPorts, overlay.ControlPlanes.HostPorts) + } + if overlay.ControlPlanes.Volumes != nil { + base.ControlPlanes.Volumes = make([]string, len(overlay.ControlPlanes.Volumes)) + copy(base.ControlPlanes.Volumes, overlay.ControlPlanes.Volumes) + } + if overlay.Workers.Count != nil { + base.Workers.Count = overlay.Workers.Count + } + if overlay.Workers.CPU != nil { + base.Workers.CPU = overlay.Workers.CPU + } + if overlay.Workers.Memory != nil { + base.Workers.Memory = overlay.Workers.Memory + } + if overlay.Workers.Image != nil { + base.Workers.Image = overlay.Workers.Image + } + if overlay.Workers.Nodes != nil { + base.Workers.Nodes = make(map[string]NodeConfig, len(overlay.Workers.Nodes)) + for key, node := range overlay.Workers.Nodes { + base.Workers.Nodes[key] = node + } + } + if overlay.Workers.HostPorts != nil { + base.Workers.HostPorts = make([]string, len(overlay.Workers.HostPorts)) + copy(base.Workers.HostPorts, overlay.Workers.HostPorts) + } + if overlay.Workers.Volumes != nil { + base.Workers.Volumes = make([]string, len(overlay.Workers.Volumes)) + copy(base.Workers.Volumes, overlay.Workers.Volumes) + } +} + +// DeepCopy creates a deep copy of the ClusterConfig object +func (c *ClusterConfig) DeepCopy() *ClusterConfig { + if c == nil { + return nil + } + + var controlPlanesNodesCopy map[string]NodeConfig + if len(c.ControlPlanes.Nodes) > 0 { + controlPlanesNodesCopy = make(map[string]NodeConfig, len(c.ControlPlanes.Nodes)) + for key, node := range c.ControlPlanes.Nodes { + var hostnameCopy *string + if node.Hostname != nil { + hostnameCopy = new(string) + *hostnameCopy = *node.Hostname + } + var nodeCopy *string + if node.Node != nil { + nodeCopy = new(string) + *nodeCopy = *node.Node + } + var endpointCopy *string + if node.Endpoint != nil { + endpointCopy = new(string) + *endpointCopy = *node.Endpoint + } + var imageCopy *string + if node.Image != nil { + imageCopy = new(string) + *imageCopy = *node.Image + } + controlPlanesNodesCopy[key] = NodeConfig{ + Hostname: hostnameCopy, + Node: nodeCopy, + Endpoint: endpointCopy, + Image: imageCopy, + HostPorts: append([]string{}, node.HostPorts...), + } + } + } + var controlPlanesHostPortsCopy []string + if len(c.ControlPlanes.HostPorts) > 0 { + controlPlanesHostPortsCopy = make([]string, len(c.ControlPlanes.HostPorts)) + copy(controlPlanesHostPortsCopy, c.ControlPlanes.HostPorts) + } + var controlPlanesVolumesCopy []string + if len(c.ControlPlanes.Volumes) > 0 { + controlPlanesVolumesCopy = make([]string, len(c.ControlPlanes.Volumes)) + copy(controlPlanesVolumesCopy, c.ControlPlanes.Volumes) + } + + var workersNodesCopy map[string]NodeConfig + if len(c.Workers.Nodes) > 0 { + workersNodesCopy = make(map[string]NodeConfig, len(c.Workers.Nodes)) + for key, node := range c.Workers.Nodes { + var hostnameCopy *string + if node.Hostname != nil { + hostnameCopy = new(string) + *hostnameCopy = *node.Hostname + } + var nodeCopy *string + if node.Node != nil { + nodeCopy = new(string) + *nodeCopy = *node.Node + } + var endpointCopy *string + if node.Endpoint != nil { + endpointCopy = new(string) + *endpointCopy = *node.Endpoint + } + var imageCopy *string + if node.Image != nil { + imageCopy = new(string) + *imageCopy = *node.Image + } + workersNodesCopy[key] = NodeConfig{ + Hostname: hostnameCopy, + Node: nodeCopy, + Endpoint: endpointCopy, + Image: imageCopy, + HostPorts: append([]string{}, node.HostPorts...), + } + } + } + var workersHostPortsCopy []string + if len(c.Workers.HostPorts) > 0 { + workersHostPortsCopy = make([]string, len(c.Workers.HostPorts)) + copy(workersHostPortsCopy, c.Workers.HostPorts) + } + var workersVolumesCopy []string + if len(c.Workers.Volumes) > 0 { + workersVolumesCopy = make([]string, len(c.Workers.Volumes)) + copy(workersVolumesCopy, c.Workers.Volumes) + } + + var enabledCopy *bool + if c.Enabled != nil { + enabledCopy = new(bool) + *enabledCopy = *c.Enabled + } + var platformCopy *string + if c.Platform != nil { + platformCopy = new(string) + *platformCopy = *c.Platform + } + var driverCopy *string + if c.Driver != nil { + driverCopy = new(string) + *driverCopy = *c.Driver + } + var endpointCopy *string + if c.Endpoint != nil { + endpointCopy = new(string) + *endpointCopy = *c.Endpoint + } + var imageCopy *string + if c.Image != nil { + imageCopy = new(string) + *imageCopy = *c.Image + } + + return &ClusterConfig{ + Enabled: enabledCopy, + Platform: platformCopy, + Driver: driverCopy, + Endpoint: endpointCopy, + Image: imageCopy, + ControlPlanes: NodeGroupConfig{ + Count: func() *int { + if c.ControlPlanes.Count != nil { + count := *c.ControlPlanes.Count + return &count + } + return nil + }(), + CPU: func() *int { + if c.ControlPlanes.CPU != nil { + cpu := *c.ControlPlanes.CPU + return &cpu + } + return nil + }(), + Memory: func() *int { + if c.ControlPlanes.Memory != nil { + memory := *c.ControlPlanes.Memory + return &memory + } + return nil + }(), + Image: func() *string { + if c.ControlPlanes.Image != nil { + image := *c.ControlPlanes.Image + return &image + } + return nil + }(), + Nodes: controlPlanesNodesCopy, + HostPorts: controlPlanesHostPortsCopy, + Volumes: controlPlanesVolumesCopy, + }, + Workers: NodeGroupConfig{ + Count: func() *int { + if c.Workers.Count != nil { + count := *c.Workers.Count + return &count + } + return nil + }(), + CPU: func() *int { + if c.Workers.CPU != nil { + cpu := *c.Workers.CPU + return &cpu + } + return nil + }(), + Memory: func() *int { + if c.Workers.Memory != nil { + memory := *c.Workers.Memory + return &memory + } + return nil + }(), + Image: func() *string { + if c.Workers.Image != nil { + image := *c.Workers.Image + return &image + } + return nil + }(), + Nodes: workersNodesCopy, + HostPorts: workersHostPortsCopy, + Volumes: workersVolumesCopy, + }, + } +} diff --git a/api/v1alpha2/config/workstation/cluster/cluster_test.go b/api/v1alpha2/config/workstation/cluster/cluster_test.go new file mode 100644 index 000000000..b12b43be6 --- /dev/null +++ b/api/v1alpha2/config/workstation/cluster/cluster_test.go @@ -0,0 +1,424 @@ +package workstation + +import ( + "reflect" + "testing" +) + +// TestClusterConfig_Merge tests the Merge method of ClusterConfig +func TestClusterConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &ClusterConfig{ + Enabled: ptrBool(true), + Driver: ptrString("talos"), + } + original := base.DeepCopy() + + base.Merge(nil) + + if !reflect.DeepEqual(base, original) { + t.Errorf("Expected no change when merging with nil overlay") + } + }) + + t.Run("MergeBasicFields", func(t *testing.T) { + base := &ClusterConfig{ + Enabled: ptrBool(false), + Driver: ptrString("kind"), + } + + overlay := &ClusterConfig{ + Enabled: ptrBool(true), + Driver: ptrString("talos"), + Platform: ptrString("local"), + } + + base.Merge(overlay) + + if !*base.Enabled { + t.Errorf("Expected Enabled to be true") + } + if *base.Driver != "talos" { + t.Errorf("Expected Driver to be 'talos', got %s", *base.Driver) + } + if *base.Platform != "local" { + t.Errorf("Expected Platform to be 'local', got %s", *base.Platform) + } + }) + + t.Run("MergeControlPlanes", func(t *testing.T) { + base := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Count: ptrInt(1), + CPU: ptrInt(2), + Memory: ptrInt(4), + }, + } + + overlay := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Count: ptrInt(3), + CPU: ptrInt(4), + Memory: ptrInt(8), + Image: ptrString("talos:v1.0.0"), + }, + } + + base.Merge(overlay) + + if *base.ControlPlanes.Count != 3 { + t.Errorf("Expected ControlPlanes.Count to be 3, got %d", *base.ControlPlanes.Count) + } + if *base.ControlPlanes.CPU != 4 { + t.Errorf("Expected ControlPlanes.CPU to be 4, got %d", *base.ControlPlanes.CPU) + } + if *base.ControlPlanes.Memory != 8 { + t.Errorf("Expected ControlPlanes.Memory to be 8, got %d", *base.ControlPlanes.Memory) + } + if *base.ControlPlanes.Image != "talos:v1.0.0" { + t.Errorf("Expected ControlPlanes.Image to be 'talos:v1.0.0', got %s", *base.ControlPlanes.Image) + } + }) + + t.Run("MergeWorkers", func(t *testing.T) { + base := &ClusterConfig{ + Workers: NodeGroupConfig{ + Count: ptrInt(1), + CPU: ptrInt(2), + Memory: ptrInt(4), + }, + } + + overlay := &ClusterConfig{ + Workers: NodeGroupConfig{ + Count: ptrInt(2), + CPU: ptrInt(4), + Memory: ptrInt(8), + Image: ptrString("talos:v1.0.0"), + }, + } + + base.Merge(overlay) + + if *base.Workers.Count != 2 { + t.Errorf("Expected Workers.Count to be 2, got %d", *base.Workers.Count) + } + if *base.Workers.CPU != 4 { + t.Errorf("Expected Workers.CPU to be 4, got %d", *base.Workers.CPU) + } + if *base.Workers.Memory != 8 { + t.Errorf("Expected Workers.Memory to be 8, got %d", *base.Workers.Memory) + } + if *base.Workers.Image != "talos:v1.0.0" { + t.Errorf("Expected Workers.Image to be 'talos:v1.0.0', got %s", *base.Workers.Image) + } + }) + + t.Run("MergeWithNodes", func(t *testing.T) { + base := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "cp1": { + Hostname: ptrString("cp1.local"), + Endpoint: ptrString("10.0.0.1"), + }, + }, + }, + } + + overlay := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "cp2": { + Hostname: ptrString("cp2.local"), + Endpoint: ptrString("10.0.0.2"), + }, + }, + }, + } + + base.Merge(overlay) + + if len(base.ControlPlanes.Nodes) != 1 { + t.Errorf("Expected 1 node, got %d", len(base.ControlPlanes.Nodes)) + } + if *base.ControlPlanes.Nodes["cp2"].Hostname != "cp2.local" { + t.Errorf("Expected hostname to be 'cp2.local', got %s", *base.ControlPlanes.Nodes["cp2"].Hostname) + } + }) + + t.Run("MergeWithHostPorts", func(t *testing.T) { + base := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + HostPorts: []string{"8080:80"}, + }, + } + + overlay := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + HostPorts: []string{"8443:443", "3000:3000"}, + }, + } + + base.Merge(overlay) + + expected := []string{"8443:443", "3000:3000"} + if !reflect.DeepEqual(base.ControlPlanes.HostPorts, expected) { + t.Errorf("Expected HostPorts to be %v, got %v", expected, base.ControlPlanes.HostPorts) + } + }) + + t.Run("MergeWithVolumes", func(t *testing.T) { + base := &ClusterConfig{ + Workers: NodeGroupConfig{ + Volumes: []string{"/tmp/data:/var/data"}, + }, + } + + overlay := &ClusterConfig{ + Workers: NodeGroupConfig{ + Volumes: []string{"/tmp/logs:/var/logs", "/tmp/cache:/var/cache"}, + }, + } + + base.Merge(overlay) + + expected := []string{"/tmp/logs:/var/logs", "/tmp/cache:/var/cache"} + if !reflect.DeepEqual(base.Workers.Volumes, expected) { + t.Errorf("Expected Volumes to be %v, got %v", expected, base.Workers.Volumes) + } + }) + + t.Run("MergeWithNodesAndSlices", func(t *testing.T) { + base := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "cp1": { + Hostname: ptrString("cp1.local"), + HostPorts: []string{"8080:80"}, + }, + }, + HostPorts: []string{"6443:6443"}, + Volumes: []string{"/tmp/data:/var/data"}, + }, + Workers: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "worker1": { + Hostname: ptrString("worker1.local"), + HostPorts: []string{"3000:3000"}, + }, + }, + HostPorts: []string{"3000:3000"}, + Volumes: []string{"/tmp/logs:/var/logs"}, + }, + } + + overlay := &ClusterConfig{ + ControlPlanes: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "cp2": { + Hostname: ptrString("cp2.local"), + HostPorts: []string{"8081:81"}, + }, + }, + HostPorts: []string{"8443:443"}, + Volumes: []string{"/tmp/config:/var/config"}, + }, + Workers: NodeGroupConfig{ + Nodes: map[string]NodeConfig{ + "worker2": { + Hostname: ptrString("worker2.local"), + HostPorts: []string{"3001:3001"}, + }, + }, + HostPorts: []string{"3001:3001"}, + Volumes: []string{"/tmp/cache:/var/cache"}, + }, + } + + base.Merge(overlay) + + // Verify control planes - overlay replaces existing data + if len(base.ControlPlanes.Nodes) != 1 { + t.Errorf("Expected 1 control plane node, got %d", len(base.ControlPlanes.Nodes)) + } + if base.ControlPlanes.Nodes["cp2"].Hostname == nil || *base.ControlPlanes.Nodes["cp2"].Hostname != "cp2.local" { + t.Errorf("Expected cp2 hostname to be 'cp2.local'") + } + if len(base.ControlPlanes.HostPorts) != 1 { + t.Errorf("Expected 1 control plane host port, got %d", len(base.ControlPlanes.HostPorts)) + } + if base.ControlPlanes.HostPorts[0] != "8443:443" { + t.Errorf("Expected control plane host port to be '8443:443', got %s", base.ControlPlanes.HostPorts[0]) + } + if len(base.ControlPlanes.Volumes) != 1 { + t.Errorf("Expected 1 control plane volume, got %d", len(base.ControlPlanes.Volumes)) + } + if base.ControlPlanes.Volumes[0] != "/tmp/config:/var/config" { + t.Errorf("Expected control plane volume to be '/tmp/config:/var/config', got %s", base.ControlPlanes.Volumes[0]) + } + + // Verify workers - overlay replaces existing data + if len(base.Workers.Nodes) != 1 { + t.Errorf("Expected 1 worker node, got %d", len(base.Workers.Nodes)) + } + if base.Workers.Nodes["worker2"].Hostname == nil || *base.Workers.Nodes["worker2"].Hostname != "worker2.local" { + t.Errorf("Expected worker2 hostname to be 'worker2.local'") + } + if len(base.Workers.HostPorts) != 1 { + t.Errorf("Expected 1 worker host port, got %d", len(base.Workers.HostPorts)) + } + if base.Workers.HostPorts[0] != "3001:3001" { + t.Errorf("Expected worker host port to be '3001:3001', got %s", base.Workers.HostPorts[0]) + } + if len(base.Workers.Volumes) != 1 { + t.Errorf("Expected 1 worker volume, got %d", len(base.Workers.Volumes)) + } + if base.Workers.Volumes[0] != "/tmp/cache:/var/cache" { + t.Errorf("Expected worker volume to be '/tmp/cache:/var/cache', got %s", base.Workers.Volumes[0]) + } + }) + + t.Run("MergeWithAllFields", func(t *testing.T) { + base := &ClusterConfig{ + Enabled: ptrBool(false), + Platform: ptrString("cloud"), + Driver: ptrString("kind"), + Endpoint: ptrString("https://old.local:6443"), + Image: ptrString("kind:v1.0.0"), + } + + overlay := &ClusterConfig{ + Enabled: ptrBool(true), + Platform: ptrString("local"), + Driver: ptrString("talos"), + Endpoint: ptrString("https://new.local:6443"), + Image: ptrString("talos:v1.0.0"), + } + + base.Merge(overlay) + + if !*base.Enabled { + t.Errorf("Expected Enabled to be true") + } + if *base.Platform != "local" { + t.Errorf("Expected Platform to be 'local'") + } + if *base.Driver != "talos" { + t.Errorf("Expected Driver to be 'talos'") + } + if *base.Endpoint != "https://new.local:6443" { + t.Errorf("Expected Endpoint to be 'https://new.local:6443'") + } + if *base.Image != "talos:v1.0.0" { + t.Errorf("Expected Image to be 'talos:v1.0.0'") + } + }) +} + +// TestClusterConfig_Copy tests the Copy method of ClusterConfig +func TestClusterConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *ClusterConfig + copied := config.DeepCopy() + + if copied != nil { + t.Errorf("Expected nil when copying nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &ClusterConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy of empty config") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &ClusterConfig{ + Enabled: ptrBool(true), + Platform: ptrString("local"), + Driver: ptrString("talos"), + Endpoint: ptrString("https://cluster.local:6443"), + Image: ptrString("talos:v1.0.0"), + ControlPlanes: NodeGroupConfig{ + Count: ptrInt(3), + CPU: ptrInt(2), + Memory: ptrInt(4), + Image: ptrString("talos:v1.0.0"), + Nodes: map[string]NodeConfig{ + "cp1": { + Hostname: ptrString("cp1.local"), + Endpoint: ptrString("10.0.0.1"), + Image: ptrString("talos:v1.0.0"), + HostPorts: []string{"8080:80", "8443:443"}, + }, + }, + HostPorts: []string{"6443:6443"}, + Volumes: []string{"/tmp/data:/var/data"}, + }, + Workers: NodeGroupConfig{ + Count: ptrInt(2), + CPU: ptrInt(4), + Memory: ptrInt(8), + Image: ptrString("talos:v1.0.0"), + Nodes: map[string]NodeConfig{ + "worker1": { + Hostname: ptrString("worker1.local"), + Endpoint: ptrString("10.0.0.10"), + Image: ptrString("talos:v1.0.0"), + HostPorts: []string{"3000:3000"}, + }, + }, + HostPorts: []string{"3000:3000"}, + Volumes: []string{"/tmp/logs:/var/logs"}, + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + + // Verify deep copy by modifying original + *config.Enabled = false + if !*copied.Enabled { + t.Errorf("Expected copy to be independent of original") + } + + // Verify deep copy of slices + config.ControlPlanes.HostPorts[0] = "9999:9999" + if copied.ControlPlanes.HostPorts[0] == "9999:9999" { + t.Errorf("Expected copy slices to be independent of original") + } + + // Verify deep copy of maps + *config.ControlPlanes.Nodes["cp1"].Hostname = "modified.local" + if *copied.ControlPlanes.Nodes["cp1"].Hostname == "modified.local" { + t.Errorf("Expected copy maps to be independent of original") + } + }) + +} + +// Helper functions to create pointers for basic types +func ptrString(s string) *string { + return &s +} + +func ptrBool(b bool) *bool { + return &b +} + +func ptrInt(i int) *int { + return &i +} diff --git a/api/v1alpha2/config/workstation/dns/dns.go b/api/v1alpha2/config/workstation/dns/dns.go new file mode 100644 index 000000000..d7875bc6e --- /dev/null +++ b/api/v1alpha2/config/workstation/dns/dns.go @@ -0,0 +1,69 @@ +package workstation + +// DNSConfig represents the DNS configuration +type DNSConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + Domain *string `yaml:"domain,omitempty"` + Address *string `yaml:"address,omitempty"` + Forward []string `yaml:"forward,omitempty"` + Records []string `yaml:"records,omitempty"` +} + +// Merge performs a deep merge of the current DNSConfig with another DNSConfig. +func (base *DNSConfig) Merge(overlay *DNSConfig) { + if overlay == nil { + return + } + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.Domain != nil { + base.Domain = overlay.Domain + } + if overlay.Address != nil { + base.Address = overlay.Address + } + if overlay.Forward != nil { + base.Forward = overlay.Forward + } + if overlay.Records != nil { + base.Records = overlay.Records + } +} + +// DeepCopy creates a deep copy of the DNSConfig object +func (c *DNSConfig) DeepCopy() *DNSConfig { + if c == nil { + return nil + } + + var forwardCopy []string + if c.Forward != nil { + forwardCopy = make([]string, len(c.Forward)) + copy(forwardCopy, c.Forward) + } + + var recordsCopy []string + if c.Records != nil { + recordsCopy = make([]string, len(c.Records)) + copy(recordsCopy, c.Records) + } + + return &DNSConfig{ + Enabled: c.Enabled, + Domain: c.Domain, + Address: c.Address, + Forward: forwardCopy, + Records: recordsCopy, + } +} + +// Helper function to create boolean pointers +func ptrBool(b bool) *bool { + return &b +} + +// Helper function to create string pointers +func ptrString(s string) *string { + return &s +} diff --git a/api/v1alpha2/config/workstation/dns/dns_test.go b/api/v1alpha2/config/workstation/dns/dns_test.go new file mode 100644 index 000000000..b0c01a233 --- /dev/null +++ b/api/v1alpha2/config/workstation/dns/dns_test.go @@ -0,0 +1,217 @@ +package workstation + +import ( + "reflect" + "testing" +) + +// TestDNSConfig_Merge tests the Merge method of DNSConfig +func TestDNSConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + } + original := base.DeepCopy() + + base.Merge(nil) + + if !reflect.DeepEqual(base, original) { + t.Errorf("Expected no change when merging with nil overlay") + } + }) + + t.Run("MergeBasicFields", func(t *testing.T) { + base := &DNSConfig{ + Enabled: ptrBool(false), + Domain: ptrString("old.local"), + } + + overlay := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("new.local"), + Address: ptrString("10.0.0.1"), + } + + base.Merge(overlay) + + if !*base.Enabled { + t.Errorf("Expected Enabled to be true") + } + if *base.Domain != "new.local" { + t.Errorf("Expected Domain to be 'new.local', got %s", *base.Domain) + } + if *base.Address != "10.0.0.1" { + t.Errorf("Expected Address to be '10.0.0.1', got %s", *base.Address) + } + }) + + t.Run("MergeForwardServers", func(t *testing.T) { + base := &DNSConfig{ + Forward: []string{"8.8.8.8"}, + } + + overlay := &DNSConfig{ + Forward: []string{"1.1.1.1", "8.8.4.4"}, + } + + base.Merge(overlay) + + expected := []string{"1.1.1.1", "8.8.4.4"} + if !reflect.DeepEqual(base.Forward, expected) { + t.Errorf("Expected Forward to be %v, got %v", expected, base.Forward) + } + }) + + t.Run("MergeRecords", func(t *testing.T) { + base := &DNSConfig{ + Records: []string{"10.0.0.1 api.local"}, + } + + overlay := &DNSConfig{ + Records: []string{"10.0.0.2 app.local", "10.0.0.3 db.local"}, + } + + base.Merge(overlay) + + expected := []string{"10.0.0.2 app.local", "10.0.0.3 db.local"} + if !reflect.DeepEqual(base.Records, expected) { + t.Errorf("Expected Records to be %v, got %v", expected, base.Records) + } + }) + + t.Run("MergeAllFields", func(t *testing.T) { + base := &DNSConfig{ + Enabled: ptrBool(false), + Domain: ptrString("old.local"), + Address: ptrString("10.0.0.1"), + Forward: []string{"8.8.8.8"}, + Records: []string{"10.0.0.1 api.local"}, + } + + overlay := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("new.local"), + Address: ptrString("10.0.0.2"), + Forward: []string{"1.1.1.1", "8.8.4.4"}, + Records: []string{"10.0.0.2 app.local", "10.0.0.3 db.local"}, + } + + base.Merge(overlay) + + if !*base.Enabled { + t.Errorf("Expected Enabled to be true") + } + if *base.Domain != "new.local" { + t.Errorf("Expected Domain to be 'new.local', got %s", *base.Domain) + } + if *base.Address != "10.0.0.2" { + t.Errorf("Expected Address to be '10.0.0.2', got %s", *base.Address) + } + + expectedForward := []string{"1.1.1.1", "8.8.4.4"} + if !reflect.DeepEqual(base.Forward, expectedForward) { + t.Errorf("Expected Forward to be %v, got %v", expectedForward, base.Forward) + } + + expectedRecords := []string{"10.0.0.2 app.local", "10.0.0.3 db.local"} + if !reflect.DeepEqual(base.Records, expectedRecords) { + t.Errorf("Expected Records to be %v, got %v", expectedRecords, base.Records) + } + }) +} + +// TestDNSConfig_Copy tests the Copy method of DNSConfig +func TestDNSConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *DNSConfig + copied := config.DeepCopy() + + if copied != nil { + t.Errorf("Expected nil when copying nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &DNSConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy of empty config") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + Address: ptrString("10.0.0.1"), + Forward: []string{"8.8.8.8", "1.1.1.1"}, + Records: []string{"10.0.0.1 api.local", "10.0.0.2 app.local"}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + + // Verify deep copy by modifying original + *config.Enabled = false + if *copied.Enabled { + t.Errorf("Expected copy to be independent of original") + } + + // Verify deep copy of slices + config.Forward[0] = "modified" + if copied.Forward[0] == "modified" { + t.Errorf("Expected copy slices to be independent of original") + } + + config.Records[0] = "modified" + if copied.Records[0] == "modified" { + t.Errorf("Expected copy records to be independent of original") + } + }) + + t.Run("CopyWithNilSlices", func(t *testing.T) { + config := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + // Forward and Records are nil + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) + + t.Run("CopyWithEmptySlices", func(t *testing.T) { + config := &DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + Forward: []string{}, + Records: []string{}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) +} diff --git a/api/v1alpha2/config/workstation/git/git.go b/api/v1alpha2/config/workstation/git/git.go new file mode 100644 index 000000000..a7d51f820 --- /dev/null +++ b/api/v1alpha2/config/workstation/git/git.go @@ -0,0 +1,109 @@ +package workstation + +// GitConfig represents the Git configuration +type GitConfig struct { + Livereload *GitLivereloadConfig `yaml:"livereload"` +} + +// GitLivereloadConfig represents the Git livereload configuration +type GitLivereloadConfig struct { + Enabled *bool `yaml:"enabled"` + Include *string `yaml:"include,omitempty"` + Exclude *string `yaml:"exclude,omitempty"` + Protect *string `yaml:"protect,omitempty"` + Username *string `yaml:"username,omitempty"` + Password *string `yaml:"password,omitempty"` + WebhookUrl *string `yaml:"webhook_url,omitempty"` + VerifySsl *bool `yaml:"verify_ssl,omitempty"` + Image *string `yaml:"image,omitempty"` +} + +// Merge performs a deep merge of the current GitConfig with another GitConfig. +func (base *GitConfig) Merge(overlay *GitConfig) { + if overlay == nil { + return + } + if overlay.Livereload != nil { + if base.Livereload == nil { + base.Livereload = &GitLivereloadConfig{} + } + if overlay.Livereload.Enabled != nil { + base.Livereload.Enabled = overlay.Livereload.Enabled + } + if overlay.Livereload.Include != nil { + base.Livereload.Include = overlay.Livereload.Include + } + if overlay.Livereload.Exclude != nil { + base.Livereload.Exclude = overlay.Livereload.Exclude + } + if overlay.Livereload.Protect != nil { + base.Livereload.Protect = overlay.Livereload.Protect + } + if overlay.Livereload.Username != nil { + base.Livereload.Username = overlay.Livereload.Username + } + if overlay.Livereload.Password != nil { + base.Livereload.Password = overlay.Livereload.Password + } + if overlay.Livereload.WebhookUrl != nil { + base.Livereload.WebhookUrl = overlay.Livereload.WebhookUrl + } + if overlay.Livereload.VerifySsl != nil { + base.Livereload.VerifySsl = overlay.Livereload.VerifySsl + } + if overlay.Livereload.Image != nil { + base.Livereload.Image = overlay.Livereload.Image + } + } +} + +// DeepCopy creates a deep copy of the GitConfig object +func (c *GitConfig) DeepCopy() *GitConfig { + if c == nil { + return nil + } + var livereloadCopy *GitLivereloadConfig + if c.Livereload != nil { + livereloadCopy = &GitLivereloadConfig{} + + if c.Livereload.Enabled != nil { + enabledCopy := *c.Livereload.Enabled + livereloadCopy.Enabled = &enabledCopy + } + if c.Livereload.Include != nil { + includeCopy := *c.Livereload.Include + livereloadCopy.Include = &includeCopy + } + if c.Livereload.Exclude != nil { + excludeCopy := *c.Livereload.Exclude + livereloadCopy.Exclude = &excludeCopy + } + if c.Livereload.Protect != nil { + protectCopy := *c.Livereload.Protect + livereloadCopy.Protect = &protectCopy + } + if c.Livereload.Username != nil { + usernameCopy := *c.Livereload.Username + livereloadCopy.Username = &usernameCopy + } + if c.Livereload.Password != nil { + passwordCopy := *c.Livereload.Password + livereloadCopy.Password = &passwordCopy + } + if c.Livereload.WebhookUrl != nil { + webhookCopy := *c.Livereload.WebhookUrl + livereloadCopy.WebhookUrl = &webhookCopy + } + if c.Livereload.VerifySsl != nil { + verifyCopy := *c.Livereload.VerifySsl + livereloadCopy.VerifySsl = &verifyCopy + } + if c.Livereload.Image != nil { + imageCopy := *c.Livereload.Image + livereloadCopy.Image = &imageCopy + } + } + return &GitConfig{ + Livereload: livereloadCopy, + } +} diff --git a/api/v1alpha2/config/workstation/git/git_test.go b/api/v1alpha2/config/workstation/git/git_test.go new file mode 100644 index 000000000..08601aee2 --- /dev/null +++ b/api/v1alpha2/config/workstation/git/git_test.go @@ -0,0 +1,412 @@ +package workstation + +import ( + "testing" +) + +// TestGitConfig_Merge tests the Merge method of GitConfig +func TestGitConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("user"), + Password: stringPtr("pass"), + WebhookUrl: stringPtr("https://webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("git-livereload:latest"), + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Livereload == nil { + t.Error("Expected Livereload to remain initialized") + } + if *base.Livereload.Enabled != *original.Livereload.Enabled { + t.Errorf("Expected Enabled to remain unchanged") + } + if *base.Livereload.Include != *original.Livereload.Include { + t.Errorf("Expected Include to remain unchanged") + } + if *base.Livereload.Exclude != *original.Livereload.Exclude { + t.Errorf("Expected Exclude to remain unchanged") + } + if *base.Livereload.Protect != *original.Livereload.Protect { + t.Errorf("Expected Protect to remain unchanged") + } + if *base.Livereload.Username != *original.Livereload.Username { + t.Errorf("Expected Username to remain unchanged") + } + if *base.Livereload.Password != *original.Livereload.Password { + t.Errorf("Expected Password to remain unchanged") + } + if *base.Livereload.WebhookUrl != *original.Livereload.WebhookUrl { + t.Errorf("Expected WebhookUrl to remain unchanged") + } + if *base.Livereload.VerifySsl != *original.Livereload.VerifySsl { + t.Errorf("Expected VerifySsl to remain unchanged") + } + if *base.Livereload.Image != *original.Livereload.Image { + t.Errorf("Expected Image to remain unchanged") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("user"), + Password: stringPtr("pass"), + WebhookUrl: stringPtr("https://webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("git-livereload:latest"), + }, + } + overlay := &GitConfig{} + + base.Merge(overlay) + + if base.Livereload == nil { + t.Error("Expected Livereload to remain initialized") + } + if *base.Livereload.Enabled != true { + t.Errorf("Expected Enabled to remain true") + } + if *base.Livereload.Include != "*.go" { + t.Errorf("Expected Include to remain '*.go'") + } + if *base.Livereload.Exclude != "*.tmp" { + t.Errorf("Expected Exclude to remain '*.tmp'") + } + if *base.Livereload.Protect != "*.secret" { + t.Errorf("Expected Protect to remain '*.secret'") + } + if *base.Livereload.Username != "user" { + t.Errorf("Expected Username to remain 'user'") + } + if *base.Livereload.Password != "pass" { + t.Errorf("Expected Password to remain 'pass'") + } + if *base.Livereload.WebhookUrl != "https://webhook.example.com" { + t.Errorf("Expected WebhookUrl to remain 'https://webhook.example.com'") + } + if *base.Livereload.VerifySsl != true { + t.Errorf("Expected VerifySsl to remain true") + } + if *base.Livereload.Image != "git-livereload:latest" { + t.Errorf("Expected Image to remain 'git-livereload:latest'") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(false), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("olduser"), + Password: stringPtr("oldpass"), + WebhookUrl: stringPtr("https://old-webhook.example.com"), + VerifySsl: boolPtr(false), + Image: stringPtr("old-git-livereload:latest"), + }, + } + overlay := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.js"), + Exclude: stringPtr("*.log"), + Protect: stringPtr("*.key"), + Username: stringPtr("newuser"), + Password: stringPtr("newpass"), + WebhookUrl: stringPtr("https://new-webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("new-git-livereload:latest"), + }, + } + + base.Merge(overlay) + + if base.Livereload == nil { + t.Error("Expected Livereload to be initialized") + } + if *base.Livereload.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Livereload.Enabled) + } + if *base.Livereload.Include != "*.js" { + t.Errorf("Expected Include to be '*.js', got %s", *base.Livereload.Include) + } + if *base.Livereload.Exclude != "*.log" { + t.Errorf("Expected Exclude to be '*.log', got %s", *base.Livereload.Exclude) + } + if *base.Livereload.Protect != "*.key" { + t.Errorf("Expected Protect to be '*.key', got %s", *base.Livereload.Protect) + } + if *base.Livereload.Username != "newuser" { + t.Errorf("Expected Username to be 'newuser', got %s", *base.Livereload.Username) + } + if *base.Livereload.Password != "newpass" { + t.Errorf("Expected Password to be 'newpass', got %s", *base.Livereload.Password) + } + if *base.Livereload.WebhookUrl != "https://new-webhook.example.com" { + t.Errorf("Expected WebhookUrl to be 'https://new-webhook.example.com', got %s", *base.Livereload.WebhookUrl) + } + if *base.Livereload.VerifySsl != true { + t.Errorf("Expected VerifySsl to be true, got %v", *base.Livereload.VerifySsl) + } + if *base.Livereload.Image != "new-git-livereload:latest" { + t.Errorf("Expected Image to be 'new-git-livereload:latest', got %s", *base.Livereload.Image) + } + }) + + t.Run("MergeWithNilBaseLivereload", func(t *testing.T) { + base := &GitConfig{} + overlay := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("user"), + Password: stringPtr("pass"), + WebhookUrl: stringPtr("https://webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("git-livereload:latest"), + }, + } + + base.Merge(overlay) + + if base.Livereload == nil { + t.Error("Expected Livereload to be initialized") + } + if *base.Livereload.Enabled != true { + t.Errorf("Expected Enabled to be true") + } + if *base.Livereload.Include != "*.go" { + t.Errorf("Expected Include to be '*.go'") + } + if *base.Livereload.Exclude != "*.tmp" { + t.Errorf("Expected Exclude to be '*.tmp'") + } + if *base.Livereload.Protect != "*.secret" { + t.Errorf("Expected Protect to be '*.secret'") + } + if *base.Livereload.Username != "user" { + t.Errorf("Expected Username to be 'user'") + } + if *base.Livereload.Password != "pass" { + t.Errorf("Expected Password to be 'pass'") + } + if *base.Livereload.WebhookUrl != "https://webhook.example.com" { + t.Errorf("Expected WebhookUrl to be 'https://webhook.example.com'") + } + if *base.Livereload.VerifySsl != true { + t.Errorf("Expected VerifySsl to be true") + } + if *base.Livereload.Image != "git-livereload:latest" { + t.Errorf("Expected Image to be 'git-livereload:latest'") + } + }) +} + +// TestGitConfig_Copy tests the Copy method of GitConfig +func TestGitConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *GitConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &GitConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.Livereload != nil { + t.Error("Expected Livereload to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("user"), + Password: stringPtr("pass"), + WebhookUrl: stringPtr("https://webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("git-livereload:latest"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.Livereload == nil { + t.Error("Expected Livereload to be copied") + } + if copied.Livereload == config.Livereload { + t.Error("Expected Livereload to be a new instance") + } + if *copied.Livereload.Enabled != *config.Livereload.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if *copied.Livereload.Include != *config.Livereload.Include { + t.Errorf("Expected Include to be copied correctly") + } + if *copied.Livereload.Exclude != *config.Livereload.Exclude { + t.Errorf("Expected Exclude to be copied correctly") + } + if *copied.Livereload.Protect != *config.Livereload.Protect { + t.Errorf("Expected Protect to be copied correctly") + } + if *copied.Livereload.Username != *config.Livereload.Username { + t.Errorf("Expected Username to be copied correctly") + } + if *copied.Livereload.Password != *config.Livereload.Password { + t.Errorf("Expected Password to be copied correctly") + } + if *copied.Livereload.WebhookUrl != *config.Livereload.WebhookUrl { + t.Errorf("Expected WebhookUrl to be copied correctly") + } + if *copied.Livereload.VerifySsl != *config.Livereload.VerifySsl { + t.Errorf("Expected VerifySsl to be copied correctly") + } + if *copied.Livereload.Image != *config.Livereload.Image { + t.Errorf("Expected Image to be copied correctly") + } + }) + + t.Run("CopyWithPartialLivereload", func(t *testing.T) { + config := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Livereload == nil { + t.Error("Expected Livereload to be copied") + } + if *copied.Livereload.Enabled != *config.Livereload.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if *copied.Livereload.Include != *config.Livereload.Include { + t.Errorf("Expected Include to be copied correctly") + } + if copied.Livereload.Exclude != nil { + t.Error("Expected Exclude to be nil in copy") + } + if copied.Livereload.Protect != nil { + t.Error("Expected Protect to be nil in copy") + } + if copied.Livereload.Username != nil { + t.Error("Expected Username to be nil in copy") + } + if copied.Livereload.Password != nil { + t.Error("Expected Password to be nil in copy") + } + if copied.Livereload.WebhookUrl != nil { + t.Error("Expected WebhookUrl to be nil in copy") + } + if copied.Livereload.VerifySsl != nil { + t.Error("Expected VerifySsl to be nil in copy") + } + if copied.Livereload.Image != nil { + t.Error("Expected Image to be nil in copy") + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &GitConfig{ + Livereload: &GitLivereloadConfig{ + Enabled: boolPtr(true), + Include: stringPtr("*.go"), + Exclude: stringPtr("*.tmp"), + Protect: stringPtr("*.secret"), + Username: stringPtr("user"), + Password: stringPtr("pass"), + WebhookUrl: stringPtr("https://webhook.example.com"), + VerifySsl: boolPtr(true), + Image: stringPtr("git-livereload:latest"), + }, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.Livereload.Enabled = false + *config.Livereload.Include = "*.js" + *config.Livereload.Exclude = "*.log" + *config.Livereload.Protect = "*.key" + *config.Livereload.Username = "newuser" + *config.Livereload.Password = "newpass" + *config.Livereload.WebhookUrl = "https://new-webhook.example.com" + *config.Livereload.VerifySsl = false + *config.Livereload.Image = "new-git-livereload:latest" + + if *copied.Livereload.Enabled != true { + t.Error("Expected copied Enabled to remain independent") + } + if *copied.Livereload.Include != "*.go" { + t.Error("Expected copied Include to remain independent") + } + if *copied.Livereload.Exclude != "*.tmp" { + t.Error("Expected copied Exclude to remain independent") + } + if *copied.Livereload.Protect != "*.secret" { + t.Error("Expected copied Protect to remain independent") + } + if *copied.Livereload.Username != "user" { + t.Error("Expected copied Username to remain independent") + } + if *copied.Livereload.Password != "pass" { + t.Error("Expected copied Password to remain independent") + } + if *copied.Livereload.WebhookUrl != "https://webhook.example.com" { + t.Error("Expected copied WebhookUrl to remain independent") + } + if *copied.Livereload.VerifySsl != true { + t.Error("Expected copied VerifySsl to remain independent") + } + if *copied.Livereload.Image != "git-livereload:latest" { + t.Error("Expected copied Image to remain independent") + } + }) +} + +func boolPtr(b bool) *bool { + return &b +} + +func stringPtr(s string) *string { + return &s +} diff --git a/api/v1alpha2/config/workstation/localstack/localstack.go b/api/v1alpha2/config/workstation/localstack/localstack.go new file mode 100644 index 000000000..e64a4ecd0 --- /dev/null +++ b/api/v1alpha2/config/workstation/localstack/localstack.go @@ -0,0 +1,41 @@ +package workstation + +// LocalstackConfig represents the Localstack configuration +type LocalstackConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + Services []string `yaml:"services,omitempty"` +} + +// Merge performs a deep merge of the current LocalstackConfig with another LocalstackConfig. +func (base *LocalstackConfig) Merge(overlay *LocalstackConfig) { + if overlay == nil { + return + } + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + if overlay.Services != nil { + base.Services = overlay.Services + } +} + +// DeepCopy creates a deep copy of the LocalstackConfig object +func (c *LocalstackConfig) DeepCopy() *LocalstackConfig { + if c == nil { + return nil + } + var enabledCopy *bool + if c.Enabled != nil { + enabledCopy = new(bool) + *enabledCopy = *c.Enabled + } + var servicesCopy []string + if c.Services != nil { + servicesCopy = make([]string, len(c.Services)) + copy(servicesCopy, c.Services) + } + return &LocalstackConfig{ + Enabled: enabledCopy, + Services: servicesCopy, + } +} diff --git a/api/v1alpha2/config/workstation/localstack/localstack_test.go b/api/v1alpha2/config/workstation/localstack/localstack_test.go new file mode 100644 index 000000000..6adc756d5 --- /dev/null +++ b/api/v1alpha2/config/workstation/localstack/localstack_test.go @@ -0,0 +1,330 @@ +package workstation + +import ( + "testing" +) + +// TestLocalstackConfig_Merge tests the Merge method of LocalstackConfig +func TestLocalstackConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"s3", "dynamodb", "lambda"}, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Enabled == nil || *base.Enabled != *original.Enabled { + t.Errorf("Expected Enabled to remain unchanged") + } + if len(base.Services) != len(original.Services) { + t.Errorf("Expected Services to remain unchanged") + } + for i, service := range base.Services { + if service != original.Services[i] { + t.Errorf("Expected Services[%d] to remain unchanged", i) + } + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"s3", "dynamodb", "lambda"}, + } + overlay := &LocalstackConfig{} + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to remain true") + } + if len(base.Services) != 3 { + t.Errorf("Expected Services to remain unchanged") + } + expectedServices := []string{"s3", "dynamodb", "lambda"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to be '%s', got '%s'", i, expectedServices[i], service) + } + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(false), + Services: []string{"s3", "dynamodb"}, + } + overlay := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"lambda", "sqs", "sns"}, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Services) != 3 { + t.Errorf("Expected 3 services, got %d", len(base.Services)) + } + expectedServices := []string{"lambda", "sqs", "sns"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to be '%s', got '%s'", i, expectedServices[i], service) + } + } + }) + + t.Run("MergeWithOnlyEnabledOverlay", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(false), + Services: []string{"s3", "dynamodb", "lambda"}, + } + overlay := &LocalstackConfig{ + Enabled: boolPtr(true), + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Services) != 3 { + t.Errorf("Expected Services to remain unchanged") + } + expectedServices := []string{"s3", "dynamodb", "lambda"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to remain '%s', got '%s'", i, expectedServices[i], service) + } + } + }) + + t.Run("MergeWithOnlyServicesOverlay", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(false), + Services: []string{"s3", "dynamodb"}, + } + overlay := &LocalstackConfig{ + Services: []string{"lambda", "sqs", "sns"}, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != false { + t.Errorf("Expected Enabled to remain false, got %v", *base.Enabled) + } + if len(base.Services) != 3 { + t.Errorf("Expected 3 services, got %d", len(base.Services)) + } + expectedServices := []string{"lambda", "sqs", "sns"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to be '%s', got '%s'", i, expectedServices[i], service) + } + } + }) + + t.Run("MergeWithNilBaseEnabled", func(t *testing.T) { + base := &LocalstackConfig{ + Services: []string{"s3", "dynamodb"}, + } + overlay := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"lambda", "sqs"}, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Services) != 2 { + t.Errorf("Expected 2 services, got %d", len(base.Services)) + } + expectedServices := []string{"lambda", "sqs"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to be '%s', got '%s'", i, expectedServices[i], service) + } + } + }) + + t.Run("MergeWithNilBaseServices", func(t *testing.T) { + base := &LocalstackConfig{ + Enabled: boolPtr(false), + } + overlay := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"s3", "dynamodb", "lambda"}, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Services) != 3 { + t.Errorf("Expected 3 services, got %d", len(base.Services)) + } + expectedServices := []string{"s3", "dynamodb", "lambda"} + for i, service := range base.Services { + if service != expectedServices[i] { + t.Errorf("Expected Services[%d] to be '%s', got '%s'", i, expectedServices[i], service) + } + } + }) +} + +// TestLocalstackConfig_Copy tests the Copy method of LocalstackConfig +func TestLocalstackConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *LocalstackConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &LocalstackConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.Enabled != nil { + t.Error("Expected Enabled to be nil in copy") + } + if copied.Services != nil { + t.Error("Expected Services to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"s3", "dynamodb", "lambda", "sqs", "sns"}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if len(copied.Services) != len(config.Services) { + t.Errorf("Expected Services length to be copied correctly") + } + for i, service := range copied.Services { + if service != config.Services[i] { + t.Errorf("Expected Services[%d] to be copied correctly", i) + } + } + }) + + t.Run("CopyWithPartialFields", func(t *testing.T) { + config := &LocalstackConfig{ + Enabled: boolPtr(true), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copied.Services != nil { + t.Error("Expected Services to be nil in copy") + } + }) + + t.Run("CopyWithOnlyServices", func(t *testing.T) { + config := &LocalstackConfig{ + Services: []string{"s3", "dynamodb"}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled != nil { + t.Error("Expected Enabled to be nil in copy") + } + if len(copied.Services) != len(config.Services) { + t.Errorf("Expected Services length to be copied correctly") + } + for i, service := range copied.Services { + if service != config.Services[i] { + t.Errorf("Expected Services[%d] to be copied correctly", i) + } + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &LocalstackConfig{ + Enabled: boolPtr(true), + Services: []string{"s3", "dynamodb", "lambda"}, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.Enabled = false + config.Services[0] = "modified-service" + config.Services = append(config.Services, "new-service") + + if *copied.Enabled != true { + t.Error("Expected copied Enabled to remain independent") + } + if len(copied.Services) != 3 { + t.Error("Expected copied Services length to remain independent") + } + if copied.Services[0] != "s3" { + t.Error("Expected copied Services[0] to remain independent") + } + expectedServices := []string{"s3", "dynamodb", "lambda"} + for i, service := range copied.Services { + if service != expectedServices[i] { + t.Errorf("Expected copied Services[%d] to remain independent", i) + } + } + }) + + t.Run("CopyWithEmptyServices", func(t *testing.T) { + config := &LocalstackConfig{ + Enabled: boolPtr(false), + Services: []string{}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copied.Services == nil { + t.Error("Expected Services to be initialized as empty slice") + } + if len(copied.Services) != 0 { + t.Error("Expected Services to be empty slice") + } + }) +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/workstation/network/network.go b/api/v1alpha2/config/workstation/network/network.go new file mode 100644 index 000000000..5de4ac03b --- /dev/null +++ b/api/v1alpha2/config/workstation/network/network.go @@ -0,0 +1,72 @@ +package workstation + +// NetworkConfig represents the network configuration +type NetworkConfig struct { + CIDRBlock *string `yaml:"cidr_block,omitempty"` + LoadBalancerIPs *struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + } `yaml:"loadbalancer_ips,omitempty"` +} + +// Merge merges the non-nil fields from another NetworkConfig into this one. +func (nc *NetworkConfig) Merge(other *NetworkConfig) { + if other != nil { + if other.CIDRBlock != nil { + nc.CIDRBlock = other.CIDRBlock + } + if other.LoadBalancerIPs != nil { + if nc.LoadBalancerIPs == nil { + nc.LoadBalancerIPs = &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{} + } + if other.LoadBalancerIPs.Start != nil { + nc.LoadBalancerIPs.Start = other.LoadBalancerIPs.Start + } + if other.LoadBalancerIPs.End != nil { + nc.LoadBalancerIPs.End = other.LoadBalancerIPs.End + } + } + } +} + +// DeepCopy creates a deep copy of the NetworkConfig. +func (nc *NetworkConfig) DeepCopy() *NetworkConfig { + if nc == nil { + return nil + } + + var cidrBlockCopy *string + if nc.CIDRBlock != nil { + cidrBlockValue := *nc.CIDRBlock + cidrBlockCopy = &cidrBlockValue + } + + var loadBalancerIPsCopy *struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + } + if nc.LoadBalancerIPs != nil { + loadBalancerIPsCopy = &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{} + + if nc.LoadBalancerIPs.Start != nil { + startValue := *nc.LoadBalancerIPs.Start + loadBalancerIPsCopy.Start = &startValue + } + + if nc.LoadBalancerIPs.End != nil { + endValue := *nc.LoadBalancerIPs.End + loadBalancerIPsCopy.End = &endValue + } + } + + return &NetworkConfig{ + CIDRBlock: cidrBlockCopy, + LoadBalancerIPs: loadBalancerIPsCopy, + } +} diff --git a/api/v1alpha2/config/workstation/network/network_test.go b/api/v1alpha2/config/workstation/network/network_test.go new file mode 100644 index 000000000..2b322cdcf --- /dev/null +++ b/api/v1alpha2/config/workstation/network/network_test.go @@ -0,0 +1,403 @@ +package workstation + +import ( + "testing" +) + +// TestNetworkConfig_Merge tests the Merge method of NetworkConfig +func TestNetworkConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.CIDRBlock == nil || *base.CIDRBlock != *original.CIDRBlock { + t.Errorf("Expected CIDRBlock to remain unchanged") + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to remain initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != *original.LoadBalancerIPs.Start { + t.Errorf("Expected LoadBalancerIPs.Start to remain unchanged") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != *original.LoadBalancerIPs.End { + t.Errorf("Expected LoadBalancerIPs.End to remain unchanged") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + overlay := &NetworkConfig{} + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected CIDRBlock to remain '10.0.0.0/24'") + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to remain initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "10.0.0.10" { + t.Errorf("Expected LoadBalancerIPs.Start to remain '10.0.0.10'") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "10.0.0.20" { + t.Errorf("Expected LoadBalancerIPs.End to remain '10.0.0.20'") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + overlay := &NetworkConfig{ + CIDRBlock: stringPtr("192.168.1.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("192.168.1.10"), + End: stringPtr("192.168.1.20"), + }, + } + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "192.168.1.0/24" { + t.Errorf("Expected CIDRBlock to be '192.168.1.0/24', got %s", *base.CIDRBlock) + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "192.168.1.10" { + t.Errorf("Expected LoadBalancerIPs.Start to be '192.168.1.10', got %s", *base.LoadBalancerIPs.Start) + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "192.168.1.20" { + t.Errorf("Expected LoadBalancerIPs.End to be '192.168.1.20', got %s", *base.LoadBalancerIPs.End) + } + }) + + t.Run("MergeWithOnlyCIDRBlock", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + overlay := &NetworkConfig{ + CIDRBlock: stringPtr("192.168.1.0/24"), + } + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "192.168.1.0/24" { + t.Errorf("Expected CIDRBlock to be '192.168.1.0/24', got %s", *base.CIDRBlock) + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to remain initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "10.0.0.10" { + t.Errorf("Expected LoadBalancerIPs.Start to remain '10.0.0.10'") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "10.0.0.20" { + t.Errorf("Expected LoadBalancerIPs.End to remain '10.0.0.20'") + } + }) + + t.Run("MergeWithOnlyLoadBalancerIPs", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + } + overlay := &NetworkConfig{ + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected CIDRBlock to remain '10.0.0.0/24'") + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "10.0.0.10" { + t.Errorf("Expected LoadBalancerIPs.Start to be '10.0.0.10'") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "10.0.0.20" { + t.Errorf("Expected LoadBalancerIPs.End to be '10.0.0.20'") + } + }) + + t.Run("MergeWithPartialLoadBalancerIPs", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + overlay := &NetworkConfig{ + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.15"), + }, + } + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected CIDRBlock to remain '10.0.0.0/24'") + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to remain initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "10.0.0.15" { + t.Errorf("Expected LoadBalancerIPs.Start to be '10.0.0.15'") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "10.0.0.20" { + t.Errorf("Expected LoadBalancerIPs.End to remain '10.0.0.20'") + } + }) + + t.Run("MergeWithNilBaseLoadBalancerIPs", func(t *testing.T) { + base := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + } + overlay := &NetworkConfig{ + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + + base.Merge(overlay) + + if base.CIDRBlock == nil || *base.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected CIDRBlock to remain '10.0.0.0/24'") + } + if base.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be initialized") + } + if base.LoadBalancerIPs.Start == nil || *base.LoadBalancerIPs.Start != "10.0.0.10" { + t.Errorf("Expected LoadBalancerIPs.Start to be '10.0.0.10'") + } + if base.LoadBalancerIPs.End == nil || *base.LoadBalancerIPs.End != "10.0.0.20" { + t.Errorf("Expected LoadBalancerIPs.End to be '10.0.0.20'") + } + }) +} + +// TestNetworkConfig_Copy tests the Copy method of NetworkConfig +func TestNetworkConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *NetworkConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &NetworkConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.CIDRBlock != nil { + t.Error("Expected CIDRBlock to be nil in copy") + } + if copied.LoadBalancerIPs != nil { + t.Error("Expected LoadBalancerIPs to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.CIDRBlock == nil || *copied.CIDRBlock != *config.CIDRBlock { + t.Errorf("Expected CIDRBlock to be copied correctly") + } + if copied.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be copied") + } + if copied.LoadBalancerIPs == config.LoadBalancerIPs { + t.Error("Expected LoadBalancerIPs to be a new instance") + } + if copied.LoadBalancerIPs.Start == nil || *copied.LoadBalancerIPs.Start != *config.LoadBalancerIPs.Start { + t.Errorf("Expected LoadBalancerIPs.Start to be copied correctly") + } + if copied.LoadBalancerIPs.End == nil || *copied.LoadBalancerIPs.End != *config.LoadBalancerIPs.End { + t.Errorf("Expected LoadBalancerIPs.End to be copied correctly") + } + }) + + t.Run("CopyWithPartialFields", func(t *testing.T) { + config := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.CIDRBlock == nil || *copied.CIDRBlock != *config.CIDRBlock { + t.Errorf("Expected CIDRBlock to be copied correctly") + } + if copied.LoadBalancerIPs != nil { + t.Error("Expected LoadBalancerIPs to be nil in copy") + } + }) + + t.Run("CopyWithOnlyLoadBalancerIPs", func(t *testing.T) { + config := &NetworkConfig{ + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.CIDRBlock != nil { + t.Error("Expected CIDRBlock to be nil in copy") + } + if copied.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be copied") + } + if copied.LoadBalancerIPs.Start == nil || *copied.LoadBalancerIPs.Start != *config.LoadBalancerIPs.Start { + t.Errorf("Expected LoadBalancerIPs.Start to be copied correctly") + } + if copied.LoadBalancerIPs.End == nil || *copied.LoadBalancerIPs.End != *config.LoadBalancerIPs.End { + t.Errorf("Expected LoadBalancerIPs.End to be copied correctly") + } + }) + + t.Run("CopyWithPartialLoadBalancerIPs", func(t *testing.T) { + config := &NetworkConfig{ + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.LoadBalancerIPs == nil { + t.Error("Expected LoadBalancerIPs to be copied") + } + if copied.LoadBalancerIPs.Start == nil || *copied.LoadBalancerIPs.Start != *config.LoadBalancerIPs.Start { + t.Errorf("Expected LoadBalancerIPs.Start to be copied correctly") + } + if copied.LoadBalancerIPs.End != nil { + t.Error("Expected LoadBalancerIPs.End to be nil in copy") + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &NetworkConfig{ + CIDRBlock: stringPtr("10.0.0.0/24"), + LoadBalancerIPs: &struct { + Start *string `yaml:"start,omitempty"` + End *string `yaml:"end,omitempty"` + }{ + Start: stringPtr("10.0.0.10"), + End: stringPtr("10.0.0.20"), + }, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.CIDRBlock = "192.168.1.0/24" + *config.LoadBalancerIPs.Start = "192.168.1.10" + *config.LoadBalancerIPs.End = "192.168.1.20" + + if *copied.CIDRBlock != "10.0.0.0/24" { + t.Error("Expected copied CIDRBlock to remain independent") + } + if *copied.LoadBalancerIPs.Start != "10.0.0.10" { + t.Error("Expected copied LoadBalancerIPs.Start to remain independent") + } + if *copied.LoadBalancerIPs.End != "10.0.0.20" { + t.Error("Expected copied LoadBalancerIPs.End to remain independent") + } + }) +} + +func stringPtr(s string) *string { + return &s +} diff --git a/api/v1alpha2/config/workstation/registries/registries.go b/api/v1alpha2/config/workstation/registries/registries.go new file mode 100644 index 000000000..8e3b41cac --- /dev/null +++ b/api/v1alpha2/config/workstation/registries/registries.go @@ -0,0 +1,66 @@ +package workstation + +// RegistriesConfig represents the container registries configuration +type RegistriesConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + Registries map[string]RegistryConfig `yaml:"registries,omitempty"` +} + +// RegistryConfig represents the registry configuration +type RegistryConfig struct { + Remote string `yaml:"remote,omitempty"` + Local string `yaml:"local,omitempty"` + HostName string `yaml:"hostname,omitempty"` + HostPort int `yaml:"hostport,omitempty"` +} + +// Merge performs a deep merge of the current RegistriesConfig with another RegistriesConfig. +func (base *RegistriesConfig) Merge(overlay *RegistriesConfig) { + if overlay == nil { + return + } + + if overlay.Enabled != nil { + base.Enabled = overlay.Enabled + } + + // Overwrite base.Registries entirely with overlay.Registries if defined, otherwise keep base.Registries + if overlay.Registries != nil { + base.Registries = overlay.Registries + } +} + +// DeepCopy creates a deep copy of the RegistriesConfig object +func (c *RegistriesConfig) DeepCopy() *RegistriesConfig { + if c == nil { + return nil + } + + var enabledCopy *bool + if c.Enabled != nil { + enabledCopy = ptrBool(*c.Enabled) + } + + var registriesCopy map[string]RegistryConfig + if c.Registries != nil { + registriesCopy = make(map[string]RegistryConfig) + for name, registry := range c.Registries { + registriesCopy[name] = RegistryConfig{ + Remote: registry.Remote, + Local: registry.Local, + HostName: registry.HostName, + HostPort: registry.HostPort, + } + } + } + + return &RegistriesConfig{ + Enabled: enabledCopy, + Registries: registriesCopy, + } +} + +// Helper function to create boolean pointers +func ptrBool(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/workstation/registries/registries_test.go b/api/v1alpha2/config/workstation/registries/registries_test.go new file mode 100644 index 000000000..1399e1459 --- /dev/null +++ b/api/v1alpha2/config/workstation/registries/registries_test.go @@ -0,0 +1,561 @@ +package workstation + +import ( + "testing" +) + +// TestRegistriesConfig_Merge tests the Merge method of RegistriesConfig +func TestRegistriesConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + }, + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Enabled == nil || *base.Enabled != *original.Enabled { + t.Errorf("Expected Enabled to remain unchanged") + } + if len(base.Registries) != len(original.Registries) { + t.Errorf("Expected Registries to remain unchanged") + } + for name, registry := range base.Registries { + if registry.Remote != original.Registries[name].Remote { + t.Errorf("Expected Registries[%s].Remote to remain unchanged", name) + } + if registry.Local != original.Registries[name].Local { + t.Errorf("Expected Registries[%s].Local to remain unchanged", name) + } + if registry.HostName != original.Registries[name].HostName { + t.Errorf("Expected Registries[%s].HostName to remain unchanged", name) + } + if registry.HostPort != original.Registries[name].HostPort { + t.Errorf("Expected Registries[%s].HostPort to remain unchanged", name) + } + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + }, + } + overlay := &RegistriesConfig{} + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to remain true") + } + if len(base.Registries) != 1 { + t.Errorf("Expected Registries to remain unchanged") + } + if base.Registries["docker.io"].Remote != "docker.io" { + t.Errorf("Expected Registries[docker.io].Remote to remain 'docker.io'") + } + if base.Registries["docker.io"].Local != "localhost:5000" { + t.Errorf("Expected Registries[docker.io].Local to remain 'localhost:5000'") + } + if base.Registries["docker.io"].HostName != "localhost" { + t.Errorf("Expected Registries[docker.io].HostName to remain 'localhost'") + } + if base.Registries["docker.io"].HostPort != 5000 { + t.Errorf("Expected Registries[docker.io].HostPort to remain 5000") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(false), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + }, + } + overlay := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + "gcr.io": { + Remote: "gcr.io", + Local: "localhost:5002", + HostName: "localhost", + HostPort: 5002, + }, + }, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Registries) != 2 { + t.Errorf("Expected 2 registries, got %d", len(base.Registries)) + } + if base.Registries["quay.io"].Remote != "quay.io" { + t.Errorf("Expected Registries[quay.io].Remote to be 'quay.io'") + } + if base.Registries["quay.io"].Local != "localhost:5001" { + t.Errorf("Expected Registries[quay.io].Local to be 'localhost:5001'") + } + if base.Registries["quay.io"].HostName != "localhost" { + t.Errorf("Expected Registries[quay.io].HostName to be 'localhost'") + } + if base.Registries["quay.io"].HostPort != 5001 { + t.Errorf("Expected Registries[quay.io].HostPort to be 5001") + } + if base.Registries["gcr.io"].Remote != "gcr.io" { + t.Errorf("Expected Registries[gcr.io].Remote to be 'gcr.io'") + } + if base.Registries["gcr.io"].Local != "localhost:5002" { + t.Errorf("Expected Registries[gcr.io].Local to be 'localhost:5002'") + } + if base.Registries["gcr.io"].HostName != "localhost" { + t.Errorf("Expected Registries[gcr.io].HostName to be 'localhost'") + } + if base.Registries["gcr.io"].HostPort != 5002 { + t.Errorf("Expected Registries[gcr.io].HostPort to be 5002") + } + }) + + t.Run("MergeWithOnlyEnabled", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(false), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + }, + } + overlay := &RegistriesConfig{ + Enabled: boolPtr(true), + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Registries) != 1 { + t.Errorf("Expected Registries to remain unchanged") + } + if base.Registries["docker.io"].Remote != "docker.io" { + t.Errorf("Expected Registries[docker.io].Remote to remain 'docker.io'") + } + if base.Registries["docker.io"].Local != "localhost:5000" { + t.Errorf("Expected Registries[docker.io].Local to remain 'localhost:5000'") + } + if base.Registries["docker.io"].HostName != "localhost" { + t.Errorf("Expected Registries[docker.io].HostName to remain 'localhost'") + } + if base.Registries["docker.io"].HostPort != 5000 { + t.Errorf("Expected Registries[docker.io].HostPort to remain 5000") + } + }) + + t.Run("MergeWithOnlyRegistries", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(false), + } + overlay := &RegistriesConfig{ + Registries: map[string]RegistryConfig{ + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + }, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != false { + t.Errorf("Expected Enabled to remain false, got %v", *base.Enabled) + } + if len(base.Registries) != 1 { + t.Errorf("Expected 1 registry, got %d", len(base.Registries)) + } + if base.Registries["quay.io"].Remote != "quay.io" { + t.Errorf("Expected Registries[quay.io].Remote to be 'quay.io'") + } + if base.Registries["quay.io"].Local != "localhost:5001" { + t.Errorf("Expected Registries[quay.io].Local to be 'localhost:5001'") + } + if base.Registries["quay.io"].HostName != "localhost" { + t.Errorf("Expected Registries[quay.io].HostName to be 'localhost'") + } + if base.Registries["quay.io"].HostPort != 5001 { + t.Errorf("Expected Registries[quay.io].HostPort to be 5001") + } + }) + + t.Run("MergeWithNilBaseEnabled", func(t *testing.T) { + base := &RegistriesConfig{ + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + }, + } + overlay := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + }, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Registries) != 1 { + t.Errorf("Expected 1 registry, got %d", len(base.Registries)) + } + if base.Registries["quay.io"].Remote != "quay.io" { + t.Errorf("Expected Registries[quay.io].Remote to be 'quay.io'") + } + if base.Registries["quay.io"].Local != "localhost:5001" { + t.Errorf("Expected Registries[quay.io].Local to be 'localhost:5001'") + } + if base.Registries["quay.io"].HostName != "localhost" { + t.Errorf("Expected Registries[quay.io].HostName to be 'localhost'") + } + if base.Registries["quay.io"].HostPort != 5001 { + t.Errorf("Expected Registries[quay.io].HostPort to be 5001") + } + }) + + t.Run("MergeWithNilBaseRegistries", func(t *testing.T) { + base := &RegistriesConfig{ + Enabled: boolPtr(false), + } + overlay := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + }, + } + + base.Merge(overlay) + + if base.Enabled == nil || *base.Enabled != true { + t.Errorf("Expected Enabled to be true, got %v", *base.Enabled) + } + if len(base.Registries) != 2 { + t.Errorf("Expected 2 registries, got %d", len(base.Registries)) + } + if base.Registries["docker.io"].Remote != "docker.io" { + t.Errorf("Expected Registries[docker.io].Remote to be 'docker.io'") + } + if base.Registries["docker.io"].Local != "localhost:5000" { + t.Errorf("Expected Registries[docker.io].Local to be 'localhost:5000'") + } + if base.Registries["docker.io"].HostName != "localhost" { + t.Errorf("Expected Registries[docker.io].HostName to be 'localhost'") + } + if base.Registries["docker.io"].HostPort != 5000 { + t.Errorf("Expected Registries[docker.io].HostPort to be 5000") + } + if base.Registries["quay.io"].Remote != "quay.io" { + t.Errorf("Expected Registries[quay.io].Remote to be 'quay.io'") + } + if base.Registries["quay.io"].Local != "localhost:5001" { + t.Errorf("Expected Registries[quay.io].Local to be 'localhost:5001'") + } + if base.Registries["quay.io"].HostName != "localhost" { + t.Errorf("Expected Registries[quay.io].HostName to be 'localhost'") + } + if base.Registries["quay.io"].HostPort != 5001 { + t.Errorf("Expected Registries[quay.io].HostPort to be 5001") + } + }) +} + +// TestRegistriesConfig_Copy tests the Copy method of RegistriesConfig +func TestRegistriesConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *RegistriesConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &RegistriesConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.Enabled != nil { + t.Error("Expected Enabled to be nil in copy") + } + if copied.Registries != nil { + t.Error("Expected Registries to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + "gcr.io": { + Remote: "gcr.io", + Local: "localhost:5002", + HostName: "localhost", + HostPort: 5002, + }, + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if len(copied.Registries) != len(config.Registries) { + t.Errorf("Expected Registries length to be copied correctly") + } + for name, registry := range copied.Registries { + if registry.Remote != config.Registries[name].Remote { + t.Errorf("Expected Registries[%s].Remote to be copied correctly", name) + } + if registry.Local != config.Registries[name].Local { + t.Errorf("Expected Registries[%s].Local to be copied correctly", name) + } + if registry.HostName != config.Registries[name].HostName { + t.Errorf("Expected Registries[%s].HostName to be copied correctly", name) + } + if registry.HostPort != config.Registries[name].HostPort { + t.Errorf("Expected Registries[%s].HostPort to be copied correctly", name) + } + } + }) + + t.Run("CopyWithPartialFields", func(t *testing.T) { + config := &RegistriesConfig{ + Enabled: boolPtr(true), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copied.Registries != nil { + t.Error("Expected Registries to be nil in copy") + } + }) + + t.Run("CopyWithOnlyRegistries", func(t *testing.T) { + config := &RegistriesConfig{ + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled != nil { + t.Error("Expected Enabled to be nil in copy") + } + if len(copied.Registries) != len(config.Registries) { + t.Errorf("Expected Registries length to be copied correctly") + } + for name, registry := range copied.Registries { + if registry.Remote != config.Registries[name].Remote { + t.Errorf("Expected Registries[%s].Remote to be copied correctly", name) + } + if registry.Local != config.Registries[name].Local { + t.Errorf("Expected Registries[%s].Local to be copied correctly", name) + } + if registry.HostName != config.Registries[name].HostName { + t.Errorf("Expected Registries[%s].HostName to be copied correctly", name) + } + if registry.HostPort != config.Registries[name].HostPort { + t.Errorf("Expected Registries[%s].HostPort to be copied correctly", name) + } + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &RegistriesConfig{ + Enabled: boolPtr(true), + Registries: map[string]RegistryConfig{ + "docker.io": { + Remote: "docker.io", + Local: "localhost:5000", + HostName: "localhost", + HostPort: 5000, + }, + "quay.io": { + Remote: "quay.io", + Local: "localhost:5001", + HostName: "localhost", + HostPort: 5001, + }, + }, + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.Enabled = false + config.Registries["docker.io"] = RegistryConfig{ + Remote: "modified-docker.io", + Local: "modified-localhost:5000", + HostName: "modified-localhost", + HostPort: 9999, + } + config.Registries["new-registry"] = RegistryConfig{ + Remote: "new-registry.io", + Local: "localhost:5003", + HostName: "localhost", + HostPort: 5003, + } + + if *copied.Enabled != true { + t.Error("Expected copied Enabled to remain independent") + } + if len(copied.Registries) != 2 { + t.Error("Expected copied Registries length to remain independent") + } + if copied.Registries["docker.io"].Remote != "docker.io" { + t.Error("Expected copied Registries[docker.io].Remote to remain independent") + } + if copied.Registries["docker.io"].Local != "localhost:5000" { + t.Error("Expected copied Registries[docker.io].Local to remain independent") + } + if copied.Registries["docker.io"].HostName != "localhost" { + t.Error("Expected copied Registries[docker.io].HostName to remain independent") + } + if copied.Registries["docker.io"].HostPort != 5000 { + t.Error("Expected copied Registries[docker.io].HostPort to remain independent") + } + if copied.Registries["quay.io"].Remote != "quay.io" { + t.Error("Expected copied Registries[quay.io].Remote to remain independent") + } + if copied.Registries["quay.io"].Local != "localhost:5001" { + t.Error("Expected copied Registries[quay.io].Local to remain independent") + } + if copied.Registries["quay.io"].HostName != "localhost" { + t.Error("Expected copied Registries[quay.io].HostName to remain independent") + } + if copied.Registries["quay.io"].HostPort != 5001 { + t.Error("Expected copied Registries[quay.io].HostPort to remain independent") + } + }) + + t.Run("CopyWithEmptyRegistries", func(t *testing.T) { + config := &RegistriesConfig{ + Enabled: boolPtr(false), + Registries: map[string]RegistryConfig{}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Enabled == nil || *copied.Enabled != *config.Enabled { + t.Errorf("Expected Enabled to be copied correctly") + } + if copied.Registries == nil { + t.Error("Expected Registries to be initialized as empty map") + } + if len(copied.Registries) != 0 { + t.Error("Expected Registries to be empty map") + } + }) +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/workstation/vm/vm.go b/api/v1alpha2/config/workstation/vm/vm.go new file mode 100644 index 000000000..34736b14a --- /dev/null +++ b/api/v1alpha2/config/workstation/vm/vm.go @@ -0,0 +1,71 @@ +package workstation + +// VMConfig represents the VM configuration +type VMConfig struct { + Address *string `yaml:"address,omitempty"` + Arch *string `yaml:"arch,omitempty"` + CPU *int `yaml:"cpu,omitempty"` + Disk *int `yaml:"disk,omitempty"` + Driver *string `yaml:"driver,omitempty"` + Memory *int `yaml:"memory,omitempty"` +} + +// Merge performs a deep merge of the current VMConfig with another VMConfig. +func (base *VMConfig) Merge(overlay *VMConfig) { + if overlay == nil { + return + } + if overlay.Address != nil { + base.Address = overlay.Address + } + if overlay.Arch != nil { + base.Arch = overlay.Arch + } + if overlay.CPU != nil { + base.CPU = overlay.CPU + } + if overlay.Disk != nil { + base.Disk = overlay.Disk + } + if overlay.Driver != nil { + base.Driver = overlay.Driver + } + if overlay.Memory != nil { + base.Memory = overlay.Memory + } +} + +// DeepCopy creates a deep copy of the VMConfig object +func (c *VMConfig) DeepCopy() *VMConfig { + if c == nil { + return nil + } + copied := &VMConfig{} + + if c.Address != nil { + addressCopy := *c.Address + copied.Address = &addressCopy + } + if c.Arch != nil { + archCopy := *c.Arch + copied.Arch = &archCopy + } + if c.CPU != nil { + cpuCopy := *c.CPU + copied.CPU = &cpuCopy + } + if c.Disk != nil { + diskCopy := *c.Disk + copied.Disk = &diskCopy + } + if c.Driver != nil { + driverCopy := *c.Driver + copied.Driver = &driverCopy + } + if c.Memory != nil { + memoryCopy := *c.Memory + copied.Memory = &memoryCopy + } + + return copied +} diff --git a/api/v1alpha2/config/workstation/vm/vm_test.go b/api/v1alpha2/config/workstation/vm/vm_test.go new file mode 100644 index 000000000..f01d03b43 --- /dev/null +++ b/api/v1alpha2/config/workstation/vm/vm_test.go @@ -0,0 +1,544 @@ +package workstation + +import ( + "testing" +) + +// TestVMConfig_Merge tests the Merge method of VMConfig +func TestVMConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + original := base.DeepCopy() + + base.Merge(nil) + + if base.Address == nil || *base.Address != *original.Address { + t.Errorf("Expected Address to remain unchanged") + } + if base.Arch == nil || *base.Arch != *original.Arch { + t.Errorf("Expected Arch to remain unchanged") + } + if base.CPU == nil || *base.CPU != *original.CPU { + t.Errorf("Expected CPU to remain unchanged") + } + if base.Disk == nil || *base.Disk != *original.Disk { + t.Errorf("Expected Disk to remain unchanged") + } + if base.Driver == nil || *base.Driver != *original.Driver { + t.Errorf("Expected Driver to remain unchanged") + } + if base.Memory == nil || *base.Memory != *original.Memory { + t.Errorf("Expected Memory to remain unchanged") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{} + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Address: stringPtr("10.0.0.50"), + Arch: stringPtr("arm64"), + CPU: intPtr(8), + Disk: intPtr(200), + Driver: stringPtr("virtualbox"), + Memory: intPtr(16384), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "10.0.0.50" { + t.Errorf("Expected Address to be '10.0.0.50', got %s", *base.Address) + } + if base.Arch == nil || *base.Arch != "arm64" { + t.Errorf("Expected Arch to be 'arm64', got %s", *base.Arch) + } + if base.CPU == nil || *base.CPU != 8 { + t.Errorf("Expected CPU to be 8, got %d", *base.CPU) + } + if base.Disk == nil || *base.Disk != 200 { + t.Errorf("Expected Disk to be 200, got %d", *base.Disk) + } + if base.Driver == nil || *base.Driver != "virtualbox" { + t.Errorf("Expected Driver to be 'virtualbox', got %s", *base.Driver) + } + if base.Memory == nil || *base.Memory != 16384 { + t.Errorf("Expected Memory to be 16384, got %d", *base.Memory) + } + }) + + t.Run("MergeWithOnlyAddress", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Address: stringPtr("10.0.0.50"), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "10.0.0.50" { + t.Errorf("Expected Address to be '10.0.0.50', got %s", *base.Address) + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithOnlyArch", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Arch: stringPtr("arm64"), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "arm64" { + t.Errorf("Expected Arch to be 'arm64', got %s", *base.Arch) + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithOnlyCPU", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + CPU: intPtr(8), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 8 { + t.Errorf("Expected CPU to be 8, got %d", *base.CPU) + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithOnlyDisk", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Disk: intPtr(200), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 200 { + t.Errorf("Expected Disk to be 200, got %d", *base.Disk) + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithOnlyDriver", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Driver: stringPtr("virtualbox"), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "virtualbox" { + t.Errorf("Expected Driver to be 'virtualbox', got %s", *base.Driver) + } + if base.Memory == nil || *base.Memory != 8192 { + t.Errorf("Expected Memory to remain 8192") + } + }) + + t.Run("MergeWithOnlyMemory", func(t *testing.T) { + base := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + overlay := &VMConfig{ + Memory: intPtr(16384), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "192.168.1.100" { + t.Errorf("Expected Address to remain '192.168.1.100'") + } + if base.Arch == nil || *base.Arch != "x86_64" { + t.Errorf("Expected Arch to remain 'x86_64'") + } + if base.CPU == nil || *base.CPU != 4 { + t.Errorf("Expected CPU to remain 4") + } + if base.Disk == nil || *base.Disk != 100 { + t.Errorf("Expected Disk to remain 100") + } + if base.Driver == nil || *base.Driver != "qemu" { + t.Errorf("Expected Driver to remain 'qemu'") + } + if base.Memory == nil || *base.Memory != 16384 { + t.Errorf("Expected Memory to be 16384, got %d", *base.Memory) + } + }) + + t.Run("MergeWithNilBaseFields", func(t *testing.T) { + base := &VMConfig{} + overlay := &VMConfig{ + Address: stringPtr("10.0.0.50"), + Arch: stringPtr("arm64"), + CPU: intPtr(8), + Disk: intPtr(200), + Driver: stringPtr("virtualbox"), + Memory: intPtr(16384), + } + + base.Merge(overlay) + + if base.Address == nil || *base.Address != "10.0.0.50" { + t.Errorf("Expected Address to be '10.0.0.50'") + } + if base.Arch == nil || *base.Arch != "arm64" { + t.Errorf("Expected Arch to be 'arm64'") + } + if base.CPU == nil || *base.CPU != 8 { + t.Errorf("Expected CPU to be 8") + } + if base.Disk == nil || *base.Disk != 200 { + t.Errorf("Expected Disk to be 200") + } + if base.Driver == nil || *base.Driver != "virtualbox" { + t.Errorf("Expected Driver to be 'virtualbox'") + } + if base.Memory == nil || *base.Memory != 16384 { + t.Errorf("Expected Memory to be 16384") + } + }) +} + +// TestVMConfig_Copy tests the Copy method of VMConfig +func TestVMConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *VMConfig + copied := config.DeepCopy() + + if copied != nil { + t.Error("Expected nil copy for nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &VMConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy of empty config") + } + if copied.Address != nil { + t.Error("Expected Address to be nil in copy") + } + if copied.Arch != nil { + t.Error("Expected Arch to be nil in copy") + } + if copied.CPU != nil { + t.Error("Expected CPU to be nil in copy") + } + if copied.Disk != nil { + t.Error("Expected Disk to be nil in copy") + } + if copied.Driver != nil { + t.Error("Expected Driver to be nil in copy") + } + if copied.Memory != nil { + t.Error("Expected Memory to be nil in copy") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied == config { + t.Error("Expected copy to be a new instance") + } + if copied.Address == nil || *copied.Address != *config.Address { + t.Errorf("Expected Address to be copied correctly") + } + if copied.Arch == nil || *copied.Arch != *config.Arch { + t.Errorf("Expected Arch to be copied correctly") + } + if copied.CPU == nil || *copied.CPU != *config.CPU { + t.Errorf("Expected CPU to be copied correctly") + } + if copied.Disk == nil || *copied.Disk != *config.Disk { + t.Errorf("Expected Disk to be copied correctly") + } + if copied.Driver == nil || *copied.Driver != *config.Driver { + t.Errorf("Expected Driver to be copied correctly") + } + if copied.Memory == nil || *copied.Memory != *config.Memory { + t.Errorf("Expected Memory to be copied correctly") + } + }) + + t.Run("CopyWithPartialFields", func(t *testing.T) { + config := &VMConfig{ + Address: stringPtr("192.168.1.100"), + CPU: intPtr(4), + Memory: intPtr(8192), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Address == nil || *copied.Address != *config.Address { + t.Errorf("Expected Address to be copied correctly") + } + if copied.Arch != nil { + t.Error("Expected Arch to be nil in copy") + } + if copied.CPU == nil || *copied.CPU != *config.CPU { + t.Errorf("Expected CPU to be copied correctly") + } + if copied.Disk != nil { + t.Error("Expected Disk to be nil in copy") + } + if copied.Driver != nil { + t.Error("Expected Driver to be nil in copy") + } + if copied.Memory == nil || *copied.Memory != *config.Memory { + t.Errorf("Expected Memory to be copied correctly") + } + }) + + t.Run("CopyWithIndependentValues", func(t *testing.T) { + config := &VMConfig{ + Address: stringPtr("192.168.1.100"), + Arch: stringPtr("x86_64"), + CPU: intPtr(4), + Disk: intPtr(100), + Driver: stringPtr("qemu"), + Memory: intPtr(8192), + } + + copied := config.DeepCopy() + + // Modify original to verify independence + *config.Address = "10.0.0.50" + *config.Arch = "arm64" + *config.CPU = 8 + *config.Disk = 200 + *config.Driver = "virtualbox" + *config.Memory = 16384 + + if *copied.Address != "192.168.1.100" { + t.Error("Expected copied Address to remain independent") + } + if *copied.Arch != "x86_64" { + t.Error("Expected copied Arch to remain independent") + } + if *copied.CPU != 4 { + t.Error("Expected copied CPU to remain independent") + } + if *copied.Disk != 100 { + t.Error("Expected copied Disk to remain independent") + } + if *copied.Driver != "qemu" { + t.Error("Expected copied Driver to remain independent") + } + if *copied.Memory != 8192 { + t.Error("Expected copied Memory to remain independent") + } + }) + + t.Run("CopyWithSingleField", func(t *testing.T) { + config := &VMConfig{ + Address: stringPtr("192.168.1.100"), + } + + copied := config.DeepCopy() + + if copied == nil { + t.Error("Expected non-nil copy") + } + if copied.Address == nil || *copied.Address != *config.Address { + t.Errorf("Expected Address to be copied correctly") + } + if copied.Arch != nil { + t.Error("Expected Arch to be nil in copy") + } + if copied.CPU != nil { + t.Error("Expected CPU to be nil in copy") + } + if copied.Disk != nil { + t.Error("Expected Disk to be nil in copy") + } + if copied.Driver != nil { + t.Error("Expected Driver to be nil in copy") + } + if copied.Memory != nil { + t.Error("Expected Memory to be nil in copy") + } + }) +} + +func stringPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} diff --git a/api/v1alpha2/config/workstation/workstation.go b/api/v1alpha2/config/workstation/workstation.go new file mode 100644 index 000000000..e3f9e6f34 --- /dev/null +++ b/api/v1alpha2/config/workstation/workstation.go @@ -0,0 +1,92 @@ +package workstation + +import ( + clusterconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/cluster" + dnsconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/dns" + gitconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/git" + localstackconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/localstack" + networkconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/network" + registriesconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/registries" + vmconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/vm" +) + +// WorkstationConfig represents the local development workstation configuration +type WorkstationConfig struct { + Registries *registriesconfig.RegistriesConfig `yaml:"registries,omitempty"` + Git *gitconfig.GitConfig `yaml:"git,omitempty"` + VM *vmconfig.VMConfig `yaml:"vm,omitempty"` + Cluster *clusterconfig.ClusterConfig `yaml:"cluster,omitempty"` + Network *networkconfig.NetworkConfig `yaml:"network,omitempty"` + DNS *dnsconfig.DNSConfig `yaml:"dns,omitempty"` + Localstack *localstackconfig.LocalstackConfig `yaml:"localstack,omitempty"` +} + +// Merge performs a deep merge of the current WorkstationConfig with another WorkstationConfig. +func (base *WorkstationConfig) Merge(overlay *WorkstationConfig) { + if overlay == nil { + return + } + if overlay.Registries != nil { + if base.Registries == nil { + base.Registries = ®istriesconfig.RegistriesConfig{} + } + base.Registries.Merge(overlay.Registries) + } + if overlay.Git != nil { + if base.Git == nil { + base.Git = &gitconfig.GitConfig{} + } + base.Git.Merge(overlay.Git) + } + if overlay.VM != nil { + if base.VM == nil { + base.VM = &vmconfig.VMConfig{} + } + base.VM.Merge(overlay.VM) + } + if overlay.Cluster != nil { + if base.Cluster == nil { + base.Cluster = &clusterconfig.ClusterConfig{} + } + base.Cluster.Merge(overlay.Cluster) + } + if overlay.Network != nil { + if base.Network == nil { + base.Network = &networkconfig.NetworkConfig{} + } + base.Network.Merge(overlay.Network) + } + if overlay.DNS != nil { + if base.DNS == nil { + base.DNS = &dnsconfig.DNSConfig{} + } + base.DNS.Merge(overlay.DNS) + } + if overlay.Localstack != nil { + if base.Localstack == nil { + base.Localstack = &localstackconfig.LocalstackConfig{} + } + base.Localstack.Merge(overlay.Localstack) + } +} + +// DeepCopy creates a deep copy of the WorkstationConfig object +func (c *WorkstationConfig) DeepCopy() *WorkstationConfig { + if c == nil { + return nil + } + return &WorkstationConfig{ + Registries: c.Registries.DeepCopy(), + Git: c.Git.DeepCopy(), + VM: c.VM.DeepCopy(), + Cluster: c.Cluster.DeepCopy(), + Network: c.Network.DeepCopy(), + DNS: c.DNS.DeepCopy(), + Localstack: c.Localstack.DeepCopy(), + } +} + +// Helper function to create boolean pointers +func ptrBool(b bool) *bool { + return &b +} diff --git a/api/v1alpha2/config/workstation/workstation_test.go b/api/v1alpha2/config/workstation/workstation_test.go new file mode 100644 index 000000000..6785c5886 --- /dev/null +++ b/api/v1alpha2/config/workstation/workstation_test.go @@ -0,0 +1,361 @@ +package workstation + +import ( + "reflect" + "testing" + + clusterconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/cluster" + dnsconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/dns" + gitconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/git" + localstackconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/localstack" + networkconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/network" + registriesconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/registries" + vmconfig "github.com/windsorcli/cli/api/v1alpha2/config/workstation/vm" +) + +// TestWorkstationConfig_Merge tests the Merge method of WorkstationConfig +func TestWorkstationConfig_Merge(t *testing.T) { + t.Run("MergeWithNilOverlay", func(t *testing.T) { + base := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + } + original := base.DeepCopy() + + base.Merge(nil) + + if !reflect.DeepEqual(base, original) { + t.Errorf("Expected no change when merging with nil overlay") + } + }) + + t.Run("MergeWithEmptyOverlay", func(t *testing.T) { + base := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + } + original := base.DeepCopy() + + overlay := &WorkstationConfig{} + base.Merge(overlay) + + if !reflect.DeepEqual(base, original) { + t.Errorf("Expected no change when merging with empty overlay") + } + }) + + t.Run("MergeWithPartialOverlay", func(t *testing.T) { + base := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + } + + overlay := &WorkstationConfig{ + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(true), + Driver: ptrString("talos"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + }, + } + + base.Merge(overlay) + + if base.Cluster == nil { + t.Errorf("Expected Cluster to be initialized") + } + if base.DNS == nil { + t.Errorf("Expected DNS to be initialized") + } + if !*base.Cluster.Enabled { + t.Errorf("Expected Cluster.Enabled to be true") + } + if *base.Cluster.Driver != "talos" { + t.Errorf("Expected Cluster.Driver to be 'talos', got %s", *base.Cluster.Driver) + } + if !*base.DNS.Enabled { + t.Errorf("Expected DNS.Enabled to be true") + } + if *base.DNS.Domain != "test.local" { + t.Errorf("Expected DNS.Domain to be 'test.local', got %s", *base.DNS.Domain) + } + }) + + t.Run("MergeWithCompleteOverlay", func(t *testing.T) { + base := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(false), + Driver: ptrString("kind"), + }, + } + + overlay := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(true), + Driver: ptrString("talos"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + }, + } + + base.Merge(overlay) + + if !*base.Cluster.Enabled { + t.Errorf("Expected Cluster.Enabled to be true after merge") + } + if *base.Cluster.Driver != "talos" { + t.Errorf("Expected Cluster.Driver to be 'talos' after merge") + } + if base.DNS == nil { + t.Errorf("Expected DNS to be initialized") + } + if !*base.DNS.Enabled { + t.Errorf("Expected DNS.Enabled to be true") + } + }) + + t.Run("MergeWithAllConfigTypes", func(t *testing.T) { + base := &WorkstationConfig{} + + overlay := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{ + Enabled: ptrBool(true), + }, + Git: &gitconfig.GitConfig{ + Livereload: &gitconfig.GitLivereloadConfig{ + Enabled: ptrBool(true), + }, + }, + VM: &vmconfig.VMConfig{ + Driver: ptrString("colima"), + }, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(true), + }, + Network: &networkconfig.NetworkConfig{ + CIDRBlock: ptrString("10.0.0.0/24"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(true), + }, + Localstack: &localstackconfig.LocalstackConfig{ + Enabled: ptrBool(true), + }, + } + + base.Merge(overlay) + + // Verify all configs are initialized + if base.Registries == nil { + t.Errorf("Expected Registries to be initialized") + } + if base.Git == nil { + t.Errorf("Expected Git to be initialized") + } + if base.VM == nil { + t.Errorf("Expected VM to be initialized") + } + if base.Cluster == nil { + t.Errorf("Expected Cluster to be initialized") + } + if base.Network == nil { + t.Errorf("Expected Network to be initialized") + } + if base.DNS == nil { + t.Errorf("Expected DNS to be initialized") + } + if base.Localstack == nil { + t.Errorf("Expected Localstack to be initialized") + } + + // Verify all configs have expected values + if !*base.Registries.Enabled { + t.Errorf("Expected Registries.Enabled to be true") + } + if base.Git.Livereload == nil || !*base.Git.Livereload.Enabled { + t.Errorf("Expected Git.Livereload.Enabled to be true") + } + if base.VM.Driver == nil || *base.VM.Driver != "colima" { + t.Errorf("Expected VM.Driver to be 'colima'") + } + if !*base.Cluster.Enabled { + t.Errorf("Expected Cluster.Enabled to be true") + } + if base.Network.CIDRBlock == nil || *base.Network.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected Network.CIDRBlock to be '10.0.0.0/24'") + } + if !*base.DNS.Enabled { + t.Errorf("Expected DNS.Enabled to be true") + } + if !*base.Localstack.Enabled { + t.Errorf("Expected Localstack.Enabled to be true") + } + }) + + t.Run("MergeWithExistingConfigs", func(t *testing.T) { + base := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{ + Enabled: ptrBool(false), + }, + Git: &gitconfig.GitConfig{ + Livereload: &gitconfig.GitLivereloadConfig{ + Enabled: ptrBool(false), + }, + }, + VM: &vmconfig.VMConfig{ + Driver: ptrString("docker"), + }, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(false), + }, + Network: &networkconfig.NetworkConfig{ + CIDRBlock: ptrString("192.168.0.0/24"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(false), + }, + Localstack: &localstackconfig.LocalstackConfig{ + Enabled: ptrBool(false), + }, + } + + overlay := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{ + Enabled: ptrBool(true), + }, + Git: &gitconfig.GitConfig{ + Livereload: &gitconfig.GitLivereloadConfig{ + Enabled: ptrBool(true), + }, + }, + VM: &vmconfig.VMConfig{ + Driver: ptrString("colima"), + }, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(true), + }, + Network: &networkconfig.NetworkConfig{ + CIDRBlock: ptrString("10.0.0.0/24"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(true), + }, + Localstack: &localstackconfig.LocalstackConfig{ + Enabled: ptrBool(true), + }, + } + + base.Merge(overlay) + + // Verify all configs are updated + if !*base.Registries.Enabled { + t.Errorf("Expected Registries.Enabled to be true after merge") + } + if base.Git.Livereload == nil || !*base.Git.Livereload.Enabled { + t.Errorf("Expected Git.Livereload.Enabled to be true after merge") + } + if base.VM.Driver == nil || *base.VM.Driver != "colima" { + t.Errorf("Expected VM.Driver to be 'colima' after merge") + } + if !*base.Cluster.Enabled { + t.Errorf("Expected Cluster.Enabled to be true after merge") + } + if base.Network.CIDRBlock == nil || *base.Network.CIDRBlock != "10.0.0.0/24" { + t.Errorf("Expected Network.CIDRBlock to be '10.0.0.0/24' after merge") + } + if !*base.DNS.Enabled { + t.Errorf("Expected DNS.Enabled to be true after merge") + } + if !*base.Localstack.Enabled { + t.Errorf("Expected Localstack.Enabled to be true after merge") + } + }) +} + +// TestWorkstationConfig_Copy tests the Copy method of WorkstationConfig +func TestWorkstationConfig_Copy(t *testing.T) { + t.Run("CopyNilConfig", func(t *testing.T) { + var config *WorkstationConfig + copied := config.DeepCopy() + + if copied != nil { + t.Errorf("Expected nil when copying nil config") + } + }) + + t.Run("CopyEmptyConfig", func(t *testing.T) { + config := &WorkstationConfig{} + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy of empty config") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) + + t.Run("CopyPopulatedConfig", func(t *testing.T) { + config := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + Cluster: &clusterconfig.ClusterConfig{ + Enabled: ptrBool(true), + Driver: ptrString("talos"), + }, + DNS: &dnsconfig.DNSConfig{ + Enabled: ptrBool(true), + Domain: ptrString("test.local"), + }, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + + // Verify deep copy by modifying original + *config.Cluster.Enabled = false + if !*copied.Cluster.Enabled { + t.Errorf("Expected copy to be independent of original") + } + }) + + t.Run("CopyWithAllFieldsPopulated", func(t *testing.T) { + config := &WorkstationConfig{ + Registries: ®istriesconfig.RegistriesConfig{}, + Git: &gitconfig.GitConfig{}, + VM: &vmconfig.VMConfig{}, + Cluster: &clusterconfig.ClusterConfig{}, + Network: &networkconfig.NetworkConfig{}, + DNS: &dnsconfig.DNSConfig{}, + Localstack: &localstackconfig.LocalstackConfig{}, + } + + copied := config.DeepCopy() + + if copied == nil { + t.Errorf("Expected non-nil copy") + } + if !reflect.DeepEqual(config, copied) { + t.Errorf("Expected copy to be equal to original") + } + }) +} + +// Helper function for creating string pointers +func ptrString(s string) *string { + return &s +}