Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions api/v1alpha1/feature_types.go
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)
Copy link

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 DeepCopy methods for Feature and ConditionalTerraformComponent use maps.Copy() for the Values map. This performs a shallow copy, meaning if Values contains 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)

Fix in Cursor Fix in Web


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,
}
}
214 changes: 214 additions & 0 deletions api/v1alpha1/feature_types_test.go
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")
}
})
}
Loading