From 8de2d66952c4977aa221a12c0fc279f6b8e8534a Mon Sep 17 00:00:00 2001 From: yxxhero Date: Fri, 10 Apr 2026 07:20:20 +0800 Subject: [PATCH] fix: apply post-renderer to output-dir-template output When --output-dir and --post-renderer are both passed to helm template, Helm writes pre-post-renderer content to files and sends post-renderer output to stdout. This workaround strips --output-dir from helm flags, captures the post-renderer-processed stdout, and writes it to the output directory. Fixes #2515 Signed-off-by: yxxhero --- pkg/helmexec/exec.go | 44 ++++++++++++++++++++++++++++++++++++--- pkg/helmexec/exec_test.go | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go index cdfa90a8..6bc3fa7c 100644 --- a/pkg/helmexec/exec.go +++ b/pkg/helmexec/exec.go @@ -646,17 +646,55 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string) helm.logger.Infof("Templating release=%v, chart=%v", name, redactedURL(chart)) args := []string{"template", name, chart} - out, err := helm.exec(append(args, flags...), map[string]string{}, nil) - var outputToFile bool + var hasPostRenderer bool for _, f := range flags { if strings.HasPrefix("--output-dir", f) { outputToFile = true - break + } + if f == "--post-renderer" { + hasPostRenderer = true } } + if outputToFile && hasPostRenderer { + // Helm does not apply --post-renderer to files written by --output-dir. + // It writes pre-post-renderer content to files and sends post-renderer output to stdout. + // Workaround: run without --output-dir, capture stdout (with post-renderer applied), + // and write the output to the output directory ourselves. + var outputDir string + filteredFlags := make([]string, 0, len(flags)) + for i := 0; i < len(flags); i++ { + if flags[i] == "--output-dir" && i+1 < len(flags) { + outputDir = flags[i+1] + i++ + continue + } + if strings.HasPrefix(flags[i], "--output-dir=") { + outputDir = strings.TrimPrefix(flags[i], "--output-dir=") + continue + } + filteredFlags = append(filteredFlags, flags[i]) + } + + out, err := helm.exec(append(args, filteredFlags...), map[string]string{}, nil) + if err != nil { + return err + } + + if len(out) > 0 { + outputPath := filepath.Join(outputDir, name+".yaml") + if writeErr := os.WriteFile(outputPath, append(out, '\n'), 0644); writeErr != nil { + return writeErr + } + helm.logger.Debugf("Wrote post-renderer output to %s", outputPath) + } + return nil + } + + out, err := helm.exec(append(args, flags...), map[string]string{}, nil) + if outputToFile { // With --output-dir is passed to helm-template, // we can safely direct all the logs from it to our logger. diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go index 9644c21a..f399cf34 100644 --- a/pkg/helmexec/exec_test.go +++ b/pkg/helmexec/exec_test.go @@ -1255,6 +1255,46 @@ exec: helm --kubeconfig config --kube-context dev template release https://examp } } +func Test_Template_PostRendererWithOutputDir(t *testing.T) { + tmpDir := t.TempDir() + var buffer bytes.Buffer + logger := NewLogger(&buffer, "debug") + + runner := &mockRunner{output: []byte("apiVersion: v1\nkind: Namespace\n")} + helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = helm.TemplateRelease("myrelease", "path/to/chart", + "--post-renderer", "/bin/echo", + "--output-dir", tmpDir, + "--values", "file.yml", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + outputPath := filepath.Join(tmpDir, "myrelease.yaml") + data, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("expected output file %s to exist: %v", outputPath, err) + } + + expected := "apiVersion: v1\nkind: Namespace\n\n" + if string(data) != expected { + t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected) + } + + outputLog := buffer.String() + if strings.Contains(outputLog, "--output-dir") { + t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog) + } + if !strings.Contains(outputLog, "--post-renderer") { + t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog) + } +} + func Test_IsHelm3(t *testing.T) { helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")} helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)