diff --git a/integration_test.go b/integration_test.go index 0e4d5e2..a6763d7 100644 --- a/integration_test.go +++ b/integration_test.go @@ -354,6 +354,31 @@ func TestIntegration(t *testing.T) { EnableKustomizeAlphaPlugins: true, }, }) + + // SAVE_SNAPSHOT=1 go1.25 test -run ^TestIntegration/kube_manifest_transformer_with_path$ ./ + // Tests that kustomize transformers referencing external files via "path:" (e.g., PatchTransformer) + // have those files copied into the temp dir so kustomize can access them. + // See https://github.com/helmfile/chartify/issues/90 + runTest(t, integrationTestCase{ + description: "kube_manifest_transformer_with_path", + release: "myapp", + chart: "./testdata/kube_manifest_yml", + opts: ChartifyOpts{ + AdhocChartDependencies: []ChartDependency{ + { + Alias: "log", + Chart: repo + chartSuffix, + Version: "0.1.0", + }, + }, + Transformers: []string{ + "./testdata/kube_manifest_transformer_with_path/transformer.yaml", + }, + SetFlags: []string{ + "--set", "log.enabled=true", + }, + }, + }) } func setupHelmConfig(t *testing.T) { diff --git a/patch.go b/patch.go index 161a4d3..13fd638 100644 --- a/patch.go +++ b/patch.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "fmt" + "io" "os" "path/filepath" "strings" @@ -172,6 +173,16 @@ resources: if err != nil { return err } + + // Resolve any external file references (e.g., "path:" in PatchTransformer) by + // copying referenced files into tempDir and rewriting paths. + // See https://github.com/helmfile/chartify/issues/90 + transformerFileDir := filepath.Dir(f) + bytes, err = r.resolveTransformerFileRefs(bytes, transformerFileDir, tempDir) + if err != nil { + return err + } + path := filepath.Join("transformers", fmt.Sprintf("transformer.%d.yaml", i)) abspath := filepath.Join(tempDir, path) if err := os.MkdirAll(filepath.Dir(abspath), 0755); err != nil { @@ -358,7 +369,7 @@ resources: return fmt.Errorf("writing %s: %w", crdsFile, err) } - removedPathList := append(append([]string{}, ContentDirs...), "strategicmergepatches", "kustomization.yaml", renderedFileName) + removedPathList := append(append([]string{}, ContentDirs...), "strategicmergepatches", "transformer-patch-files", "kustomization.yaml", renderedFileName) for _, f := range removedPathList { d := filepath.Join(tempDir, f) @@ -418,3 +429,143 @@ resources: return nil } + +// resolveTransformerFileRefs scans transformer YAML content for top-level "path" fields +// that reference external files (e.g., PatchTransformer's "path" field). It copies those +// files into tempDir and rewrites the path references to point to the copied locations, +// so that kustomize can access them within its restricted root. +// See https://github.com/helmfile/chartify/issues/90 +func (r *Runner) resolveTransformerFileRefs(transformerBytes []byte, transformerFileDir string, tempDir string) ([]byte, error) { + // Decode all YAML documents from the transformer content. + // A transformer file can be a single document, a list of documents, or multi-document YAML. + decoder := yaml.NewDecoder(bytes.NewReader(transformerBytes)) + + var docs []interface{} + for { + var doc interface{} + if err := decoder.Decode(&doc); err != nil { + if err == io.EOF { + break + } + // If we can't parse the YAML, return the original content unchanged and let kustomize handle it. + return transformerBytes, nil + } + docs = append(docs, doc) + } + + if len(docs) == 0 { + return transformerBytes, nil + } + + // Collect all transformer map nodes. This handles both single-document and + // list-of-documents formats. Maps are reference types in Go, so modifying + // them below will be reflected when re-encoding docs. + var allMaps []map[string]interface{} + for _, doc := range docs { + switch v := doc.(type) { + case map[string]interface{}: + allMaps = append(allMaps, v) + case []interface{}: + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + allMaps = append(allMaps, m) + } + } + } + } + + // Look for top-level "path" fields that reference existing files and copy them into tempDir. + patchFileCounter := 0 + modified := false + for _, m := range allMaps { + pathStr, ok := m["path"].(string) + if !ok || pathStr == "" { + continue + } + + // Resolve the referenced file's path. Kustomize resolves paths in transformers + // relative to the kustomization root (the user's CWD). We also try the transformer + // file's own directory as a fallback for colocated files. + resolvedPath, found := r.resolveTransformerPath(pathStr, transformerFileDir) + if !found { + continue + } + + // Skip directories — the "path" field should reference a file. + // A directory match is likely coincidental (e.g., a "path" field that + // happens to match a directory name); leave it for kustomize to handle. + info, err := os.Stat(resolvedPath) + if err != nil { + return nil, fmt.Errorf("checking file referenced by transformer path %q: %w", pathStr, err) + } + if info.IsDir() { + continue + } + + fileBytes, err := r.ReadFile(resolvedPath) + if err != nil { + return nil, fmt.Errorf("reading file referenced by transformer path %q: %w", pathStr, err) + } + + // Copy the referenced file into tempDir under a known subdirectory. + // The path in the transformer is rewritten relative to the kustomization root (tempDir), + // which is how kustomize resolves file references in transformers. + destRelPath := filepath.Join("transformer-patch-files", fmt.Sprintf("patchfile.%d.yaml", patchFileCounter)) + destAbsPath := filepath.Join(tempDir, destRelPath) + if err := os.MkdirAll(filepath.Dir(destAbsPath), 0755); err != nil { + return nil, fmt.Errorf("creating directory for transformer patch file: %w", err) + } + if err := r.WriteFile(destAbsPath, fileBytes, 0644); err != nil { + return nil, fmt.Errorf("writing transformer patch file: %w", err) + } + + r.Logf("Copied transformer path reference %q to %q", pathStr, destRelPath) + + m["path"] = destRelPath + modified = true + patchFileCounter++ + } + + if !modified { + return transformerBytes, nil + } + + // Re-encode the YAML with the updated path references. + var out bytes.Buffer + encoder := yaml.NewEncoder(&out) + encoder.SetIndent(2) + for _, doc := range docs { + if err := encoder.Encode(doc); err != nil { + return nil, fmt.Errorf("re-encoding transformer YAML after path resolution: %w", err) + } + } + if err := encoder.Close(); err != nil { + return nil, fmt.Errorf("closing transformer YAML encoder: %w", err) + } + + return out.Bytes(), nil +} + +// resolveTransformerPath resolves a "path" field from a transformer document to an +// existing file. It tries the CWD first (matching kustomize's behavior where paths +// are relative to the kustomization root), then falls back to the transformer file's +// directory (for colocated files). Returns the resolved path and true if found. +func (r *Runner) resolveTransformerPath(pathStr string, transformerFileDir string) (string, bool) { + if filepath.IsAbs(pathStr) { + exists, _ := r.Exists(pathStr) + return pathStr, exists + } + + // Try CWD first — this is how kustomize resolves paths in transformers. + if exists, _ := r.Exists(pathStr); exists { + return pathStr, true + } + + // Fall back to the transformer file's directory for colocated files. + candidate := filepath.Join(transformerFileDir, pathStr) + if exists, _ := r.Exists(candidate); exists { + return candidate, true + } + + return pathStr, false +} diff --git a/testdata/integration/testcases/kube_manifest_transformer_with_path/want b/testdata/integration/testcases/kube_manifest_transformer_with_path/want new file mode 100644 index 0000000..5c9a9f1 --- /dev/null +++ b/testdata/integration/testcases/kube_manifest_transformer_with_path/want @@ -0,0 +1,86 @@ +--- +# Source: kube_manifest_yml/templates/patched_resources.yaml +apiVersion: v1 +data: + bar: | + -----BEGIN CERTIFICATE----- + FOO + -----END CERTIFICATE----- + foo: patched-via-transformer-path +kind: ConfigMap +metadata: + name: myconfig1 +--- +# Source: kube_manifest_yml/templates/patched_resources.yaml +apiVersion: v1 +data: + bar: | + -----BEGIN CERTIFICATE----- + FOO + -----END CERTIFICATE----- + foo: bar +kind: ConfigMap +metadata: + name: myconfig2 +--- +# Source: kube_manifest_yml/templates/patched_resources.yaml +apiVersion: v1 +data: + baz: | + -----BEGIN CERTIFICATE----- + FOO + -----END CERTIFICATE----- + foo: baz +kind: ConfigMap +metadata: + name: myconfig3 +--- +# Source: kube_manifest_yml/templates/patched_resources.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: myapp + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: log + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: log-0.1.0 + name: myapp-log +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: myapp + app.kubernetes.io/name: log + template: + metadata: + labels: + app.kubernetes.io/instance: myapp + app.kubernetes.io/name: log + spec: + containers: + - image: nginx:1.16.0 + name: log +--- +# Source: kube_manifest_yml/templates/patched_resources.yaml +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: myapp + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: log + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: log-0.1.0 + name: myapp-log-test-connection +spec: + containers: + - args: + - myapp-log:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/testdata/kube_manifest_transformer_with_path/patch.yaml b/testdata/kube_manifest_transformer_with_path/patch.yaml new file mode 100644 index 0000000..2fe58e0 --- /dev/null +++ b/testdata/kube_manifest_transformer_with_path/patch.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: myconfig1 +data: + foo: patched-via-transformer-path diff --git a/testdata/kube_manifest_transformer_with_path/transformer.yaml b/testdata/kube_manifest_transformer_with_path/transformer.yaml new file mode 100644 index 0000000..5e54856 --- /dev/null +++ b/testdata/kube_manifest_transformer_with_path/transformer.yaml @@ -0,0 +1,8 @@ +apiVersion: builtin +kind: PatchTransformer +metadata: + name: not-important +path: patch.yaml +target: + kind: ConfigMap + name: myconfig1