From 79aef31032af20736ef250cb379cd7285b8b003f Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Fri, 27 Jun 2025 13:33:51 -0400 Subject: [PATCH] logic fixes * don't use cluster group project if it's not needed * add integration test for running init and then init --with-secrets * allow namespaces to be strings or maps --- src/internal/pattern/pattern.go | 79 ++++++++++++++++++- src/internal/types/clustergroup.go | 76 ++++++++++++++++-- src/main_test.go | 2 +- test/expected_values_prod.yaml | 1 - test/expected_values_prod_with_secrets.yaml | 2 +- ...expected_values_renamed_cluster_group.yaml | 16 +++- test/integration_test.sh | 66 ++++++++++++++-- 7 files changed, 223 insertions(+), 19 deletions(-) diff --git a/src/internal/pattern/pattern.go b/src/internal/pattern/pattern.go index da2156b..0ff2c13 100644 --- a/src/internal/pattern/pattern.go +++ b/src/internal/pattern/pattern.go @@ -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 @@ -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 + } + } +} diff --git a/src/internal/types/clustergroup.go b/src/internal/types/clustergroup.go index d3a10a9..19681d0 100644 --- a/src/internal/types/clustergroup.go +++ b/src/internal/types/clustergroup.go @@ -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 { @@ -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"` @@ -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", @@ -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{}), } } diff --git a/src/main_test.go b/src/main_test.go index a73c430..77b85ce 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -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)) } diff --git a/test/expected_values_prod.yaml b/test/expected_values_prod.yaml index 04e31ac..a53f5f3 100644 --- a/test/expected_values_prod.yaml +++ b/test/expected_values_prod.yaml @@ -3,7 +3,6 @@ clusterGroup: namespaces: - trivial-pattern projects: - - prod - trivial-pattern subscriptions: {} applications: diff --git a/test/expected_values_prod_with_secrets.yaml b/test/expected_values_prod_with_secrets.yaml index 1deeaef..ecbe17c 100644 --- a/test/expected_values_prod_with_secrets.yaml +++ b/test/expected_values_prod_with_secrets.yaml @@ -5,8 +5,8 @@ clusterGroup: - vault - golang-external-secrets projects: - - prod - trivial-pattern + - prod subscriptions: {} applications: golang-external-secrets: diff --git a/test/expected_values_renamed_cluster_group.yaml b/test/expected_values_renamed_cluster_group.yaml index a179008..b9376a0 100644 --- a/test/expected_values_renamed_cluster_group.yaml +++ b/test/expected_values_renamed_cluster_group.yaml @@ -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 @@ -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.* diff --git a/test/integration_test.sh b/test/integration_test.sh index 041bd91..c1820a3 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -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}" @@ -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}" @@ -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 @@ -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"