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
79 changes: 77 additions & 2 deletions src/internal/pattern/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,14 @@ func ProcessClusterGroupValues(patternName, clusterGroupName, repoRoot string, c
}

if err == nil {
// File exists, unmarshal into our defaults (natural merging)
if err = yaml.Unmarshal(yamlFile, values); err != nil {
// File exists, unmarshal into a separate struct first
var existingValues types.ValuesClusterGroup
if err = yaml.Unmarshal(yamlFile, &existingValues); err != nil {
return fmt.Errorf("failed to unmarshal YAML from %s: %w", clusterGroupValuesPath, err)
}

// Merge existing values with new defaults intelligently
mergeClusterGroupValues(values, &existingValues)
}

// Write back the merged values
Expand All @@ -136,3 +140,74 @@ func ProcessClusterGroupValues(patternName, clusterGroupName, repoRoot string, c

return nil
}

// mergeClusterGroupValues intelligently merges existing values with new defaults
func mergeClusterGroupValues(defaults, existing *types.ValuesClusterGroup) {
// Preserve existing applications and merge with new ones
for key, app := range existing.ClusterGroup.Applications {
defaults.ClusterGroup.Applications[key] = app
}

// For namespaces: preserve existing ones and add secrets-related ones if needed
existingNamespaceMap := make(map[string]bool)
for _, ns := range existing.ClusterGroup.Namespaces {
// Add existing namespace to defaults if not already present
found := false
for _, defaultNs := range defaults.ClusterGroup.Namespaces {
if ns.Equal(defaultNs) {
found = true
break
}
}
if !found {
defaults.ClusterGroup.Namespaces = append(defaults.ClusterGroup.Namespaces, ns)
}
// Track what we have
if nsStr, ok := ns.GetString(); ok {
existingNamespaceMap[nsStr] = true
}
}

// For projects: preserve existing ones and add cluster group project if secrets are needed
existingProjectMap := make(map[string]bool)
for _, proj := range existing.ClusterGroup.Projects {
existingProjectMap[proj] = true
}

// Rebuild projects list preserving existing order but ensuring required projects are present
mergedProjects := make([]string, 0)

// Add existing projects first
mergedProjects = append(mergedProjects, existing.ClusterGroup.Projects...)

// Add any missing required projects
for _, proj := range defaults.ClusterGroup.Projects {
if !existingProjectMap[proj] {
mergedProjects = append(mergedProjects, proj)
}
}

defaults.ClusterGroup.Projects = mergedProjects

// Preserve other fields from existing
if existing.ClusterGroup.IsHubCluster {
defaults.ClusterGroup.IsHubCluster = existing.ClusterGroup.IsHubCluster
}

// Merge subscriptions
for key, sub := range existing.ClusterGroup.Subscriptions {
defaults.ClusterGroup.Subscriptions[key] = sub
}

// Merge other fields
if existing.ClusterGroup.OtherFields != nil {
for key, value := range existing.ClusterGroup.OtherFields {
defaults.ClusterGroup.OtherFields[key] = value
}
}
if existing.OtherFields != nil {
for key, value := range existing.OtherFields {
defaults.OtherFields[key] = value
}
}
}
76 changes: 71 additions & 5 deletions src/internal/types/clustergroup.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
package types

import "path/filepath"
import (
"fmt"
"path/filepath"

"gopkg.in/yaml.v3"
)

// NamespaceEntry represents a namespace that can be either a string or a map with additional configuration
type NamespaceEntry struct {
value interface{}
}

// MarshalYAML implements the yaml.Marshaler interface for NamespaceEntry
func (ne NamespaceEntry) MarshalYAML() (interface{}, error) {
return ne.value, nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for NamespaceEntry
func (ne *NamespaceEntry) UnmarshalYAML(value *yaml.Node) error {
ne.value = nil

var str string
if err := value.Decode(&str); err == nil {
ne.value = str
return nil
}

var m map[string]interface{}
if err := value.Decode(&m); err == nil {
ne.value = m
return nil
}

return fmt.Errorf("namespaces entry at line %d, column %d must be either a string or a map", value.Line, value.Column)
}

// GetString returns the string value if this NamespaceEntry is a string, and a boolean indicating success
func (ne NamespaceEntry) GetString() (string, bool) {
if str, ok := ne.value.(string); ok {
return str, true
}
return "", false
}

// Equal compares two NamespaceEntry values for equality
func (ne NamespaceEntry) Equal(other NamespaceEntry) bool {
// Simple comparison - could be enhanced for deep map comparison if needed
if str1, ok1 := ne.GetString(); ok1 {
if str2, ok2 := other.GetString(); ok2 {
return str1 == str2
}
}
// For maps or other complex types, we'd need deeper comparison
// For now, assume they're different if not both strings
return false
}

// NewNamespaceEntry creates a new NamespaceEntry from a string
func NewNamespaceEntry(namespace string) NamespaceEntry {
return NamespaceEntry{value: namespace}
}

// Application defines the structure for an ArgoCD application entry.
type Application struct {
Expand All @@ -26,7 +86,7 @@ type Subscription struct {
type ClusterGroup struct {
Name string `yaml:"name"`
IsHubCluster bool `yaml:"isHubCluster,omitempty"`
Namespaces []string `yaml:"namespaces"`
Namespaces []NamespaceEntry `yaml:"namespaces"`
Projects []string `yaml:"projects"`
Subscriptions map[string]Subscription `yaml:"subscriptions"`
Applications map[string]Application `yaml:"applications"`
Expand All @@ -42,12 +102,16 @@ type ValuesClusterGroup struct {
// NewDefaultValuesClusterGroup creates a default configuration for a cluster group.
// It conditionally includes secrets-related resources based on the useSecrets flag.
func NewDefaultValuesClusterGroup(patternName, clusterGroupName string, chartPaths []string, useSecrets bool) *ValuesClusterGroup {
namespaces := []string{patternName}
projects := []string{clusterGroupName, patternName}
namespaces := []NamespaceEntry{NewNamespaceEntry(patternName)}
projects := []string{patternName}
applications := make(map[string]Application)

if useSecrets {
namespaces = append(namespaces, "vault", "golang-external-secrets")
projects = append(projects, clusterGroupName)
}

if useSecrets {
namespaces = append(namespaces, NewNamespaceEntry("vault"), NewNamespaceEntry("golang-external-secrets"))
applications["vault"] = Application{
Name: "vault",
Namespace: "vault",
Expand Down Expand Up @@ -82,6 +146,8 @@ func NewDefaultValuesClusterGroup(patternName, clusterGroupName string, chartPat
Projects: projects,
Subscriptions: make(map[string]Subscription),
Applications: applications,
OtherFields: make(map[string]interface{}),
},
OtherFields: make(map[string]interface{}),
}
}
2 changes: 1 addition & 1 deletion src/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestNewDefaultValuesClusterGroup(t *testing.T) {
t.Errorf("Expected %d namespaces, got %d", len(expectedNamespaces), len(values.ClusterGroup.Namespaces))
}

expectedProjects := []string{"test-group", "test-pattern"}
expectedProjects := []string{"test-pattern"}
if len(values.ClusterGroup.Projects) != len(expectedProjects) {
t.Errorf("Expected %d projects, got %d", len(expectedProjects), len(values.ClusterGroup.Projects))
}
Expand Down
1 change: 0 additions & 1 deletion test/expected_values_prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ clusterGroup:
namespaces:
- trivial-pattern
projects:
- prod
- trivial-pattern
subscriptions: {}
applications:
Expand Down
2 changes: 1 addition & 1 deletion test/expected_values_prod_with_secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ clusterGroup:
- vault
- golang-external-secrets
projects:
- prod
- trivial-pattern
- prod
subscriptions: {}
applications:
golang-external-secrets:
Expand Down
16 changes: 15 additions & 1 deletion test/expected_values_renamed_cluster_group.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ clusterGroup:
name: renamed-cluster-group
namespaces:
- renamed-pattern
- vault
- golang-external-secrets
projects:
- renamed-cluster-group
- renamed-pattern
- renamed-cluster-group
subscriptions: {}
applications:
golang-external-secrets:
name: golang-external-secrets
namespace: golang-external-secrets
project: renamed-cluster-group
chart: golang-external-secrets
chartVersion: 0.1.*
simple:
name: simple
namespace: renamed-pattern
Expand All @@ -17,3 +25,9 @@ clusterGroup:
namespace: renamed-pattern
project: renamed-pattern
path: charts/trivial
vault:
name: vault
namespace: vault
project: renamed-cluster-group
chart: hashicorp-vault
chartVersion: 0.1.*
66 changes: 58 additions & 8 deletions test/integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ check_file_exists "values-secret.yaml.template" "values-secret.yaml.template fil
echo -e "${GREEN}=== Test 2: Initialization with secrets PASSED ===${NC}"

#
# Test 3: Custom pattern and cluster group names (merging test)
# Test 3: Custom pattern and cluster group names (merging test with secrets)
#
echo -e "${YELLOW}=== Test 3: Custom pattern and cluster group names ===${NC}"
echo -e "${YELLOW}=== Test 3: Custom pattern and cluster group names (with secrets) ===${NC}"

cd "$REPO_ROOT" # Go back to repo root
echo -e "${YELLOW}Cloning test repository for custom names test...${NC}"
Expand All @@ -211,8 +211,8 @@ cd "$TEST_DIR_CUSTOM"
echo -e "${YELLOW}Setting up initial values-global.yaml with custom names...${NC}"
cp "$INITIAL_VALUES_GLOBAL_CUSTOM" "values-global.yaml"

echo -e "${YELLOW}Running patternizer init (should preserve custom names)...${NC}"
PATTERNIZER_RESOURCES_DIR="$REPO_ROOT" "$PATTERNIZER_BINARY" init
echo -e "${YELLOW}Running patternizer init --with-secrets (should preserve custom names)...${NC}"
PATTERNIZER_RESOURCES_DIR="$REPO_ROOT" "$PATTERNIZER_BINARY" init --with-secrets

echo -e "${YELLOW}Running verification tests for custom names...${NC}"

Expand All @@ -222,8 +222,8 @@ compare_yaml "$EXPECTED_VALUES_GLOBAL_CUSTOM" "values-global.yaml" "values-globa
# Test 3.2: Check custom cluster group file is created with correct content
compare_yaml "$EXPECTED_VALUES_RENAMED_CLUSTER_GROUP" "values-renamed-cluster-group.yaml" "values-renamed-cluster-group.yaml content"

# Test 3.3: Check pattern.sh exists and has USE_SECRETS=false
check_file_content "pattern.sh" 'USE_SECRETS:=false' "pattern.sh contains USE_SECRETS=false (custom names)"
# Test 3.3: Check pattern.sh exists and has USE_SECRETS=true
check_file_content "pattern.sh" 'USE_SECRETS:=true' "pattern.sh contains USE_SECRETS=true (custom names)"

# Test 3.4: Verify pattern.sh is executable
if [ -x "pattern.sh" ]; then
Expand All @@ -233,10 +233,60 @@ else
exit 1
fi

echo -e "${GREEN}=== Test 3: Custom pattern and cluster group names PASSED ===${NC}"
# Test 3.5: Check values-secret.yaml.template exists
check_file_exists "values-secret.yaml.template" "values-secret.yaml.template file exists (custom names)"

echo -e "${GREEN}=== Test 3: Custom pattern and cluster group names (with secrets) PASSED ===${NC}"

#
# Test 4: Sequential execution (init followed by init --with-secrets)
#
echo -e "${YELLOW}=== Test 4: Sequential execution (init + init --with-secrets) ===${NC}"

cd "$REPO_ROOT" # Go back to repo root
TEST_DIR_SEQUENTIAL="/tmp/patternizer-integration-test-sequential"

# Clean up any previous sequential test runs
if [ -d "$TEST_DIR_SEQUENTIAL" ]; then
rm -rf "$TEST_DIR_SEQUENTIAL"
fi

echo -e "${YELLOW}Cloning test repository for sequential test...${NC}"
git clone "$TEST_REPO_URL" "$TEST_DIR_SEQUENTIAL"
cd "$TEST_DIR_SEQUENTIAL"

echo -e "${YELLOW}Running patternizer init (first)...${NC}"
PATTERNIZER_RESOURCES_DIR="$REPO_ROOT" "$PATTERNIZER_BINARY" init

echo -e "${YELLOW}Running patternizer init --with-secrets (second)...${NC}"
PATTERNIZER_RESOURCES_DIR="$REPO_ROOT" "$PATTERNIZER_BINARY" init --with-secrets

echo -e "${YELLOW}Running verification tests for sequential execution...${NC}"

# Test 4.1: Check values-global.yaml (should be same as basic case)
compare_yaml "$EXPECTED_VALUES_GLOBAL" "values-global.yaml" "values-global.yaml content (sequential)"

# Test 4.2: Check values-prod.yaml matches the --with-secrets output
compare_yaml "$EXPECTED_VALUES_PROD_WITH_SECRETS" "values-prod.yaml" "values-prod.yaml content (sequential, should match --with-secrets)"

# Test 4.3: Check pattern.sh exists and has USE_SECRETS=true
check_file_content "pattern.sh" 'USE_SECRETS:=true' "pattern.sh contains USE_SECRETS=true (sequential)"

# Test 4.4: Verify pattern.sh is executable
if [ -x "pattern.sh" ]; then
echo -e "${GREEN}PASS: pattern.sh is executable (sequential)${NC}"
else
echo -e "${RED}FAIL: pattern.sh is not executable (sequential)${NC}"
exit 1
fi

# Test 4.5: Check values-secret.yaml.template exists
check_file_exists "values-secret.yaml.template" "values-secret.yaml.template file exists (sequential)"

echo -e "${GREEN}=== Test 4: Sequential execution PASSED ===${NC}"

echo -e "${GREEN}All integration tests passed!${NC}"

# Clean up
cd "$REPO_ROOT"
rm -rf "$TEST_DIR" "$TEST_DIR_SECRETS" "$TEST_DIR_CUSTOM"
rm -rf "$TEST_DIR" "$TEST_DIR_SECRETS" "$TEST_DIR_CUSTOM" "$TEST_DIR_SEQUENTIAL"