From 25cc84be192a35f52bc420822bdd47193384d6a5 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Thu, 7 Aug 2025 19:59:27 -0400 Subject: [PATCH] refactor(config): Begin designing v1alpha2 config schema The v1alpha2 schema represents the next structure of the `windsor.yaml` file. The major change involves creating a dedicated block for configuring the workstation. Now, workstations could be considered more of a common resource that does not strictly belong to a context. If a local workstation exists, contexts can provision resources on it and leverage its services. Other changes involve organizing cloud provider blocks, and simplifying Terraform backend config. Some minor changes like dropping "provider" and "id" are in preparation for a blueprint focused `values.yaml` feature in the future. --- api/v1alpha2/config/config.go | 77 +++ api/v1alpha2/config/config_test.go | 236 ++++++++ api/v1alpha2/config/providers/aws/aws.go | 69 +++ api/v1alpha2/config/providers/aws/aws_test.go | 132 +++++ api/v1alpha2/config/providers/azure/azure.go | 62 ++ .../config/providers/azure/azure_test.go | 164 +++++ api/v1alpha2/config/providers/providers.go | 48 ++ .../config/providers/providers_test.go | 317 ++++++++++ .../config/secrets/onepassword/onepassword.go | 57 ++ .../secrets/onepassword/onepassword_test.go | 237 ++++++++ api/v1alpha2/config/secrets/secrets.go | 33 ++ api/v1alpha2/config/secrets/secrets_test.go | 116 ++++ api/v1alpha2/config/terraform/terraform.go | 48 ++ .../config/terraform/terraform_test.go | 148 +++++ .../config/workstation/cluster/cluster.go | 301 ++++++++++ .../workstation/cluster/cluster_test.go | 424 +++++++++++++ api/v1alpha2/config/workstation/dns/dns.go | 69 +++ .../config/workstation/dns/dns_test.go | 217 +++++++ api/v1alpha2/config/workstation/git/git.go | 109 ++++ .../config/workstation/git/git_test.go | 412 +++++++++++++ .../workstation/localstack/localstack.go | 41 ++ .../workstation/localstack/localstack_test.go | 330 +++++++++++ .../config/workstation/network/network.go | 72 +++ .../workstation/network/network_test.go | 403 +++++++++++++ .../workstation/registries/registries.go | 66 +++ .../workstation/registries/registries_test.go | 561 ++++++++++++++++++ api/v1alpha2/config/workstation/vm/vm.go | 71 +++ api/v1alpha2/config/workstation/vm/vm_test.go | 544 +++++++++++++++++ .../config/workstation/workstation.go | 92 +++ .../config/workstation/workstation_test.go | 361 +++++++++++ 30 files changed, 5817 insertions(+) create mode 100644 api/v1alpha2/config/config.go create mode 100644 api/v1alpha2/config/config_test.go create mode 100644 api/v1alpha2/config/providers/aws/aws.go create mode 100644 api/v1alpha2/config/providers/aws/aws_test.go create mode 100644 api/v1alpha2/config/providers/azure/azure.go create mode 100644 api/v1alpha2/config/providers/azure/azure_test.go create mode 100644 api/v1alpha2/config/providers/providers.go create mode 100644 api/v1alpha2/config/providers/providers_test.go create mode 100644 api/v1alpha2/config/secrets/onepassword/onepassword.go create mode 100644 api/v1alpha2/config/secrets/onepassword/onepassword_test.go create mode 100644 api/v1alpha2/config/secrets/secrets.go create mode 100644 api/v1alpha2/config/secrets/secrets_test.go create mode 100644 api/v1alpha2/config/terraform/terraform.go create mode 100644 api/v1alpha2/config/terraform/terraform_test.go create mode 100644 api/v1alpha2/config/workstation/cluster/cluster.go create mode 100644 api/v1alpha2/config/workstation/cluster/cluster_test.go create mode 100644 api/v1alpha2/config/workstation/dns/dns.go create mode 100644 api/v1alpha2/config/workstation/dns/dns_test.go create mode 100644 api/v1alpha2/config/workstation/git/git.go create mode 100644 api/v1alpha2/config/workstation/git/git_test.go create mode 100644 api/v1alpha2/config/workstation/localstack/localstack.go create mode 100644 api/v1alpha2/config/workstation/localstack/localstack_test.go create mode 100644 api/v1alpha2/config/workstation/network/network.go create mode 100644 api/v1alpha2/config/workstation/network/network_test.go create mode 100644 api/v1alpha2/config/workstation/registries/registries.go create mode 100644 api/v1alpha2/config/workstation/registries/registries_test.go create mode 100644 api/v1alpha2/config/workstation/vm/vm.go create mode 100644 api/v1alpha2/config/workstation/vm/vm_test.go create mode 100644 api/v1alpha2/config/workstation/workstation.go create mode 100644 api/v1alpha2/config/workstation/workstation_test.go 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 +}