From 7b874193e1914b6ef87eb68578ef87ee78f57432 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Wed, 17 Sep 2025 22:32:34 -0400 Subject: [PATCH] chore(blueprint): Add blueprint Feature type Sets up the new Feature type, which behaves as a trimmed down conditional fragment of a blueprint. --- api/v1alpha1/feature_types.go | 137 ++++++++++++++++++ api/v1alpha1/feature_types_test.go | 214 +++++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 api/v1alpha1/feature_types.go create mode 100644 api/v1alpha1/feature_types_test.go diff --git a/api/v1alpha1/feature_types.go b/api/v1alpha1/feature_types.go new file mode 100644 index 000000000..dc0426b74 --- /dev/null +++ b/api/v1alpha1/feature_types.go @@ -0,0 +1,137 @@ +// Package v1alpha1 contains types for the v1alpha1 API group +// +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. +type Feature struct { + // Kind is the feature type, following Kubernetes conventions. + Kind string `yaml:"kind"` + + // ApiVersion is the API schema version of the feature. + ApiVersion string `yaml:"apiVersion"` + + // Metadata includes the feature's name and description. + Metadata Metadata `yaml:"metadata"` + + // When is a CEL expression that determines if this feature should be applied. + // The expression is evaluated against user configuration values. + // Examples: "provider == 'aws'", "observability.enabled == true && observability.backend == 'quickwit'" + When string `yaml:"when,omitempty"` + + // TerraformComponents are Terraform modules in the feature. + TerraformComponents []ConditionalTerraformComponent `yaml:"terraform,omitempty"` + + // Kustomizations are kustomization configs in the feature. + Kustomizations []ConditionalKustomization `yaml:"kustomize,omitempty"` +} + +// ConditionalTerraformComponent extends TerraformComponent with conditional logic support. +type ConditionalTerraformComponent struct { + TerraformComponent `yaml:",inline"` + + // 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"` +} + +// ConditionalKustomization extends Kustomization with conditional logic support. +type ConditionalKustomization struct { + Kustomization `yaml:",inline"` + + // 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"` +} + +// DeepCopy creates a deep copy of the Feature object. +func (f *Feature) DeepCopy() *Feature { + if f == nil { + return nil + } + + metadataCopy := Metadata{ + Name: f.Metadata.Name, + Description: f.Metadata.Description, + } + + 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) + + 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, + } + } + + kustomizationsCopy := make([]ConditionalKustomization, len(f.Kustomizations)) + for i, kustomization := range f.Kustomizations { + kustomizationsCopy[i] = ConditionalKustomization{ + Kustomization: *kustomization.Kustomization.DeepCopy(), + When: kustomization.When, + } + } + + return &Feature{ + Kind: f.Kind, + ApiVersion: f.ApiVersion, + Metadata: metadataCopy, + When: f.When, + TerraformComponents: terraformComponentsCopy, + Kustomizations: kustomizationsCopy, + } +} + +// DeepCopy creates a deep copy of the ConditionalTerraformComponent object. +func (c *ConditionalTerraformComponent) DeepCopy() *ConditionalTerraformComponent { + if c == nil { + return nil + } + + valuesCopy := make(map[string]any, len(c.Values)) + maps.Copy(valuesCopy, c.Values) + + 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, + } +} + +// DeepCopy creates a deep copy of the ConditionalKustomization object. +func (c *ConditionalKustomization) DeepCopy() *ConditionalKustomization { + if c == nil { + return nil + } + + return &ConditionalKustomization{ + Kustomization: *c.Kustomization.DeepCopy(), + When: c.When, + } +} diff --git a/api/v1alpha1/feature_types_test.go b/api/v1alpha1/feature_types_test.go new file mode 100644 index 000000000..2ef4a3979 --- /dev/null +++ b/api/v1alpha1/feature_types_test.go @@ -0,0 +1,214 @@ +package v1alpha1 + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestFeatureDeepCopy(t *testing.T) { + t.Run("ReturnsNilForNilFeature", func(t *testing.T) { + var f *Feature + result := f.DeepCopy() + if result != nil { + t.Errorf("Expected nil, got %v", result) + } + }) + + t.Run("CreatesDeepCopyOfFeature", func(t *testing.T) { + original := &Feature{ + Kind: "Feature", + ApiVersion: "blueprints.windsorcli.dev/v1alpha1", + Metadata: Metadata{ + Name: "test-feature", + Description: "Test feature", + }, + When: "provider == 'aws'", + TerraformComponents: []ConditionalTerraformComponent{ + { + TerraformComponent: TerraformComponent{ + Path: "network/aws-vpc", + DependsOn: []string{"policy-base"}, + Values: map[string]any{ + "cidr": "10.0.0.0/16", + }, + }, + When: "vpc.enabled == true", + }, + }, + Kustomizations: []ConditionalKustomization{ + { + Kustomization: Kustomization{ + Name: "ingress", + Path: "ingress", + Components: []string{"nginx", "nginx/web"}, + DependsOn: []string{"pki-base"}, + }, + When: "ingress.enabled == true", + }, + }, + } + + copy := original.DeepCopy() + + if copy.Kind != original.Kind { + t.Errorf("Expected Kind %s, got %s", original.Kind, copy.Kind) + } + if copy.ApiVersion != original.ApiVersion { + t.Errorf("Expected ApiVersion %s, got %s", original.ApiVersion, copy.ApiVersion) + } + if copy.Metadata.Name != original.Metadata.Name { + t.Errorf("Expected Name %s, got %s", original.Metadata.Name, copy.Metadata.Name) + } + if copy.When != original.When { + t.Errorf("Expected When %s, got %s", original.When, copy.When) + } + + // Verify deep copy by modifying original + original.Metadata.Description = "modified" + if copy.Metadata.Description == "modified" { + 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.Kustomizations[0].Components[0] = "modified" + if copy.Kustomizations[0].Components[0] == "modified" { + t.Error("Deep copy failed: kustomization components slice was not copied") + } + }) +} + +func TestConditionalTerraformComponentDeepCopy(t *testing.T) { + t.Run("ReturnsNilForNilComponent", func(t *testing.T) { + var c *ConditionalTerraformComponent + result := c.DeepCopy() + if result != nil { + t.Errorf("Expected nil, got %v", result) + } + }) + + t.Run("CreatesDeepCopyOfConditionalTerraformComponent", func(t *testing.T) { + original := &ConditionalTerraformComponent{ + TerraformComponent: TerraformComponent{ + Path: "network/aws-vpc", + DependsOn: []string{"policy-base", "pki-base"}, + Values: map[string]any{ + "cidr": "10.0.0.0/16", + "subnets": []string{"10.0.1.0/24", "10.0.2.0/24"}, + }, + }, + When: "provider == 'aws'", + } + + copy := original.DeepCopy() + + if copy.Path != original.Path { + t.Errorf("Expected Path %s, got %s", original.Path, copy.Path) + } + if copy.When != original.When { + t.Errorf("Expected When %s, got %s", original.When, copy.When) + } + + // Verify deep copy by modifying original + original.DependsOn[0] = "modified" + if copy.DependsOn[0] == "modified" { + 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") + } + }) +} + +func TestConditionalKustomizationDeepCopy(t *testing.T) { + t.Run("ReturnsNilForNilKustomization", func(t *testing.T) { + var k *ConditionalKustomization + result := k.DeepCopy() + if result != nil { + t.Errorf("Expected nil, got %v", result) + } + }) + + t.Run("CreatesDeepCopyOfConditionalKustomization", func(t *testing.T) { + interval := &metav1.Duration{} + original := &ConditionalKustomization{ + Kustomization: Kustomization{ + Name: "ingress", + Path: "ingress", + Components: []string{"nginx", "nginx/web"}, + DependsOn: []string{"pki-base"}, + Interval: interval, + }, + When: "ingress.enabled == true", + } + + copy := original.DeepCopy() + + if copy.Name != original.Name { + t.Errorf("Expected Name %s, got %s", original.Name, copy.Name) + } + if copy.When != original.When { + t.Errorf("Expected When %s, got %s", original.When, copy.When) + } + + // Verify deep copy by modifying original + original.Components[0] = "modified" + if copy.Components[0] == "modified" { + t.Error("Deep copy failed: components slice was not copied") + } + + original.DependsOn[0] = "modified" + if copy.DependsOn[0] == "modified" { + t.Error("Deep copy failed: dependsOn slice was not copied") + } + }) +} + +func TestFeatureYAMLTags(t *testing.T) { + t.Run("FeatureHasCorrectYamlTags", func(t *testing.T) { + feature := Feature{ + Kind: "Feature", + ApiVersion: "blueprints.windsorcli.dev/v1alpha1", + Metadata: Metadata{ + Name: "test-feature", + Description: "Test feature description", + }, + When: "provider == 'aws'", + TerraformComponents: []ConditionalTerraformComponent{ + { + TerraformComponent: TerraformComponent{ + Path: "network/aws-vpc", + }, + When: "vpc.enabled == true", + }, + }, + Kustomizations: []ConditionalKustomization{ + { + Kustomization: Kustomization{ + Name: "ingress", + Path: "ingress", + }, + When: "ingress.enabled == true", + }, + }, + } + + // 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") + } + if feature.ApiVersion == "" { + t.Error("Feature should have ApiVersion field") + } + if feature.When == "" { + t.Error("Feature should have When field") + } + }) +}