diff --git a/api/v1alpha1/blueprint_types.go b/api/v1alpha1/blueprint_types.go index 2c0df9189..463741d99 100644 --- a/api/v1alpha1/blueprint_types.go +++ b/api/v1alpha1/blueprint_types.go @@ -36,16 +36,6 @@ type Blueprint struct { Kustomizations []Kustomization `yaml:"kustomize"` } -type PartialBlueprint struct { - Kind string `yaml:"kind"` - ApiVersion string `yaml:"apiVersion"` - Metadata Metadata `yaml:"metadata"` - Sources []Source `yaml:"sources"` - Repository Repository `yaml:"repository"` - TerraformComponents []TerraformComponent `yaml:"terraform"` - Kustomizations []map[string]any `yaml:"kustomize"` -} - // Metadata describes a blueprint. type Metadata struct { // Name is the blueprint's unique identifier. @@ -117,8 +107,11 @@ type TerraformComponent struct { // DependsOn lists dependencies of this terraform component. DependsOn []string `yaml:"dependsOn,omitempty"` - // Values are configuration values for the module. - Values map[string]any `yaml:"values,omitempty"` + // Inputs are configuration values for the module. + // These values can be expressions using ${} syntax (e.g., "${cluster.name}") or literals. + // Values with ${} are evaluated as expressions, plain values are passed through as literals. + // These are used for generating tfvars files and are not written to the final context blueprint.yaml. + Inputs map[string]any `yaml:"inputs,omitempty"` // Destroy determines if the component should be destroyed during down operations. // Defaults to true if not specified. @@ -129,6 +122,28 @@ type TerraformComponent struct { Parallelism *int `yaml:"parallelism,omitempty"` } +// DeepCopy creates a deep copy of the TerraformComponent object. +func (t *TerraformComponent) DeepCopy() *TerraformComponent { + if t == nil { + return nil + } + + inputsCopy := maps.Clone(t.Inputs) + + dependsOnCopy := make([]string, len(t.DependsOn)) + copy(dependsOnCopy, t.DependsOn) + + return &TerraformComponent{ + Source: t.Source, + Path: t.Path, + FullPath: t.FullPath, + DependsOn: dependsOnCopy, + Inputs: inputsCopy, + Destroy: t.Destroy, + Parallelism: t.Parallelism, + } +} + // BlueprintPatch represents a patch in the blueprint format. // This is converted to kustomize.Patch during processing. // Supports both blueprint format (Path) and Flux format (Patch + Target). @@ -184,12 +199,15 @@ type Kustomization struct { // Cleanup lists resources to clean up after the kustomization is applied. Cleanup []string `yaml:"cleanup,omitempty"` - // PostBuild is a post-build step to run after the kustomization is applied. - PostBuild *PostBuild `yaml:"postBuild,omitempty"` - // Destroy determines if the kustomization should be destroyed during down operations. // Defaults to true if not specified. Destroy *bool `yaml:"destroy,omitempty"` + + // Substitutions contains values for post-build variable replacement, + // collected and stored in ConfigMaps for use by Flux postBuild substitution. + // All values are converted to strings as required by Flux variable substitution. + // These are used for generating ConfigMaps and are not written to the final context blueprint.yaml. + Substitutions map[string]string `yaml:"substitutions,omitempty"` } // PostBuild is a post-build step to run after the kustomization is applied. @@ -255,20 +273,7 @@ func (b *Blueprint) DeepCopy() *Blueprint { terraformComponentsCopy := make([]TerraformComponent, len(b.TerraformComponents)) for i, component := range b.TerraformComponents { - valuesCopy := make(map[string]any, len(component.Values)) - maps.Copy(valuesCopy, component.Values) - - dependsOnCopy := append([]string{}, component.DependsOn...) - - terraformComponentsCopy[i] = TerraformComponent{ - Source: component.Source, - Path: component.Path, - FullPath: component.FullPath, - DependsOn: dependsOnCopy, - Values: valuesCopy, - Destroy: component.Destroy, - Parallelism: component.Parallelism, - } + terraformComponentsCopy[i] = *component.DeepCopy() } kustomizationsCopy := make([]Kustomization, len(b.Kustomizations)) @@ -287,71 +292,73 @@ func (b *Blueprint) DeepCopy() *Blueprint { } } -// StrategicMerge performs a strategic merge of the provided overlay Blueprint into the receiver Blueprint. -// This method appends to array fields, deep merges map fields, and updates scalar fields if present in the overlay. +// StrategicMerge performs a strategic merge of the provided overlay Blueprints into the receiver Blueprint. +// This method appends to array fields, deep merges map fields, and updates scalar fields if present in the overlays. // It is designed for feature composition, enabling the combination of multiple features into a single blueprint. -func (b *Blueprint) StrategicMerge(overlay *Blueprint) error { - if overlay == nil { - return nil - } +func (b *Blueprint) StrategicMerge(overlays ...*Blueprint) error { + for _, overlay := range overlays { + if overlay == nil { + continue + } - if overlay.Kind != "" { - b.Kind = overlay.Kind - } - if overlay.ApiVersion != "" { - b.ApiVersion = overlay.ApiVersion - } + if overlay.Kind != "" { + b.Kind = overlay.Kind + } + if overlay.ApiVersion != "" { + b.ApiVersion = overlay.ApiVersion + } - if overlay.Metadata.Name != "" { - b.Metadata.Name = overlay.Metadata.Name - } - if overlay.Metadata.Description != "" { - b.Metadata.Description = overlay.Metadata.Description - } + if overlay.Metadata.Name != "" { + b.Metadata.Name = overlay.Metadata.Name + } + if overlay.Metadata.Description != "" { + b.Metadata.Description = overlay.Metadata.Description + } - if overlay.Repository.Url != "" { - b.Repository.Url = overlay.Repository.Url - } + if overlay.Repository.Url != "" { + b.Repository.Url = overlay.Repository.Url + } - if overlay.Repository.Ref.Commit != "" { - b.Repository.Ref.Commit = overlay.Repository.Ref.Commit - } else if overlay.Repository.Ref.Name != "" { - b.Repository.Ref.Name = overlay.Repository.Ref.Name - } else if overlay.Repository.Ref.SemVer != "" { - b.Repository.Ref.SemVer = overlay.Repository.Ref.SemVer - } else if overlay.Repository.Ref.Tag != "" { - b.Repository.Ref.Tag = overlay.Repository.Ref.Tag - } else if overlay.Repository.Ref.Branch != "" { - b.Repository.Ref.Branch = overlay.Repository.Ref.Branch - } + if overlay.Repository.Ref.Commit != "" { + b.Repository.Ref.Commit = overlay.Repository.Ref.Commit + } else if overlay.Repository.Ref.Name != "" { + b.Repository.Ref.Name = overlay.Repository.Ref.Name + } else if overlay.Repository.Ref.SemVer != "" { + b.Repository.Ref.SemVer = overlay.Repository.Ref.SemVer + } else if overlay.Repository.Ref.Tag != "" { + b.Repository.Ref.Tag = overlay.Repository.Ref.Tag + } else if overlay.Repository.Ref.Branch != "" { + b.Repository.Ref.Branch = overlay.Repository.Ref.Branch + } - if overlay.Repository.SecretName != "" { - b.Repository.SecretName = overlay.Repository.SecretName - } + if overlay.Repository.SecretName != "" { + b.Repository.SecretName = overlay.Repository.SecretName + } - sourceMap := make(map[string]Source) - for _, source := range b.Sources { - sourceMap[source.Name] = source - } - for _, overlaySource := range overlay.Sources { - if overlaySource.Name != "" { - sourceMap[overlaySource.Name] = overlaySource + sourceMap := make(map[string]Source) + for _, source := range b.Sources { + sourceMap[source.Name] = source + } + for _, overlaySource := range overlay.Sources { + if overlaySource.Name != "" { + sourceMap[overlaySource.Name] = overlaySource + } + } + b.Sources = make([]Source, 0, len(sourceMap)) + for _, source := range sourceMap { + b.Sources = append(b.Sources, source) } - } - b.Sources = make([]Source, 0, len(sourceMap)) - for _, source := range sourceMap { - b.Sources = append(b.Sources, source) - } - for _, overlayComponent := range overlay.TerraformComponents { - if err := b.strategicMergeTerraformComponent(overlayComponent); err != nil { - return err + for _, overlayComponent := range overlay.TerraformComponents { + if err := b.strategicMergeTerraformComponent(overlayComponent); err != nil { + return err + } } - } - for _, overlayK := range overlay.Kustomizations { - if err := b.strategicMergeKustomization(overlayK); err != nil { - return err + for _, overlayK := range overlay.Kustomizations { + if err := b.strategicMergeKustomization(overlayK); err != nil { + return err + } } } return nil @@ -363,11 +370,11 @@ func (b *Blueprint) StrategicMerge(overlay *Blueprint) error { func (b *Blueprint) strategicMergeTerraformComponent(component TerraformComponent) error { for i, existing := range b.TerraformComponents { if existing.Path == component.Path && existing.Source == component.Source { - if len(component.Values) > 0 { - if existing.Values == nil { - existing.Values = make(map[string]any) + if len(component.Inputs) > 0 { + if existing.Inputs == nil { + existing.Inputs = make(map[string]any) } - maps.Copy(existing.Values, component.Values) + maps.Copy(existing.Inputs, component.Inputs) } for _, dep := range component.DependsOn { if !slices.Contains(existing.DependsOn, dep) { @@ -566,19 +573,6 @@ func (k *Kustomization) DeepCopy() *Kustomization { return nil } - var postBuildCopy *PostBuild - if k.PostBuild != nil { - substituteCopy := maps.Clone(k.PostBuild.Substitute) - substituteFromCopy := slices.Clone(k.PostBuild.SubstituteFrom) - - if len(substituteCopy) > 0 || len(substituteFromCopy) > 0 { - postBuildCopy = &PostBuild{ - Substitute: substituteCopy, - SubstituteFrom: substituteFromCopy, - } - } - } - return &Kustomization{ Name: k.Name, Path: k.Path, @@ -593,8 +587,8 @@ func (k *Kustomization) DeepCopy() *Kustomization { Prune: k.Prune, Components: slices.Clone(k.Components), Cleanup: slices.Clone(k.Cleanup), - PostBuild: postBuildCopy, Destroy: k.Destroy, + Substitutions: maps.Clone(k.Substitutions), } } diff --git a/api/v1alpha1/blueprint_types_test.go b/api/v1alpha1/blueprint_types_test.go index be17f678a..5f12b7ea5 100644 --- a/api/v1alpha1/blueprint_types_test.go +++ b/api/v1alpha1/blueprint_types_test.go @@ -21,7 +21,7 @@ func TestBlueprint_StrategicMerge(t *testing.T) { { Path: "network/vpc", Source: "core", - Values: map[string]any{"cidr": "10.0.0.0/16"}, + Inputs: map[string]any{"cidr": "10.0.0.0/16"}, DependsOn: []string{"backend"}, }, }, @@ -33,13 +33,13 @@ func TestBlueprint_StrategicMerge(t *testing.T) { { Path: "network/vpc", // Same path+source - should merge Source: "core", - Values: map[string]any{"enable_dns": true}, + Inputs: map[string]any{"enable_dns": true}, DependsOn: []string{"security"}, }, { Path: "cluster/eks", // New component - should append Source: "core", - Values: map[string]any{"version": "1.28"}, + Inputs: map[string]any{"version": "1.28"}, }, }, } @@ -57,13 +57,13 @@ func TestBlueprint_StrategicMerge(t *testing.T) { if vpc.Path != "network/vpc" { t.Errorf("Expected path 'network/vpc', got '%s'", vpc.Path) } - if len(vpc.Values) != 2 { - t.Errorf("Expected 2 values, got %d", len(vpc.Values)) + if len(vpc.Inputs) != 2 { + t.Errorf("Expected 2 inputs, got %d", len(vpc.Inputs)) } - if vpc.Values["cidr"] != "10.0.0.0/16" { + if vpc.Inputs["cidr"] != "10.0.0.0/16" { t.Errorf("Expected original cidr value preserved") } - if vpc.Values["enable_dns"] != true { + if vpc.Inputs["enable_dns"] != true { t.Errorf("Expected new enable_dns value added") } if len(vpc.DependsOn) != 2 { @@ -263,15 +263,15 @@ func TestBlueprint_StrategicMerge(t *testing.T) { // Given a base blueprint with existing components base := &Blueprint{ TerraformComponents: []TerraformComponent{ - {Path: "existing-component", Source: "core", Values: map[string]any{"key": "value"}}, - {Path: "another-existing", Source: "core", Values: map[string]any{"other": "data"}}, + {Path: "existing-component", Source: "core", Inputs: map[string]any{"key": "value"}}, + {Path: "another-existing", Source: "core", Inputs: map[string]any{"other": "data"}}, }, } // When strategic merging with overlay that only has one component overlay := &Blueprint{ TerraformComponents: []TerraformComponent{ - {Path: "new-component", Source: "core", Values: map[string]any{"new": "value"}}, + {Path: "new-component", Source: "core", Inputs: map[string]any{"new": "value"}}, }, } @@ -295,18 +295,18 @@ func TestBlueprint_StrategicMerge(t *testing.T) { switch comp.Path { case "existing-component": foundExisting = true - if comp.Values["key"] != "value" { - t.Errorf("Expected existing component values to be preserved") + if comp.Inputs["key"] != "value" { + t.Errorf("Expected existing component inputs to be preserved") } case "another-existing": foundAnother = true - if comp.Values["other"] != "data" { - t.Errorf("Expected another existing component values to be preserved") + if comp.Inputs["other"] != "data" { + t.Errorf("Expected another existing component inputs to be preserved") } case "new-component": foundNew = true - if comp.Values["new"] != "value" { - t.Errorf("Expected new component values to be added") + if comp.Inputs["new"] != "value" { + t.Errorf("Expected new component inputs to be added") } } } @@ -740,7 +740,7 @@ func TestBlueprint_DeepCopy(t *testing.T) { { Source: "source1", Path: "module/path1", - Values: map[string]any{ + Inputs: map[string]any{ "key1": "value1", }, }, @@ -750,14 +750,6 @@ func TestBlueprint_DeepCopy(t *testing.T) { Name: "kustomization1", Path: "kustomize/path1", Components: []string{"component1"}, - PostBuild: &PostBuild{ - Substitute: map[string]string{ - "key1": "value1", - }, - SubstituteFrom: []SubstituteReference{ - {Kind: "ConfigMap", Name: "config1"}, - }, - }, }, }, } @@ -774,8 +766,8 @@ func TestBlueprint_DeepCopy(t *testing.T) { if copy.TerraformComponents[0].Path != "module/path1" { t.Errorf("Expected copy to have terraform component path %v, but got %v", "module/path1", copy.TerraformComponents[0].Path) } - if len(copy.TerraformComponents[0].Values) != 1 || copy.TerraformComponents[0].Values["key1"] != "value1" { - t.Errorf("Expected copy to have terraform component value 'key1' with value 'value1', but got %v", copy.TerraformComponents[0].Values) + if len(copy.TerraformComponents[0].Inputs) != 1 || copy.TerraformComponents[0].Inputs["key1"] != "value1" { + t.Errorf("Expected copy to have terraform component input 'key1' with value 'value1', but got %v", copy.TerraformComponents[0].Inputs) } if len(copy.Kustomizations) != 1 || copy.Kustomizations[0].Name != "kustomization1" { t.Errorf("Expected copy to have kustomization 'kustomization1', but got %v", copy.Kustomizations) @@ -783,12 +775,6 @@ func TestBlueprint_DeepCopy(t *testing.T) { if len(copy.Kustomizations[0].Components) != 1 || copy.Kustomizations[0].Components[0] != "component1" { t.Errorf("Expected copy to have kustomization component 'component1', but got %v", copy.Kustomizations[0].Components) } - if len(copy.Kustomizations[0].PostBuild.Substitute) != 1 || copy.Kustomizations[0].PostBuild.Substitute["key1"] != "value1" { - t.Errorf("Expected copy to have Substitute 'key1:value1', but got %v", copy.Kustomizations[0].PostBuild.Substitute) - } - if len(copy.Kustomizations[0].PostBuild.SubstituteFrom) != 1 || copy.Kustomizations[0].PostBuild.SubstituteFrom[0].Kind != "ConfigMap" || copy.Kustomizations[0].PostBuild.SubstituteFrom[0].Name != "config1" { - t.Errorf("Expected copy to have SubstituteFrom 'ConfigMap:config1', but got %v", copy.Kustomizations[0].PostBuild.SubstituteFrom) - } }) t.Run("EmptyBlueprint", func(t *testing.T) { diff --git a/api/v1alpha1/feature_types.go b/api/v1alpha1/feature_types.go index 954d4f27b..9063f28d9 100644 --- a/api/v1alpha1/feature_types.go +++ b/api/v1alpha1/feature_types.go @@ -2,10 +2,6 @@ // +groupName=blueprints.windsorcli.dev package v1alpha1 -import ( - "maps" -) - // Feature represents a conditional blueprint fragment that can be merged into a base blueprint. // Features enable modular composition of blueprints based on user configuration values. // Features inherit Repository and Sources from the base blueprint they are merged into. @@ -38,11 +34,6 @@ type ConditionalTerraformComponent struct { // When is a CEL expression that determines if this terraform component should be applied. // If empty, the component is always applied when the parent feature matches. When string `yaml:"when,omitempty"` - - // Inputs contains input values for the terraform module. - // Values can be expressions using ${} syntax (e.g., "${cluster.workers.count + 2}") or literals (e.g., "us-east-1"). - // Values with ${} are evaluated as expressions, plain values are passed through as literals. - Inputs map[string]any `yaml:"inputs,omitempty"` } // ConditionalKustomization extends Kustomization with conditional logic support. @@ -52,13 +43,6 @@ type ConditionalKustomization struct { // When is a CEL expression that determines if this kustomization should be applied. // If empty, the kustomization is always applied when the parent feature matches. When string `yaml:"when,omitempty"` - - // Substitutions contains substitution values for post-build variable replacement. - // These values are collected and stored in ConfigMaps for use by Flux postBuild substitution. - // Values can be expressions using ${} syntax (e.g., "${dns.domain}") or literals (e.g., "example.com"). - // Values with ${} are evaluated as expressions, plain values are passed through as literals. - // All values are converted to strings as required by Flux variable substitution. - Substitutions map[string]string `yaml:"substitutions,omitempty"` } // DeepCopy creates a deep copy of the Feature object. @@ -74,39 +58,12 @@ func (f *Feature) DeepCopy() *Feature { terraformComponentsCopy := make([]ConditionalTerraformComponent, len(f.TerraformComponents)) for i, component := range f.TerraformComponents { - valuesCopy := make(map[string]any, len(component.Values)) - maps.Copy(valuesCopy, component.Values) - - inputsCopy := make(map[string]any, len(component.Inputs)) - maps.Copy(inputsCopy, component.Inputs) - - dependsOnCopy := append([]string{}, component.DependsOn...) - - terraformComponentsCopy[i] = ConditionalTerraformComponent{ - TerraformComponent: TerraformComponent{ - Source: component.Source, - Path: component.Path, - FullPath: component.FullPath, - DependsOn: dependsOnCopy, - Values: valuesCopy, - Destroy: component.Destroy, - Parallelism: component.Parallelism, - }, - When: component.When, - Inputs: inputsCopy, - } + terraformComponentsCopy[i] = *component.DeepCopy() } kustomizationsCopy := make([]ConditionalKustomization, len(f.Kustomizations)) for i, kustomization := range f.Kustomizations { - substitutionsCopy := make(map[string]string, len(kustomization.Substitutions)) - maps.Copy(substitutionsCopy, kustomization.Substitutions) - - kustomizationsCopy[i] = ConditionalKustomization{ - Kustomization: *kustomization.Kustomization.DeepCopy(), - When: kustomization.When, - Substitutions: substitutionsCopy, - } + kustomizationsCopy[i] = *kustomization.DeepCopy() } return &Feature{ @@ -125,26 +82,9 @@ func (c *ConditionalTerraformComponent) DeepCopy() *ConditionalTerraformComponen return nil } - valuesCopy := make(map[string]any, len(c.Values)) - maps.Copy(valuesCopy, c.Values) - - inputsCopy := make(map[string]any, len(c.Inputs)) - maps.Copy(inputsCopy, c.Inputs) - - dependsOnCopy := append([]string{}, c.DependsOn...) - return &ConditionalTerraformComponent{ - TerraformComponent: TerraformComponent{ - Source: c.Source, - Path: c.Path, - FullPath: c.FullPath, - DependsOn: dependsOnCopy, - Values: valuesCopy, - Destroy: c.Destroy, - Parallelism: c.Parallelism, - }, - When: c.When, - Inputs: inputsCopy, + TerraformComponent: *c.TerraformComponent.DeepCopy(), + When: c.When, } } @@ -154,12 +94,8 @@ func (c *ConditionalKustomization) DeepCopy() *ConditionalKustomization { return nil } - substitutionsCopy := make(map[string]string, len(c.Substitutions)) - maps.Copy(substitutionsCopy, c.Substitutions) - return &ConditionalKustomization{ Kustomization: *c.Kustomization.DeepCopy(), When: c.When, - Substitutions: substitutionsCopy, } } diff --git a/api/v1alpha1/feature_types_test.go b/api/v1alpha1/feature_types_test.go index 74c5d49e7..f6bab292b 100644 --- a/api/v1alpha1/feature_types_test.go +++ b/api/v1alpha1/feature_types_test.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "testing" + "github.com/goccy/go-yaml" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,7 +30,7 @@ func TestFeatureDeepCopy(t *testing.T) { TerraformComponent: TerraformComponent{ Path: "network/aws-vpc", DependsOn: []string{"policy-base"}, - Values: map[string]any{ + Inputs: map[string]any{ "cidr": "10.0.0.0/16", }, }, @@ -43,11 +44,11 @@ func TestFeatureDeepCopy(t *testing.T) { Path: "ingress", Components: []string{"nginx", "nginx/web"}, DependsOn: []string{"pki-base"}, + Substitutions: map[string]string{ + "host": "example.com", + }, }, When: "ingress.enabled == true", - Substitutions: map[string]string{ - "host": "example.com", - }, }, }, } @@ -73,9 +74,9 @@ func TestFeatureDeepCopy(t *testing.T) { t.Error("Deep copy failed: metadata was not copied") } - original.TerraformComponents[0].Values["cidr"] = "modified" - if copy.TerraformComponents[0].Values["cidr"] == "modified" { - t.Error("Deep copy failed: terraform values map was not copied") + original.TerraformComponents[0].Inputs["cidr"] = "modified" + if copy.TerraformComponents[0].Inputs["cidr"] == "modified" { + t.Error("Deep copy failed: terraform inputs map was not copied") } original.Kustomizations[0].Components[0] = "modified" @@ -104,7 +105,7 @@ func TestConditionalTerraformComponentDeepCopy(t *testing.T) { TerraformComponent: TerraformComponent{ Path: "network/aws-vpc", DependsOn: []string{"policy-base", "pki-base"}, - Values: map[string]any{ + Inputs: map[string]any{ "cidr": "10.0.0.0/16", "subnets": []string{"10.0.1.0/24", "10.0.2.0/24"}, }, @@ -127,9 +128,9 @@ func TestConditionalTerraformComponentDeepCopy(t *testing.T) { t.Error("Deep copy failed: dependsOn slice was not copied") } - original.Values["cidr"] = "modified" - if copy.Values["cidr"] == "modified" { - t.Error("Deep copy failed: values map was not copied") + original.Inputs["cidr"] = "modified" + if copy.Inputs["cidr"] == "modified" { + t.Error("Deep copy failed: inputs map was not copied") } }) } @@ -152,12 +153,11 @@ func TestConditionalKustomizationDeepCopy(t *testing.T) { Components: []string{"nginx", "nginx/web"}, DependsOn: []string{"pki-base"}, Interval: interval, + Substitutions: map[string]string{ + "host": "example.com", + }, }, When: "ingress.enabled == true", - Substitutions: map[string]string{ - "host": "example.com", - "replicas": "3", - }, } copy := original.DeepCopy() @@ -188,7 +188,7 @@ func TestConditionalKustomizationDeepCopy(t *testing.T) { } func TestFeatureYAMLTags(t *testing.T) { - t.Run("FeatureHasCorrectYamlTags", func(t *testing.T) { + t.Run("FeatureMarshalsAndUnmarshalsYAML", func(t *testing.T) { feature := Feature{ Kind: "Feature", ApiVersion: "blueprints.windsorcli.dev/v1alpha1", @@ -216,16 +216,37 @@ func TestFeatureYAMLTags(t *testing.T) { }, } - // This test ensures the struct can be marshaled/unmarshaled - // The actual YAML tag validation is implicit through compilation - if feature.Kind == "" { - t.Error("Feature should have Kind field") + data, err := yaml.Marshal(&feature) + if err != nil { + t.Fatalf("Failed to marshal Feature struct to YAML: %v", err) + } + + var out Feature + err = yaml.Unmarshal(data, &out) + if err != nil { + t.Fatalf("Failed to unmarshal YAML into Feature struct: %v", err) + } + + if out.Kind != feature.Kind { + t.Errorf("Expected Kind %q, got %q after YAML unmarshal", feature.Kind, out.Kind) + } + if out.ApiVersion != feature.ApiVersion { + t.Errorf("Expected ApiVersion %q, got %q after YAML unmarshal", feature.ApiVersion, out.ApiVersion) + } + if out.Metadata.Name != feature.Metadata.Name { + t.Errorf("Expected Metadata.Name %q, got %q after YAML unmarshal", feature.Metadata.Name, out.Metadata.Name) + } + if out.Metadata.Description != feature.Metadata.Description { + t.Errorf("Expected Metadata.Description %q, got %q after YAML unmarshal", feature.Metadata.Description, out.Metadata.Description) + } + if out.When != feature.When { + t.Errorf("Expected When %q, got %q after YAML unmarshal", feature.When, out.When) } - if feature.ApiVersion == "" { - t.Error("Feature should have ApiVersion field") + if len(out.TerraformComponents) != len(feature.TerraformComponents) { + t.Errorf("Expected %d TerraformComponents, got %d after YAML unmarshal", len(feature.TerraformComponents), len(out.TerraformComponents)) } - if feature.When == "" { - t.Error("Feature should have When field") + if len(out.Kustomizations) != len(feature.Kustomizations) { + t.Errorf("Expected %d Kustomizations, got %d after YAML unmarshal", len(feature.Kustomizations), len(out.Kustomizations)) } }) } diff --git a/pkg/blueprint/blueprint_handler.go b/pkg/blueprint/blueprint_handler.go index fedaf2a9e..205383a86 100644 --- a/pkg/blueprint/blueprint_handler.go +++ b/pkg/blueprint/blueprint_handler.go @@ -210,7 +210,7 @@ func (b *BaseBlueprintHandler) Write(overwrite ...bool) error { cleanedBlueprint := b.blueprint.DeepCopy() for i := range cleanedBlueprint.TerraformComponents { - cleanedBlueprint.TerraformComponents[i].Values = map[string]any{} + cleanedBlueprint.TerraformComponents[i].Inputs = map[string]any{} } data, err := b.shims.YamlMarshal(cleanedBlueprint) @@ -434,15 +434,6 @@ func (b *BaseBlueprintHandler) GetKustomizations() []blueprintv1alpha1.Kustomiza kustomizations[i].Destroy = &defaultDestroy } - kustomizations[i].PostBuild = &blueprintv1alpha1.PostBuild{ - SubstituteFrom: []blueprintv1alpha1.SubstituteReference{ - { - Kind: "ConfigMap", - Name: "values-common", - Optional: false, - }, - }, - } } return kustomizations @@ -653,9 +644,6 @@ func (b *BaseBlueprintHandler) destroyKustomizations(ctx context.Context, kustom RetryInterval: &metav1.Duration{Duration: constants.DEFAULT_FLUX_KUSTOMIZATION_RETRY_INTERVAL}, Wait: func() *bool { b := true; return &b }(), Force: func() *bool { b := true; return &b }(), - PostBuild: &blueprintv1alpha1.PostBuild{ - SubstituteFrom: []blueprintv1alpha1.SubstituteReference{}, - }, } if err := b.kubernetesManager.ApplyKustomization(b.toFluxKustomization(*cleanupKustomization, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE)); err != nil { @@ -824,11 +812,11 @@ func (b *BaseBlueprintHandler) processFeatures(templateData map[string][]byte, c } if len(filteredInputs) > 0 { - if component.Values == nil { - component.Values = make(map[string]any) + if component.Inputs == nil { + component.Inputs = make(map[string]any) } - component.Values = b.deepMergeMaps(component.Values, filteredInputs) + component.Inputs = b.deepMergeMaps(component.Inputs, filteredInputs) } } @@ -1001,27 +989,12 @@ func (b *BaseBlueprintHandler) resolveComponentPaths(blueprint *blueprintv1alpha // components and kustomizations lacking a source to use the OCI source, and ensures the OCI source is present // or updated in the sources slice. func (b *BaseBlueprintHandler) processBlueprintData(data []byte, blueprint *blueprintv1alpha1.Blueprint, ociInfo ...*artifact.OCIArtifactInfo) error { - newBlueprint := &blueprintv1alpha1.PartialBlueprint{} + newBlueprint := &blueprintv1alpha1.Blueprint{} if err := b.shims.YamlUnmarshal(data, newBlueprint); err != nil { return fmt.Errorf("error unmarshalling blueprint data: %w", err) } - var kustomizations []blueprintv1alpha1.Kustomization - - for _, kMap := range newBlueprint.Kustomizations { - kustomizationYAML, err := b.shims.YamlMarshalNonNull(kMap) - if err != nil { - return fmt.Errorf("error marshalling kustomization map: %w", err) - } - - var kustomization blueprintv1alpha1.Kustomization - err = b.shims.K8sYamlUnmarshal(kustomizationYAML, &kustomization) - if err != nil { - return fmt.Errorf("error unmarshalling kustomization YAML: %w", err) - } - - kustomizations = append(kustomizations, kustomization) - } + kustomizations := newBlueprint.Kustomizations sources := newBlueprint.Sources terraformComponents := newBlueprint.TerraformComponents @@ -1369,31 +1342,8 @@ func (b *BaseBlueprintHandler) toFluxKustomization(k blueprintv1alpha1.Kustomiza }) } - if k.PostBuild != nil { - for _, ref := range k.PostBuild.SubstituteFrom { - duplicate := false - for _, existing := range substituteFrom { - if existing.Kind == ref.Kind && existing.Name == ref.Name { - duplicate = true - break - } - } - if !duplicate { - substituteFrom = append(substituteFrom, kustomizev1.SubstituteReference{ - Kind: ref.Kind, - Name: ref.Name, - Optional: ref.Optional, - }) - } - } - postBuild = &kustomizev1.PostBuild{ - Substitute: k.PostBuild.Substitute, - SubstituteFrom: substituteFrom, - } - } else { - postBuild = &kustomizev1.PostBuild{ - SubstituteFrom: substituteFrom, - } + postBuild = &kustomizev1.PostBuild{ + SubstituteFrom: substituteFrom, } interval := metav1.Duration{Duration: k.Interval.Duration} diff --git a/pkg/blueprint/blueprint_handler_private_test.go b/pkg/blueprint/blueprint_handler_private_test.go index d17165be2..545bdf590 100644 --- a/pkg/blueprint/blueprint_handler_private_test.go +++ b/pkg/blueprint/blueprint_handler_private_test.go @@ -796,7 +796,7 @@ func TestBaseBlueprintHandler_toFluxKustomization(t *testing.T) { } }) - t.Run("WithExistingPostBuild", func(t *testing.T) { + t.Run("WithoutFeatureSubstitutions", func(t *testing.T) { // Given a handler handler := setup(t) @@ -824,7 +824,7 @@ func TestBaseBlueprintHandler_toFluxKustomization(t *testing.T) { return nil, os.ErrNotExist } - // And a kustomization with existing PostBuild + // And a kustomization without PostBuild kustomization := blueprintv1alpha1.Kustomization{ Name: "test-kustomization", Path: "test/path", @@ -834,54 +834,19 @@ func TestBaseBlueprintHandler_toFluxKustomization(t *testing.T) { Timeout: &metav1.Duration{Duration: 10 * time.Minute}, Force: &[]bool{false}[0], Wait: &[]bool{false}[0], - PostBuild: &blueprintv1alpha1.PostBuild{ - Substitute: map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - }, - SubstituteFrom: []blueprintv1alpha1.SubstituteReference{ - { - Kind: "ConfigMap", - Name: "existing-config", - Optional: true, - }, - }, - }, } // When converting to Flux kustomization result := handler.toFluxKustomization(kustomization, "test-namespace") - // Then it should have PostBuild with both existing and new references + // Then it should have PostBuild with only ConfigMap references from feature substitutions if result.Spec.PostBuild == nil { t.Fatal("expected PostBuild to be set") } - // And it should preserve existing Substitute values - if len(result.Spec.PostBuild.Substitute) != 2 { - t.Errorf("expected 2 Substitute values, got %d", len(result.Spec.PostBuild.Substitute)) - } - if result.Spec.PostBuild.Substitute["VAR1"] != "value1" { - t.Errorf("expected VAR1 to be 'value1', got '%s'", result.Spec.PostBuild.Substitute["VAR1"]) - } - if result.Spec.PostBuild.Substitute["VAR2"] != "value2" { - t.Errorf("expected VAR2 to be 'value2', got '%s'", result.Spec.PostBuild.Substitute["VAR2"]) - } - - // And it should preserve the existing SubstituteFrom reference - existingConfigFound := false - - for _, ref := range result.Spec.PostBuild.SubstituteFrom { - if ref.Kind == "ConfigMap" && ref.Name == "existing-config" { - existingConfigFound = true - if ref.Optional != true { - t.Errorf("expected existing-config to be Optional=true, got %v", ref.Optional) - } - } - } - - if !existingConfigFound { - t.Error("expected existing-config ConfigMap reference to be preserved") + // And it should not have any Substitute values since PostBuild is no longer user-facing + if len(result.Spec.PostBuild.Substitute) != 0 { + t.Errorf("expected 0 Substitute values, got %d", len(result.Spec.PostBuild.Substitute)) } // And it should not add a component ConfigMap without feature substitutions @@ -3296,9 +3261,8 @@ metadata: when: provider == "aws" terraform: - path: cluster/aws-eks - values: - cluster_name: my-cluster inputs: + cluster_name: my-cluster node_groups: default: instance_types: @@ -3336,13 +3300,13 @@ terraform: component := handler.blueprint.TerraformComponents[0] - if component.Values["cluster_name"] != "my-cluster" { - t.Errorf("Expected cluster_name to be 'my-cluster', got %v", component.Values["cluster_name"]) + if component.Inputs["cluster_name"] != "my-cluster" { + t.Errorf("Expected cluster_name to be 'my-cluster', got %v", component.Inputs["cluster_name"]) } - nodeGroups, ok := component.Values["node_groups"].(map[string]any) + nodeGroups, ok := component.Inputs["node_groups"].(map[string]any) if !ok { - t.Fatalf("Expected node_groups to be a map, got %T", component.Values["node_groups"]) + t.Fatalf("Expected node_groups to be a map, got %T", component.Inputs["node_groups"]) } defaultGroup, ok := nodeGroups["default"].(map[string]any) @@ -3370,12 +3334,12 @@ terraform: t.Errorf("Expected desired_size to be 3, got %v", defaultGroup["desired_size"]) } - if component.Values["region"] != "us-east-1" { - t.Errorf("Expected region to be literal 'us-east-1', got %v", component.Values["region"]) + if component.Inputs["region"] != "us-east-1" { + t.Errorf("Expected region to be literal 'us-east-1', got %v", component.Inputs["region"]) } - if component.Values["literal_string"] != "my-literal-value" { - t.Errorf("Expected literal_string to be 'my-literal-value', got %v", component.Values["literal_string"]) + if component.Inputs["literal_string"] != "my-literal-value" { + t.Errorf("Expected literal_string to be 'my-literal-value', got %v", component.Inputs["literal_string"]) } }) diff --git a/pkg/blueprint/blueprint_handler_public_test.go b/pkg/blueprint/blueprint_handler_public_test.go index e4674d133..912526f75 100644 --- a/pkg/blueprint/blueprint_handler_public_test.go +++ b/pkg/blueprint/blueprint_handler_public_test.go @@ -882,27 +882,6 @@ func TestBlueprintHandler_LoadConfig(t *testing.T) { } }) - t.Run("ErrorMarshallingYamlNonNull", func(t *testing.T) { - // Given a blueprint handler - handler, mocks := setup(t) - - // And a mock yaml marshaller that returns an error - mocks.Shims.YamlMarshalNonNull = func(v any) ([]byte, error) { - return nil, fmt.Errorf("mock error marshalling yaml non null") - } - - // When loading the config - err := handler.LoadConfig() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error when marshalling yaml non null, got nil") - } - if !strings.Contains(err.Error(), "mock error marshalling yaml non null") { - t.Errorf("Expected error containing 'mock error marshalling yaml non null', got: %v", err) - } - }) - t.Run("PathBackslashNormalization", func(t *testing.T) { handler, _ := setup(t) handler.blueprint.Kustomizations = []blueprintv1alpha1.Kustomization{ @@ -2208,7 +2187,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { { Source: "source1", Path: "path/to/module", - Values: map[string]any{"key": "value"}, + Inputs: map[string]any{"key": "value"}, }, } handler.blueprint.TerraformComponents = components @@ -2234,8 +2213,8 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { } // And the values should be preserved - if resolvedComponents[0].Values["key"] != "value" { - t.Errorf("Expected value 'value' for key 'key', got %q", resolvedComponents[0].Values["key"]) + if resolvedComponents[0].Inputs["key"] != "value" { + t.Errorf("Expected value 'value' for key 'key', got %q", resolvedComponents[0].Inputs["key"]) } }) @@ -2282,7 +2261,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { { Source: "source1", Path: "path\\to\\module", - Values: map[string]any{"key": "value"}, + Inputs: map[string]any{"key": "value"}, }, } handler.blueprint.TerraformComponents = components @@ -2332,7 +2311,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { { Source: "oci-source", Path: "cluster/talos", - Values: map[string]any{"key": "value"}, + Inputs: map[string]any{"key": "value"}, }, } handler.blueprint.TerraformComponents = components @@ -3587,7 +3566,7 @@ func TestBlueprintHandler_Write(t *testing.T) { { Source: "core", Path: "cluster/talos", - Values: map[string]any{ + Inputs: map[string]any{ "cluster_name": "test-cluster", // Should be kept (not a terraform variable) "cluster_endpoint": "https://test:6443", // Should be filtered if it's a terraform variable "controlplanes": []string{"node1"}, // Should be filtered if it's a terraform variable @@ -3644,17 +3623,8 @@ func TestBlueprintHandler_Write(t *testing.T) { component := writtenBlueprint.TerraformComponents[0] // Verify all values are cleared from the blueprint.yaml - if len(component.Values) != 0 { - t.Errorf("Expected all values to be cleared, but got %d values: %v", len(component.Values), component.Values) - } - - // Also verify kustomizations have postBuild cleared - if len(writtenBlueprint.Kustomizations) > 0 { - for i, kustomization := range writtenBlueprint.Kustomizations { - if kustomization.PostBuild != nil { - t.Errorf("Expected PostBuild to be cleared for kustomization %d, but got %v", i, kustomization.PostBuild) - } - } + if len(component.Inputs) != 0 { + t.Errorf("Expected all inputs to be cleared, but got %d inputs: %v", len(component.Inputs), component.Inputs) } }) diff --git a/pkg/generators/generator_test.go b/pkg/generators/generator_test.go index da2e32dd8..06da34ce1 100644 --- a/pkg/generators/generator_test.go +++ b/pkg/generators/generator_test.go @@ -116,7 +116,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { Source: "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git//terraform/remote/path@v1.0.0", Path: "remote/path", FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "remote/path"), - Values: map[string]any{ + Inputs: map[string]any{ "remote_variable1": "default_value", }, } @@ -125,7 +125,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { Source: "local/path", Path: "local/path", FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "local/path"), - Values: map[string]any{ + Inputs: map[string]any{ "local_variable1": "default_value", }, } @@ -321,8 +321,6 @@ func TestGenerator_Initialize(t *testing.T) { }) } - - func TestGenerator_Generate(t *testing.T) { setup := func(t *testing.T) (*BaseGenerator, *Mocks) { mocks := setupMocks(t) diff --git a/pkg/generators/terraform_generator.go b/pkg/generators/terraform_generator.go index 52acf10cb..0793f4978 100644 --- a/pkg/generators/terraform_generator.go +++ b/pkg/generators/terraform_generator.go @@ -81,7 +81,7 @@ func (g *TerraformGenerator) Generate(data map[string]any, overwrite ...bool) er components := g.blueprintHandler.GetTerraformComponents() for _, component := range components { - componentValues := component.Values + componentValues := component.Inputs if componentValues == nil { componentValues = make(map[string]any) } diff --git a/pkg/stack/stack_test.go b/pkg/stack/stack_test.go index 9a8f8984a..8e4fe1475 100644 --- a/pkg/stack/stack_test.go +++ b/pkg/stack/stack_test.go @@ -79,7 +79,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { Source: "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git//terraform/remote/path@v1.0.0", Path: "remote/path", FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "remote", "path"), - Values: map[string]any{ + Inputs: map[string]any{ "remote_variable1": "default_value", }, }, @@ -87,7 +87,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { Source: "", Path: "local/path", FullPath: filepath.Join(tmpDir, "terraform", "local", "path"), - Values: map[string]any{ + Inputs: map[string]any{ "local_variable1": "default_value", }, }, diff --git a/pkg/terraform/module_resolver_test.go b/pkg/terraform/module_resolver_test.go index 2c70dde75..7a0cf6353 100644 --- a/pkg/terraform/module_resolver_test.go +++ b/pkg/terraform/module_resolver_test.go @@ -108,7 +108,7 @@ contexts: Path: "test-module", Source: "git::https://github.com/test/module.git", FullPath: filepath.Join(tmpDir, "terraform", "test-module"), - Values: map[string]any{ + Inputs: map[string]any{ "cluster_name": "test-cluster", }, },