From 4e48203eaae4fb362be5dc4cb9486b454e5c83a9 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:02:34 -0500 Subject: [PATCH 1/6] fix(blueprint): Re-implement patch generation Adds correct patch generation back to the system. This functionality was lost while migrating to the new architecture. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- api/v1alpha1/blueprint_types.go | 30 +- pkg/composer/blueprint/blueprint_handler.go | 314 +++++++++++++++++++- pkg/composer/blueprint/feature_evaluator.go | 18 +- 3 files changed, 340 insertions(+), 22 deletions(-) diff --git a/api/v1alpha1/blueprint_types.go b/api/v1alpha1/blueprint_types.go index b1f41b437..4a059fa75 100644 --- a/api/v1alpha1/blueprint_types.go +++ b/api/v1alpha1/blueprint_types.go @@ -528,20 +528,22 @@ func (k *Kustomization) ToFluxKustomization(namespace string, defaultSourceName patches := make([]kustomize.Patch, 0, len(k.Patches)) for _, p := range k.Patches { - if p.Patch != "" { - var target *kustomize.Selector - if p.Target != nil { - target = &kustomize.Selector{ - Kind: p.Target.Kind, - Name: p.Target.Name, - Namespace: p.Target.Namespace, - } + patchContent := p.Patch + if patchContent == "" && p.Path != "" { + continue + } + var target *kustomize.Selector + if p.Target != nil { + target = &kustomize.Selector{ + Kind: p.Target.Kind, + Name: p.Target.Name, + Namespace: p.Target.Namespace, } - patches = append(patches, kustomize.Patch{ - Patch: p.Patch, - Target: target, - }) } + patches = append(patches, kustomize.Patch{ + Patch: patchContent, + Target: target, + }) } var postBuild *kustomizev1.PostBuild @@ -625,6 +627,7 @@ func (b *Blueprint) strategicMergeTerraformComponent(component TerraformComponen // strategicMergeKustomization performs a strategic merge of the provided Kustomization into the Blueprint. // It merges unique components and dependencies, updates fields if provided, and maintains dependency order. +// Patches from the provided kustomization are appended to existing patches. // Returns an error if a dependency cycle is detected during sorting. func (b *Blueprint) strategicMergeKustomization(kustomization Kustomization) error { for i, existing := range b.Kustomizations { @@ -649,6 +652,9 @@ func (b *Blueprint) strategicMergeKustomization(kustomization Kustomization) err if kustomization.Destroy != nil { existing.Destroy = kustomization.Destroy } + if len(kustomization.Patches) > 0 { + existing.Patches = append(existing.Patches, kustomization.Patches...) + } b.Kustomizations[i] = existing return b.sortKustomize() } diff --git a/pkg/composer/blueprint/blueprint_handler.go b/pkg/composer/blueprint/blueprint_handler.go index 0342455cf..f98c1892f 100644 --- a/pkg/composer/blueprint/blueprint_handler.go +++ b/pkg/composer/blueprint/blueprint_handler.go @@ -165,6 +165,7 @@ func (b *BaseBlueprintHandler) LoadBlueprint() error { // the file is only written if it does not already exist. The method ensures the target directory exists, // marshals the blueprint to YAML, and writes the file using the configured shims. // Terraform inputs and kustomization substitutions are manually cleared to prevent them from appearing in the final blueprint.yaml. +// Also writes patches from Features and local templates to the context patches directory. func (b *BaseBlueprintHandler) Write(overwrite ...bool) error { shouldOverwrite := false if len(overwrite) > 0 { @@ -178,12 +179,6 @@ func (b *BaseBlueprintHandler) Write(overwrite ...bool) error { yamlPath := filepath.Join(configRoot, "blueprint.yaml") - if !shouldOverwrite { - if _, err := b.shims.Stat(yamlPath); err == nil { - return nil - } - } - if err := b.shims.MkdirAll(filepath.Dir(yamlPath), 0755); err != nil { return fmt.Errorf("error creating directory: %w", err) } @@ -192,10 +187,30 @@ func (b *BaseBlueprintHandler) Write(overwrite ...bool) error { return fmt.Errorf("error setting repository defaults: %w", err) } + for _, kustomization := range b.blueprint.Kustomizations { + strategicMergePatchesToWrite, _ := b.categorizePatches(kustomization) + if len(strategicMergePatchesToWrite) > 0 { + kustomizationWithPatches := kustomization + kustomizationWithPatches.Patches = strategicMergePatchesToWrite + if err := b.writeLocalTemplatePatches(kustomizationWithPatches, shouldOverwrite); err != nil { + return fmt.Errorf("error writing patches to context: %w", err) + } + } + } + + if !shouldOverwrite { + if _, err := b.shims.Stat(yamlPath); err == nil { + return nil + } + } + cleanedBlueprint := b.blueprint.DeepCopy() for i := range cleanedBlueprint.TerraformComponents { cleanedBlueprint.TerraformComponents[i].Inputs = map[string]any{} } + for i := range cleanedBlueprint.Kustomizations { + cleanedBlueprint.Kustomizations[i].Patches = nil + } data, err := b.shims.YamlMarshal(cleanedBlueprint) if err != nil { @@ -267,6 +282,107 @@ func (b *BaseBlueprintHandler) Generate() *blueprintv1alpha1.Blueprint { if subs, exists := b.featureSubstitutions[generated.Kustomizations[i].Name]; exists { generated.Kustomizations[i].Substitutions = maps.Clone(subs) } + + strategicMergePatchesToWrite, inlinePatches := b.categorizePatches(generated.Kustomizations[i]) + + if len(strategicMergePatchesToWrite) > 0 { + kustomizationWithPatches := generated.Kustomizations[i] + kustomizationWithPatches.Patches = strategicMergePatchesToWrite + b.writeLocalTemplatePatches(kustomizationWithPatches, true) + generated.Kustomizations[i].Patches = inlinePatches + } else { + generated.Kustomizations[i].Patches = inlinePatches + } + + configRoot := b.runtime.ConfigRoot + if configRoot != "" { + patchesDir := filepath.Join(configRoot, "patches", generated.Kustomizations[i].Name) + if _, err := b.shims.Stat(patchesDir); err == nil { + var discoveredPatches []blueprintv1alpha1.BlueprintPatch + err := b.shims.Walk(patchesDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(strings.ToLower(info.Name()), ".yaml") && !strings.HasSuffix(strings.ToLower(info.Name()), ".yml") { + return nil + } + data, err := b.shims.ReadFile(path) + if err != nil { + return nil + } + patchContent := string(data) + relPath, err := filepath.Rel(patchesDir, path) + if err != nil { + return nil + } + defaultNamespace := b.runtime.ConfigHandler.GetContext() + isJSON6902, target := func() (bool, *kustomize.Selector) { + decoder := yaml.NewDecoder(strings.NewReader(patchContent)) + for { + var doc map[string]any + if err := decoder.Decode(&doc); err != nil { + if err == io.EOF { + break + } + continue + } + if doc == nil { + continue + } + hasAPIVersion := false + hasKind := false + hasMetadata := false + if _, ok := doc["apiVersion"]; ok { + hasAPIVersion = true + } + if _, ok := doc["kind"]; ok { + hasKind = true + } + if _, ok := doc["metadata"]; ok { + hasMetadata = true + } + if hasAPIVersion && hasKind && hasMetadata { + for key, v := range doc { + if key == "patch" || key == "patches" { + if arr, ok := v.([]any); ok { + if len(arr) > 0 { + if firstItem, ok := arr[0].(map[string]any); ok { + if _, ok := firstItem["op"].(string); ok { + if _, hasPath := firstItem["path"]; hasPath { + return true, b.extractTargetFromPatchData(doc, defaultNamespace) + } + } + } + } + } + } + } + return false, nil + } + } + return false, nil + }() + patch := blueprintv1alpha1.BlueprintPatch{ + Path: relPath, + Patch: patchContent, + } + if isJSON6902 && target != nil { + patch.Target = target + } + discoveredPatches = append(discoveredPatches, patch) + return nil + }) + if err == nil && len(discoveredPatches) > 0 { + for j := range discoveredPatches { + discoveredPatches[j].Path = "" + } + generated.Kustomizations[i].Patches = append(generated.Kustomizations[i].Patches, discoveredPatches...) + } + } + } } mergedCommonValues := make(map[string]string) @@ -698,6 +814,16 @@ func (b *BaseBlueprintHandler) processFeatures(templateData map[string][]byte, c maps.Copy(b.featureSubstitutions[kustomizationCopy.Name], evaluatedSubstitutions) } + for j := range kustomizationCopy.Patches { + if kustomizationCopy.Patches[j].Patch != "" { + evaluated, err := b.featureEvaluator.InterpolateString(kustomizationCopy.Patches[j].Patch, config, feature.Path) + if err != nil { + return fmt.Errorf("failed to evaluate patch for kustomization '%s': %w", kustomizationCopy.Name, err) + } + kustomizationCopy.Patches[j].Patch = evaluated + } + } + // Clear substitutions as they are used for ConfigMap generation and should not appear in the final blueprint kustomizationCopy.Substitutions = nil @@ -1056,6 +1182,182 @@ func (b *BaseBlueprintHandler) isOCISource(sourceNameOrURL string) bool { return false } +// categorizePatches categorizes patches into strategic merge patches to write and inline patches to keep in-memory. +// Returns strategic merge patches to write and inline patches (JSON 6902 or OCI patches). +func (b *BaseBlueprintHandler) categorizePatches(kustomization blueprintv1alpha1.Kustomization) ([]blueprintv1alpha1.BlueprintPatch, []blueprintv1alpha1.BlueprintPatch) { + strategicMergePatchesToWrite := make([]blueprintv1alpha1.BlueprintPatch, 0) + inlinePatches := make([]blueprintv1alpha1.BlueprintPatch, 0) + + for _, patch := range kustomization.Patches { + isLocalTemplatePatch := false + if patch.Path != "" && !strings.HasPrefix(patch.Path, "patches/") { + patchFilePath := filepath.Join(b.runtime.TemplateRoot, patch.Path) + if _, err := b.shims.Stat(patchFilePath); err == nil { + isLocalTemplatePatch = true + } + } + + if patch.Target != nil { + if isLocalTemplatePatch && patch.Patch == "" && patch.Path != "" { + patchFilePath := filepath.Join(b.runtime.TemplateRoot, patch.Path) + data, err := b.shims.ReadFile(patchFilePath) + if err == nil { + patch.Patch = string(data) + patch.Path = "" + } + } + inlinePatches = append(inlinePatches, patch) + } else if patch.Patch != "" { + strategicMergePatchesToWrite = append(strategicMergePatchesToWrite, patch) + } else if isLocalTemplatePatch { + strategicMergePatchesToWrite = append(strategicMergePatchesToWrite, patch) + } else { + inlinePatches = append(inlinePatches, patch) + } + } + + return strategicMergePatchesToWrite, inlinePatches +} + +// writeLocalTemplatePatches writes patches from local _template to the context patches directory. +// Patches are written to contexts//patches// as individual YAML files. +// Each patch file is named using kind-name.yaml format extracted from the patch metadata. +// Patch content is evaluated (jsonnet expressions are processed) before writing. +// If overwrite is false, existing patch files are not overwritten. +// Returns an error if directory creation or file writing fails. +func (b *BaseBlueprintHandler) writeLocalTemplatePatches(kustomization blueprintv1alpha1.Kustomization, overwrite bool) error { + if len(kustomization.Patches) == 0 { + return nil + } + + configRoot := b.runtime.ConfigRoot + if configRoot == "" { + return nil + } + + patchesDir := filepath.Join(configRoot, "patches", kustomization.Name) + if err := b.shims.MkdirAll(patchesDir, 0755); err != nil { + return fmt.Errorf("failed to create patches directory: %w", err) + } + + config := make(map[string]any) + contextValues, err := b.runtime.ConfigHandler.GetContextValues() + if err == nil { + for k, v := range contextValues { + if k != "substitutions" { + config[k] = v + } + } + } + + patchMap := make(map[string]string) + for i, patch := range kustomization.Patches { + if patch.Path == "" && patch.Patch == "" { + continue + } + + var patchContent string + + if patch.Patch != "" { + patchContent = patch.Patch + } else if patch.Path != "" { + patchFileFullPath := filepath.Join(b.runtime.TemplateRoot, patch.Path) + data, err := b.shims.ReadFile(patchFileFullPath) + if err != nil { + continue + } + patchContent = string(data) + evaluated, err := b.featureEvaluator.InterpolateString(patchContent, config, filepath.Dir(patchFileFullPath)) + if err != nil { + return fmt.Errorf("failed to evaluate patch file %s: %w", patch.Path, err) + } + patchContent = evaluated + } else { + continue + } + + if strings.TrimSpace(patchContent) == "" { + continue + } + + var patchFileName string + var doc map[string]any + if err := b.shims.YamlUnmarshal([]byte(patchContent), &doc); err == nil { + if kind, ok := doc["kind"].(string); ok { + if metadata, ok := doc["metadata"].(map[string]any); ok { + if name, ok := metadata["name"].(string); ok { + filename := fmt.Sprintf("%s-%s.yaml", strings.ToLower(kind), name) + invalidChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} + for _, char := range invalidChars { + filename = strings.ReplaceAll(filename, char, "-") + } + patchFileName = filename + } + } + } + } + if patchFileName == "" { + patchFileName = fmt.Sprintf("%d.yaml", i) + } + existingContent, exists := patchMap[patchFileName] + if exists { + var existingDoc, newDoc map[string]any + if err := b.shims.YamlUnmarshal([]byte(existingContent), &existingDoc); err != nil { + return fmt.Errorf("failed to parse existing patch: %w", err) + } + if err := b.shims.YamlUnmarshal([]byte(patchContent), &newDoc); err != nil { + return fmt.Errorf("failed to parse new patch: %w", err) + } + existingKind, existingKindOk := existingDoc["kind"].(string) + var existingName string + var existingNameOk bool + if metadata, ok := existingDoc["metadata"].(map[string]any); ok { + if name, ok := metadata["name"].(string); ok { + existingName = name + existingNameOk = true + } + } + newKind, newKindOk := newDoc["kind"].(string) + var newName string + var newNameOk bool + if metadata, ok := newDoc["metadata"].(map[string]any); ok { + if name, ok := metadata["name"].(string); ok { + newName = name + newNameOk = true + } + } + if existingKindOk && existingNameOk && newKindOk && newNameOk && + existingKind == newKind && existingName == newName { + merged := b.deepMergeMaps(existingDoc, newDoc) + mergedYAML, err := b.shims.YamlMarshal(merged) + if err != nil { + return fmt.Errorf("failed to marshal merged patch: %w", err) + } + patchMap[patchFileName] = strings.TrimSpace(string(mergedYAML)) + } else { + patchMap[patchFileName] = strings.TrimSpace(existingContent) + "\n---\n" + strings.TrimSpace(patchContent) + } + } else { + patchMap[patchFileName] = strings.TrimSpace(patchContent) + } + } + + for patchFileName, patchContent := range patchMap { + patchFilePath := filepath.Join(patchesDir, patchFileName) + if !overwrite { + if _, err := b.shims.Stat(patchFilePath); err == nil { + continue + } + } + + if err := b.shims.WriteFile(patchFilePath, []byte(patchContent), 0644); err != nil { + return fmt.Errorf("failed to write patch file %s: %w", patchFilePath, err) + } + } + + return nil +} + // mergeLegacySpecialVariables merges legacy special variables into the common values map for backward compatibility. // These variables were previously extracted from config and kustomize/values.yaml and are now merged from the config handler. // This method can be removed when legacy variable support is no longer needed. diff --git a/pkg/composer/blueprint/feature_evaluator.go b/pkg/composer/blueprint/feature_evaluator.go index cdf4cf903..b6db79255 100644 --- a/pkg/composer/blueprint/feature_evaluator.go +++ b/pkg/composer/blueprint/feature_evaluator.go @@ -331,7 +331,7 @@ func (e *FeatureEvaluator) evaluateDefaultValue(value any, config map[string]any return e.EvaluateValue(expr, config, featurePath) } if strings.Contains(v, "${") { - return e.interpolateString(v, config, featurePath) + return e.InterpolateString(v, config, featurePath) } return v, nil @@ -385,8 +385,9 @@ func (e *FeatureEvaluator) extractExpression(s string) string { return "" } -// interpolateString replaces all ${expression} occurrences in a string with their evaluated values. -func (e *FeatureEvaluator) interpolateString(s string, config map[string]any, featurePath string) (string, error) { +// InterpolateString replaces all ${expression} occurrences in a string with their evaluated values. +// This is used to process template expressions in patch content and other string values. +func (e *FeatureEvaluator) InterpolateString(s string, config map[string]any, featurePath string) (string, error) { result := s for strings.Contains(result, "${") { @@ -409,7 +410,16 @@ func (e *FeatureEvaluator) interpolateString(s string, config map[string]any, fe if value == nil { replacement = "" } else { - replacement = fmt.Sprintf("%v", value) + switch value.(type) { + case map[string]any, []any: + yamlData, err := e.shims.YamlMarshal(value) + if err != nil { + return "", fmt.Errorf("failed to marshal expression result to YAML: %w", err) + } + replacement = strings.TrimSpace(string(yamlData)) + default: + replacement = fmt.Sprintf("%v", value) + } } result = result[:start] + replacement + result[end+1:] From 03bccf1a7de6b24315f0121fc81fdb2f597874cf Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:35:48 -0500 Subject: [PATCH 2/6] Skip empty patches Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- api/v1alpha1/blueprint_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/blueprint_types.go b/api/v1alpha1/blueprint_types.go index 4a059fa75..c99671ac4 100644 --- a/api/v1alpha1/blueprint_types.go +++ b/api/v1alpha1/blueprint_types.go @@ -529,7 +529,7 @@ func (k *Kustomization) ToFluxKustomization(namespace string, defaultSourceName patches := make([]kustomize.Patch, 0, len(k.Patches)) for _, p := range k.Patches { patchContent := p.Patch - if patchContent == "" && p.Path != "" { + if patchContent == "" && p.Path == "" { continue } var target *kustomize.Selector From 00c8ebe8303b5613f62fa81edebf083d2bccae0f Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:39:34 -0500 Subject: [PATCH 3/6] Fix gosec and enhance coverage Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/composer/blueprint/blueprint_handler.go | 11 - .../blueprint_handler_private_test.go | 850 +++++++++++++++++- .../blueprint_handler_public_test.go | 543 +++++++++++ 3 files changed, 1385 insertions(+), 19 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler.go b/pkg/composer/blueprint/blueprint_handler.go index f98c1892f..070709155 100644 --- a/pkg/composer/blueprint/blueprint_handler.go +++ b/pkg/composer/blueprint/blueprint_handler.go @@ -283,17 +283,6 @@ func (b *BaseBlueprintHandler) Generate() *blueprintv1alpha1.Blueprint { generated.Kustomizations[i].Substitutions = maps.Clone(subs) } - strategicMergePatchesToWrite, inlinePatches := b.categorizePatches(generated.Kustomizations[i]) - - if len(strategicMergePatchesToWrite) > 0 { - kustomizationWithPatches := generated.Kustomizations[i] - kustomizationWithPatches.Patches = strategicMergePatchesToWrite - b.writeLocalTemplatePatches(kustomizationWithPatches, true) - generated.Kustomizations[i].Patches = inlinePatches - } else { - generated.Kustomizations[i].Patches = inlinePatches - } - configRoot := b.runtime.ConfigRoot if configRoot != "" { patchesDir := filepath.Join(configRoot, "patches", generated.Kustomizations[i].Name) diff --git a/pkg/composer/blueprint/blueprint_handler_private_test.go b/pkg/composer/blueprint/blueprint_handler_private_test.go index f6048285e..3a539b54d 100644 --- a/pkg/composer/blueprint/blueprint_handler_private_test.go +++ b/pkg/composer/blueprint/blueprint_handler_private_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/composer/artifact" @@ -3103,8 +3104,8 @@ terraform: `) templateData := map[string][]byte{ - "blueprint": baseBlueprint, - "features/replace.yaml": replaceFeature, + "blueprint": baseBlueprint, + "features/replace.yaml": replaceFeature, } config := map[string]any{} @@ -3170,8 +3171,8 @@ terraform: `) templateData := map[string][]byte{ - "blueprint": baseBlueprint, - "features/merge.yaml": mergeFeature, + "blueprint": baseBlueprint, + "features/merge.yaml": mergeFeature, } config := map[string]any{} @@ -3235,8 +3236,8 @@ kustomize: `) templateData := map[string][]byte{ - "blueprint": baseBlueprint, - "features/replace.yaml": replaceFeature, + "blueprint": baseBlueprint, + "features/replace.yaml": replaceFeature, } config := map[string]any{} @@ -3304,8 +3305,8 @@ kustomize: `) templateData := map[string][]byte{ - "blueprint": baseBlueprint, - "features/merge.yaml": mergeFeature, + "blueprint": baseBlueprint, + "features/merge.yaml": mergeFeature, } config := map[string]any{} @@ -4288,3 +4289,836 @@ func TestNewShims(t *testing.T) { } }) } + +func TestBaseBlueprintHandler_writeLocalTemplatePatches(t *testing.T) { + setup := func(t *testing.T) (*BaseBlueprintHandler, *BlueprintTestMocks) { + t.Helper() + mocks := setupBlueprintMocks(t) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } + handler.shims = mocks.Shims + return handler, mocks + } + + t.Run("WritesPatchWithKindAndName", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + mocks.Runtime.TemplateRoot = "/test/template" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key: value`, + }, + }, + } + + var writtenPath string + var writtenContent []byte + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenPath = name + writtenContent = data + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + mocks.Shims.YamlMarshal = yaml.Marshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedPath := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization", "configmap-test-config.yaml") + if writtenPath != expectedPath { + t.Errorf("Expected patch file at %s, got %s", expectedPath, writtenPath) + } + + if len(writtenContent) == 0 { + t.Error("Expected patch content to be written") + } + }) + + t.Run("HandlesEmptyPatches", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{}, + } + + var writeFileCalled bool + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writeFileCalled = true + } + return nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if writeFileCalled { + t.Error("Expected WriteFile not to be called for empty patches") + } + }) + + t.Run("HandlesEmptyConfigRoot", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config`, + }, + }, + } + + var writeFileCalled bool + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writeFileCalled = true + } + return nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if writeFileCalled { + t.Error("Expected WriteFile not to be called when ConfigRoot is empty") + } + }) + + t.Run("HandlesMkdirAllError", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config`, + }, + }, + } + + expectedError := fmt.Errorf("mkdir error") + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + if strings.Contains(path, "patches/") { + return expectedError + } + return nil + } + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err == nil { + t.Error("Expected error from MkdirAll, got nil") + } + + if !strings.Contains(err.Error(), "failed to create patches directory") { + t.Errorf("Expected error about creating patches directory, got: %v", err) + } + }) + + t.Run("HandlesWriteFileError", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config`, + }, + }, + } + + expectedError := fmt.Errorf("write file error") + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + return expectedError + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err == nil { + t.Error("Expected error from WriteFile, got nil") + } + + if !strings.Contains(err.Error(), "failed to write patch file") { + t.Errorf("Expected error about writing patch file, got: %v", err) + } + }) + + t.Run("RespectsOverwriteFlag", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config`, + }, + }, + } + + var writeFileCalled bool + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writeFileCalled = true + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "patches/") { + return &mockFileInfo{name: "configmap-test-config.yaml", isDir: false}, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, false) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if writeFileCalled { + t.Error("Expected WriteFile not to be called when overwrite is false and file exists") + } + + writeFileCalled = false + err = handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !writeFileCalled { + t.Error("Expected WriteFile to be called when overwrite is true") + } + }) + + t.Run("EvaluatesPatchContentFromPath", func(t *testing.T) { + handler, mocks := setup(t) + tmpDir := t.TempDir() + mocks.Runtime.ConfigRoot = "/test/config" + mocks.Runtime.TemplateRoot = tmpDir + + patchFile := filepath.Join(tmpDir, "kustomize", "patches", "test-patch.yaml") + if err := os.MkdirAll(filepath.Dir(patchFile), 0755); err != nil { + t.Fatalf("Failed to create patch directory: %v", err) + } + + patchContent := `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config` + if err := os.WriteFile(patchFile, []byte(patchContent), 0644); err != nil { + t.Fatalf("Failed to write patch file: %v", err) + } + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Path: "kustomize/patches/test-patch.yaml", + }, + }, + } + + var writtenContent []byte + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenContent = data + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.ReadFile = os.ReadFile + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + mocks.Shims.YamlMarshal = yaml.Marshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(writtenContent) == 0 { + t.Error("Expected patch content to be written") + } + + if !strings.Contains(string(writtenContent), "test-config") { + t.Error("Expected written content to contain patch data") + } + }) + + t.Run("MergesPatchesWithSameKindAndName", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key1: value1`, + }, + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key2: value2`, + }, + }, + } + + var writtenContent []byte + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenContent = data + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + mocks.Shims.YamlMarshal = yaml.Marshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(writtenContent) == 0 { + t.Fatal("Expected merged patch content to be written") + } + + var mergedDoc map[string]any + if err := yaml.Unmarshal(writtenContent, &mergedDoc); err != nil { + t.Fatalf("Failed to unmarshal merged patch: %v", err) + } + + if data, ok := mergedDoc["data"].(map[string]any); ok { + if data["key1"] != "value1" { + t.Error("Expected key1 to be in merged patch") + } + if data["key2"] != "value2" { + t.Error("Expected key2 to be in merged patch") + } + } else { + t.Error("Expected data section in merged patch") + } + }) + + t.Run("AppendsPatchesWithDifferentKindOrName", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: config-1 +data: + key1: value1`, + }, + { + Patch: `apiVersion: v1 +kind: Deployment +metadata: + name: deployment-1 +spec: + replicas: 3`, + }, + }, + } + + var writtenFiles = make(map[string][]byte) + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenFiles[name] = data + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + mocks.Shims.YamlMarshal = yaml.Marshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(writtenFiles) == 0 { + t.Fatal("Expected patch files to be written") + } + + foundConfigMap := false + foundDeployment := false + for path, content := range writtenFiles { + contentStr := string(content) + if strings.Contains(path, "configmap-config-1.yaml") { + foundConfigMap = true + if !strings.Contains(contentStr, "config-1") { + t.Error("Expected config-1 patch content") + } + } + if strings.Contains(path, "deployment-deployment-1.yaml") { + foundDeployment = true + if !strings.Contains(contentStr, "deployment-1") { + t.Error("Expected deployment-1 patch content") + } + } + } + + if !foundConfigMap { + t.Error("Expected ConfigMap patch file to be written") + } + if !foundDeployment { + t.Error("Expected Deployment patch file to be written") + } + }) + + t.Run("SanitizesInvalidFilenameCharacters", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test/config:name*with?invalid"chars<| +data: + key: value`, + }, + }, + } + + var writtenPath string + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenPath = name + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if writtenPath == "" { + t.Fatal("Expected patch file to be written") + } + + fileName := filepath.Base(writtenPath) + invalidChars := []string{"/", "\\", ":", "*", "?", "\"", "<", "|"} + for _, char := range invalidChars { + if strings.Contains(fileName, char) { + t.Errorf("Expected invalid character '%s' to be sanitized from filename, got: %s", char, fileName) + } + } + + if !strings.Contains(fileName, "configmap-test") { + t.Errorf("Expected sanitized filename to contain 'configmap-test', got: %s", fileName) + } + + if !strings.HasSuffix(fileName, ".yaml") { + t.Errorf("Expected filename to end with .yaml, got: %s", fileName) + } + }) + + t.Run("SkipsPatchWhenReadFileFails", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + mocks.Runtime.TemplateRoot = "/test/template" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Path: "kustomize/patches/test-patch.yaml", + }, + }, + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.ReadFile = func(name string) ([]byte, error) { + if strings.Contains(name, "patches/test-patch.yaml") { + return nil, fmt.Errorf("read file error") + } + return nil, os.ErrNotExist + } + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + var writeFileCalled bool + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writeFileCalled = true + } + return nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error (patch skipped), got %v", err) + } + + if writeFileCalled { + t.Error("Expected WriteFile not to be called when patch file cannot be read") + } + }) + + t.Run("UsesIndexWhenNoMetadata", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +data: + key: value`, + }, + }, + } + + var writtenPath string + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenPath = name + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlUnmarshal = yaml.Unmarshal + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + err := handler.writeLocalTemplatePatches(kustomization, true) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !strings.Contains(writtenPath, "0.yaml") { + t.Errorf("Expected filename with index 0, got: %s", filepath.Base(writtenPath)) + } + }) +} + +func TestBaseBlueprintHandler_categorizePatches(t *testing.T) { + setup := func(t *testing.T) (*BaseBlueprintHandler, *BlueprintTestMocks) { + t.Helper() + mocks := setupBlueprintMocks(t) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } + handler.shims = mocks.Shims + return handler, mocks + } + + t.Run("CategorizesStrategicMergePatches", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.TemplateRoot = "/test/template" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config`, + }, + }, + } + + strategicMerge, inline := handler.categorizePatches(kustomization) + + if len(strategicMerge) != 1 { + t.Errorf("Expected 1 strategic merge patch, got %d", len(strategicMerge)) + } + + if len(inline) != 0 { + t.Errorf("Expected 0 inline patches, got %d", len(inline)) + } + }) + + t.Run("CategorizesInlinePatches", func(t *testing.T) { + handler, _ := setup(t) + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Target: &kustomize.Selector{ + Kind: "ConfigMap", + Name: "test-config", + }, + Patch: `[{"op": "replace", "path": "/data/key", "value": "newvalue"}]`, + }, + }, + } + + strategicMerge, inline := handler.categorizePatches(kustomization) + + if len(strategicMerge) != 0 { + t.Errorf("Expected 0 strategic merge patches, got %d", len(strategicMerge)) + } + + if len(inline) != 1 { + t.Errorf("Expected 1 inline patch, got %d", len(inline)) + } + }) + + t.Run("CategorizesJSON6902Patches", func(t *testing.T) { + handler, _ := setup(t) + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Target: &kustomize.Selector{ + Kind: "Deployment", + Name: "test-deployment", + Namespace: "default", + }, + Patch: `[{"op": "replace", "path": "/spec/replicas", "value": 5}]`, + }, + }, + } + + strategicMerge, inline := handler.categorizePatches(kustomization) + + if len(strategicMerge) != 0 { + t.Errorf("Expected 0 strategic merge patches, got %d", len(strategicMerge)) + } + + if len(inline) != 1 { + t.Errorf("Expected 1 inline patch, got %d", len(inline)) + } + + if inline[0].Target == nil { + t.Error("Expected target to be set for JSON6902 patch") + } + }) + + t.Run("CategorizesLocalTemplatePatches", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.TemplateRoot = "/test/template" + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Path: "kustomize/patches/test-patch.yaml", + }, + }, + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "template/kustomize/patches/test-patch.yaml") { + return &mockFileInfo{name: "test-patch.yaml", isDir: false}, nil + } + return nil, os.ErrNotExist + } + + strategicMerge, inline := handler.categorizePatches(kustomization) + + if len(strategicMerge) != 1 { + t.Errorf("Expected 1 strategic merge patch for local template, got %d", len(strategicMerge)) + } + + if len(inline) != 0 { + t.Errorf("Expected 0 inline patches, got %d", len(inline)) + } + }) + + t.Run("HandlesEmptyPatches", func(t *testing.T) { + handler, _ := setup(t) + + kustomization := blueprintv1alpha1.Kustomization{ + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{}, + } + + strategicMerge, inline := handler.categorizePatches(kustomization) + + if len(strategicMerge) != 0 { + t.Errorf("Expected 0 strategic merge patches, got %d", len(strategicMerge)) + } + + if len(inline) != 0 { + t.Errorf("Expected 0 inline patches, got %d", len(inline)) + } + }) +} diff --git a/pkg/composer/blueprint/blueprint_handler_public_test.go b/pkg/composer/blueprint/blueprint_handler_public_test.go index 1ee53bb9f..74952d4f2 100644 --- a/pkg/composer/blueprint/blueprint_handler_public_test.go +++ b/pkg/composer/blueprint/blueprint_handler_public_test.go @@ -10,6 +10,7 @@ import ( "time" helmv2 "github.com/fluxcd/helm-controller/api/v2" + "github.com/fluxcd/pkg/apis/kustomize" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" @@ -2055,6 +2056,151 @@ func TestBlueprintHandler_Write(t *testing.T) { t.Errorf("Expected error from WriteFile, got nil") } }) + + t.Run("WritesStrategicMergePatchesToDisk", func(t *testing.T) { + // Given a blueprint handler with kustomization containing strategic merge patches + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + mocks.Runtime.TemplateRoot = "/test/template" + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Kind: "Blueprint", + ApiVersion: "v1alpha1", + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key: value`, + }, + }, + }, + }, + } + + var writtenPatchPaths []string + var writtenPatchContents []string + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenPatchPaths = append(writtenPatchPaths, name) + writtenPatchContents = append(writtenPatchContents, string(data)) + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "blueprint.yaml") { + return nil, os.ErrNotExist + } + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlMarshal = func(v any) ([]byte, error) { + return []byte("kind: Blueprint\n"), nil + } + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + // When Write is called + err := handler.Write() + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And patch file should be written + if len(writtenPatchPaths) == 0 { + t.Error("Expected patch file to be written, but no patch files were written") + } + + expectedPatchPath := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization", "configmap-test-config.yaml") + found := false + for _, path := range writtenPatchPaths { + if path == expectedPatchPath { + found = true + break + } + } + if !found { + t.Errorf("Expected patch file to be written at %s, but got: %v", expectedPatchPath, writtenPatchPaths) + } + }) + + t.Run("SkipsWritingWhenNoStrategicMergePatches", func(t *testing.T) { + // Given a blueprint handler with kustomization containing only inline patches + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Kind: "Blueprint", + ApiVersion: "v1alpha1", + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Target: &kustomize.Selector{ + Kind: "ConfigMap", + Name: "test-config", + }, + Patch: `[{"op": "replace", "path": "/data/key", "value": "newvalue"}]`, + }, + }, + }, + }, + } + + var writtenPatchPaths []string + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writtenPatchPaths = append(writtenPatchPaths, name) + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + mocks.Shims.YamlMarshal = func(v any) ([]byte, error) { + return []byte("kind: Blueprint\n"), nil + } + + // When Write is called + err := handler.Write() + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And no patch files should be written + if len(writtenPatchPaths) != 0 { + t.Errorf("Expected no patch files to be written, but got: %v", writtenPatchPaths) + } + }) } func TestBlueprintHandler_LoadBlueprint(t *testing.T) { @@ -4626,4 +4772,401 @@ csi: t.Errorf("Expected storage_class 'fast-ssd' from context, got '%s'", csiKustomization.Substitutions["storage_class"]) } }) + + t.Run("DoesNotWritePatchesDuringGeneration", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" + mocks.Runtime.TemplateRoot = "/test/template" + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + Patches: []blueprintv1alpha1.BlueprintPatch{ + { + Patch: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + key: value`, + }, + }, + }, + }, + } + + var writeFileCalls []string + mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + if strings.Contains(name, "patches/") { + writeFileCalls = append(writeFileCalls, name) + } + return nil + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) { + return make(map[string]any), nil + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(writeFileCalls) > 0 { + t.Errorf("Expected no patch files to be written during Generate(), but got %d writes: %v", len(writeFileCalls), writeFileCalls) + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + if len(generated.Kustomizations[0].Patches) == 0 { + t.Error("Expected patches to be preserved in generated blueprint") + } + }) + + t.Run("DiscoversPatchesFromDisk", func(t *testing.T) { + handler, mocks := setup(t) + tmpDir := t.TempDir() + mocks.Runtime.ConfigRoot = filepath.Join(tmpDir, "contexts", "test-context") + patchesDir := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization") + if err := os.MkdirAll(patchesDir, 0755); err != nil { + t.Fatalf("Failed to create patches directory: %v", err) + } + + patchContent := `apiVersion: v1 +kind: ConfigMap +metadata: + name: discovered-config +data: + key: value` + patchFile := filepath.Join(patchesDir, "configmap-discovered-config.yaml") + if err := os.WriteFile(patchFile, []byte(patchContent), 0644); err != nil { + t.Fatalf("Failed to write patch file: %v", err) + } + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "patches/test-kustomization") { + return &mockFileInfo{name: "test-kustomization", isDir: true}, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.Walk = filepath.Walk + mocks.Shims.ReadFile = os.ReadFile + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + if len(generated.Kustomizations[0].Patches) == 0 { + t.Error("Expected discovered patches to be added to kustomization") + } + + found := false + for _, patch := range generated.Kustomizations[0].Patches { + if strings.Contains(patch.Patch, "discovered-config") { + found = true + if patch.Path != "" { + t.Error("Expected discovered patch Path to be cleared") + } + break + } + } + if !found { + t.Error("Expected to find discovered patch in generated blueprint") + } + }) + + t.Run("AppliesFeatureSubstitutions", func(t *testing.T) { + handler, _ := setup(t) + handler.featureSubstitutions = map[string]map[string]string{ + "test-kustomization": { + "domain": "example.com", + "region": "us-west-2", + }, + } + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + subs := generated.Kustomizations[0].Substitutions + if subs == nil { + t.Fatal("Expected substitutions to be set") + } + + if subs["domain"] != "example.com" { + t.Errorf("Expected domain 'example.com', got '%s'", subs["domain"]) + } + + if subs["region"] != "us-west-2" { + t.Errorf("Expected region 'us-west-2', got '%s'", subs["region"]) + } + }) + + t.Run("DiscoversJSON6902Patches", func(t *testing.T) { + handler, mocks := setup(t) + tmpDir := t.TempDir() + mocks.Runtime.ConfigRoot = filepath.Join(tmpDir, "contexts", "test-context") + patchesDir := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization") + if err := os.MkdirAll(patchesDir, 0755); err != nil { + t.Fatalf("Failed to create patches directory: %v", err) + } + + json6902PatchContent := `apiVersion: v1 +kind: Deployment +metadata: + name: test-deployment + namespace: default +patch: +- op: replace + path: /spec/replicas + value: 5` + patchFile := filepath.Join(patchesDir, "deployment-patch.yaml") + if err := os.WriteFile(patchFile, []byte(json6902PatchContent), 0644); err != nil { + t.Fatalf("Failed to write patch file: %v", err) + } + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "patches/test-kustomization") { + return &mockFileInfo{name: "test-kustomization", isDir: true}, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.Walk = filepath.Walk + mocks.Shims.ReadFile = os.ReadFile + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + found := false + for _, patch := range generated.Kustomizations[0].Patches { + if patch.Target != nil { + found = true + if patch.Target.Kind != "Deployment" { + t.Errorf("Expected target kind 'Deployment', got '%s'", patch.Target.Kind) + } + if patch.Target.Name != "test-deployment" { + t.Errorf("Expected target name 'test-deployment', got '%s'", patch.Target.Name) + } + break + } + } + if !found { + t.Error("Expected to find JSON6902 patch with target in generated blueprint") + } + }) + + t.Run("HandlesPatchDiscoveryErrors", func(t *testing.T) { + handler, mocks := setup(t) + tmpDir := t.TempDir() + mocks.Runtime.ConfigRoot = filepath.Join(tmpDir, "contexts", "test-context") + patchesDir := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization") + if err := os.MkdirAll(patchesDir, 0755); err != nil { + t.Fatalf("Failed to create patches directory: %v", err) + } + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + walkError := fmt.Errorf("walk error") + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "patches/test-kustomization") { + return &mockFileInfo{name: "patches", isDir: true}, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.Walk = func(root string, fn filepath.WalkFunc) error { + return walkError + } + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + if len(generated.Kustomizations[0].Patches) != 0 { + t.Error("Expected no patches when Walk returns error") + } + }) + + t.Run("SkipsNonYamlFilesInPatchDiscovery", func(t *testing.T) { + handler, mocks := setup(t) + tmpDir := t.TempDir() + mocks.Runtime.ConfigRoot = filepath.Join(tmpDir, "contexts", "test-context") + patchesDir := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization") + if err := os.MkdirAll(patchesDir, 0755); err != nil { + t.Fatalf("Failed to create patches directory: %v", err) + } + + patchContent := `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config` + if err := os.WriteFile(filepath.Join(patchesDir, "configmap-test-config.yaml"), []byte(patchContent), 0644); err != nil { + t.Fatalf("Failed to write patch file: %v", err) + } + + if err := os.WriteFile(filepath.Join(patchesDir, "not-a-patch.txt"), []byte("not yaml"), 0644); err != nil { + t.Fatalf("Failed to write non-patch file: %v", err) + } + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "patches/test-kustomization") { + return &mockFileInfo{name: "test-kustomization", isDir: true}, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.Walk = filepath.Walk + mocks.Shims.ReadFile = os.ReadFile + + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + if len(generated.Kustomizations[0].Patches) != 1 { + t.Errorf("Expected 1 discovered patch (yaml file only), got %d", len(generated.Kustomizations[0].Patches)) + } + }) + + t.Run("HandlesEmptyConfigRoot", func(t *testing.T) { + handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "" + + handler.blueprint = blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + generated := handler.Generate() + + if generated == nil { + t.Fatal("Expected non-nil generated blueprint") + } + + if len(generated.Kustomizations) != 1 { + t.Fatalf("Expected 1 kustomization, got %d", len(generated.Kustomizations)) + } + + if len(generated.Kustomizations[0].Patches) != 0 { + t.Error("Expected no patches when ConfigRoot is empty") + } + }) } From f00ed7f6eea0370577471a26505d436513ddea52 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:03:04 -0500 Subject: [PATCH 4/6] Normalize paths for windows Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/composer/blueprint/blueprint_handler_private_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler_private_test.go b/pkg/composer/blueprint/blueprint_handler_private_test.go index 3a539b54d..07ed51314 100644 --- a/pkg/composer/blueprint/blueprint_handler_private_test.go +++ b/pkg/composer/blueprint/blueprint_handler_private_test.go @@ -4325,7 +4325,8 @@ data: var writtenPath string var writtenContent []byte mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenPath = name writtenContent = data } @@ -4354,7 +4355,7 @@ data: } expectedPath := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization", "configmap-test-config.yaml") - if writtenPath != expectedPath { + if filepath.ToSlash(writtenPath) != filepath.ToSlash(expectedPath) { t.Errorf("Expected patch file at %s, got %s", expectedPath, writtenPath) } @@ -4457,7 +4458,7 @@ metadata: err := handler.writeLocalTemplatePatches(kustomization, true) if err == nil { - t.Error("Expected error from MkdirAll, got nil") + t.Fatal("Expected error from MkdirAll, got nil") } if !strings.Contains(err.Error(), "failed to create patches directory") { From 4c58232a71b380d5b29097793fdbe082755df86e Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:15:10 -0500 Subject: [PATCH 5/6] Normalize paths Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../blueprint_handler_private_test.go | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler_private_test.go b/pkg/composer/blueprint/blueprint_handler_private_test.go index 07ed51314..2fb46028c 100644 --- a/pkg/composer/blueprint/blueprint_handler_private_test.go +++ b/pkg/composer/blueprint/blueprint_handler_private_test.go @@ -4375,7 +4375,8 @@ data: var writeFileCalled bool mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writeFileCalled = true } return nil @@ -4410,7 +4411,8 @@ metadata: var writeFileCalled bool mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writeFileCalled = true } return nil @@ -4445,7 +4447,8 @@ metadata: expectedError := fmt.Errorf("mkdir error") mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { - if strings.Contains(path, "patches/") { + normalizedPath := filepath.ToSlash(path) + if strings.Contains(normalizedPath, "patches/") { return expectedError } return nil @@ -4484,7 +4487,8 @@ metadata: expectedError := fmt.Errorf("write file error") mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { return expectedError } return nil @@ -4507,7 +4511,7 @@ metadata: err := handler.writeLocalTemplatePatches(kustomization, true) if err == nil { - t.Error("Expected error from WriteFile, got nil") + t.Fatal("Expected error from WriteFile, got nil") } if !strings.Contains(err.Error(), "failed to write patch file") { @@ -4533,14 +4537,16 @@ metadata: var writeFileCalled bool mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writeFileCalled = true } return nil } mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { return &mockFileInfo{name: "configmap-test-config.yaml", isDir: false}, nil } return nil, os.ErrNotExist @@ -4608,7 +4614,8 @@ metadata: var writtenContent []byte mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenContent = data } return nil @@ -4673,7 +4680,8 @@ data: var writtenContent []byte mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenContent = data } return nil @@ -4749,7 +4757,8 @@ spec: var writtenFiles = make(map[string][]byte) mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenFiles[name] = data } return nil @@ -4826,7 +4835,8 @@ data: var writtenPath string mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenPath = name } return nil @@ -4896,7 +4906,8 @@ data: } mocks.Shims.ReadFile = func(name string) ([]byte, error) { - if strings.Contains(name, "patches/test-patch.yaml") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/test-patch.yaml") { return nil, fmt.Errorf("read file error") } return nil, os.ErrNotExist @@ -4908,7 +4919,8 @@ data: var writeFileCalled bool mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writeFileCalled = true } return nil @@ -4942,7 +4954,8 @@ data: var writtenPath string mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenPath = name } return nil @@ -5087,7 +5100,8 @@ metadata: } mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "template/kustomize/patches/test-patch.yaml") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "template/kustomize/patches/test-patch.yaml") { return &mockFileInfo{name: "test-patch.yaml", isDir: false}, nil } return nil, os.ErrNotExist From 4fbfa4adff32b962ff07f16a07e14fc053f0bfec Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:38:14 -0500 Subject: [PATCH 6/6] Windows fix Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../blueprint_handler_public_test.go | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler_public_test.go b/pkg/composer/blueprint/blueprint_handler_public_test.go index 74952d4f2..9f1c208d4 100644 --- a/pkg/composer/blueprint/blueprint_handler_public_test.go +++ b/pkg/composer/blueprint/blueprint_handler_public_test.go @@ -10,8 +10,8 @@ import ( "time" helmv2 "github.com/fluxcd/helm-controller/api/v2" - "github.com/fluxcd/pkg/apis/kustomize" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/composer/artifact" @@ -2089,7 +2089,8 @@ data: var writtenPatchPaths []string var writtenPatchContents []string mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenPatchPaths = append(writtenPatchPaths, name) writtenPatchContents = append(writtenPatchContents, string(data)) } @@ -2131,7 +2132,7 @@ data: expectedPatchPath := filepath.Join(mocks.Runtime.ConfigRoot, "patches", "test-kustomization", "configmap-test-config.yaml") found := false for _, path := range writtenPatchPaths { - if path == expectedPatchPath { + if filepath.ToSlash(path) == filepath.ToSlash(expectedPatchPath) { found = true break } @@ -2170,7 +2171,8 @@ data: var writtenPatchPaths []string mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writtenPatchPaths = append(writtenPatchPaths, name) } return nil @@ -4801,7 +4803,8 @@ data: var writeFileCalls []string mocks.Shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - if strings.Contains(name, "patches/") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/") { writeFileCalls = append(writeFileCalls, name) } return nil @@ -4866,7 +4869,8 @@ data: } mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "patches/test-kustomization") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/test-kustomization") { return &mockFileInfo{name: "test-kustomization", isDir: true}, nil } return nil, os.ErrNotExist @@ -4987,7 +4991,8 @@ patch: } mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "patches/test-kustomization") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/test-kustomization") { return &mockFileInfo{name: "test-kustomization", isDir: true}, nil } return nil, os.ErrNotExist @@ -5050,7 +5055,8 @@ patch: walkError := fmt.Errorf("walk error") mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "patches/test-kustomization") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/test-kustomization") { return &mockFileInfo{name: "patches", isDir: true}, nil } return nil, os.ErrNotExist @@ -5112,7 +5118,8 @@ metadata: } mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "patches/test-kustomization") { + normalizedName := filepath.ToSlash(name) + if strings.Contains(normalizedName, "patches/test-kustomization") { return &mockFileInfo{name: "test-kustomization", isDir: true}, nil } return nil, os.ErrNotExist