Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
153 changes: 152 additions & 1 deletion patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions testdata/kube_manifest_transformer_with_path/patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: myconfig1
data:
foo: patched-via-transformer-path
8 changes: 8 additions & 0 deletions testdata/kube_manifest_transformer_with_path/transformer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: builtin
kind: PatchTransformer
metadata:
name: not-important
path: patch.yaml
target:
kind: ConfigMap
name: myconfig1