-
Notifications
You must be signed in to change notification settings - Fork 0
chore(blueprint): Add blueprint Feature type #1667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+351
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| } | ||
| }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Deep Copy Methods Fail for Mutable Map Values
The
DeepCopymethods forFeatureandConditionalTerraformComponentusemaps.Copy()for theValuesmap. This performs a shallow copy, meaning ifValuescontains mutable types like slices or nested maps, the original and copy will share references. Modifying these shared values in one object will unexpectedly affect the other, violating deep copy behavior.Additional Locations (1)
api/v1alpha1/feature_types.go#L107-L109