From cba6bdbec90925a7c13cf2629774b67166fcf156 Mon Sep 17 00:00:00 2001 From: yxxhero Date: Sun, 21 Jun 2026 15:39:37 +0800 Subject: [PATCH] fix: copy external files referenced by kustomize transformer 'path:' into temp dir Kustomize transformers like PatchTransformer can reference external files via a top-level 'path:' field (as opposed to inline 'patch:'). Chartify was copying the transformer YAML to tempDir but not the files it referenced, causing kustomize to fail with security errors since the referenced files were outside its restricted root. This adds resolveTransformerFileRefs() which scans transformer YAML for top-level 'path:' fields, copies referenced files into tempDir, and rewrites the paths so kustomize can access them. Path resolution matches kustomize semantics (relative to kustomization root/CWD), with a fallback to the transformer file's directory for colocated files. Fixes #90 Signed-off-by: yxxhero --- integration_test.go | 25 +++ patch.go | 153 +++++++++++++++++- .../kube_manifest_transformer_with_path/want | 86 ++++++++++ .../patch.yaml | 6 + .../transformer.yaml | 8 + 5 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 testdata/integration/testcases/kube_manifest_transformer_with_path/want create mode 100644 testdata/kube_manifest_transformer_with_path/patch.yaml create mode 100644 testdata/kube_manifest_transformer_with_path/transformer.yaml 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