From dfea32aff5da0d44950d138fd7f4487ef98a23d7 Mon Sep 17 00:00:00 2001 From: Seonghyun Hong Date: Thu, 18 Jun 2026 21:21:43 +0900 Subject: [PATCH 1/2] fix(template): route registry client output to stderr so it doesn't corrupt manifest stdout helm template oci://... printed the registry client's "Pulled:" and "Digest:" diagnostics to stdout, intermixed with the rendered manifests, so piping to `kubectl apply -f -` failed. This is a regression from #32056, which rewired the registry client writer to the command's stdout (correct for pull/push/login, but not for template whose stdout is the manifest stream). Route the registry client output to stderr for the template command, restoring the pre-regression behavior while keeping #32056's intent for other commands. Closes #32215 Signed-off-by: Seonghyun Hong --- pkg/cmd/helpers_test.go | 35 +++++++++++++++++++++++++++++++++++ pkg/cmd/template.go | 5 ++++- pkg/cmd/template_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 37ceb6a85..009fe49c3 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -127,6 +127,41 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) return c, result, err } +// executeActionCommandStdoutStderrC runs cmd and returns stdout and stderr captured +// into separate buffers, so tests can assert that diagnostic output does not leak +// into the primary (stdout) output stream. +func executeActionCommandStdoutStderrC(store *storage.Storage, cmd string) (*cobra.Command, string, string, error) { + args, err := shellwords.Parse(cmd) + if err != nil { + return nil, "", "", err + } + + outBuf := new(bytes.Buffer) + errBuf := new(bytes.Buffer) + + actionConfig := &action.Configuration{ + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: common.DefaultCapabilities, + } + + root, err := newRootCmdWithConfig(actionConfig, outBuf, args, SetupLogging) + if err != nil { + return nil, "", "", err + } + + root.SetOut(outBuf) + root.SetErr(errBuf) + root.SetArgs(args) + + if mem, ok := store.Driver.(*driver.Memory); ok { + mem.SetNamespace(settings.Namespace()) + } + c, err := root.ExecuteC() + + return c, outBuf.String(), errBuf.String(), err +} + // cmdTestCase describes a test case that works with releases. type cmdTestCase struct { name string diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index bb364231b..30a1c3b44 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -85,7 +85,10 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.KubeVersion = parsedKubeVersion } - registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, + // Registry client diagnostic messages (e.g. "Pulled:", "Digest:") must not + // be written to the rendered manifest stream, otherwise they corrupt the + // YAML that `helm template` prints to stdout. Route them to stderr instead. + registryClient, err := newRegistryClient(cmd.ErrOrStderr(), client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index b43151413..869b7bc08 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -19,7 +19,12 @@ package cmd import ( "fmt" "path/filepath" + "strings" "testing" + + "helm.sh/helm/v4/pkg/repo/v1/repotest" + "helm.sh/helm/v4/pkg/storage" + "helm.sh/helm/v4/pkg/storage/driver" ) var chartPath = "testdata/testcharts/subchart" @@ -176,6 +181,40 @@ func TestTemplateCmd(t *testing.T) { runTestCmd(t, tests) } +// TestTemplateOCIRegistryOutputNotInManifest verifies that registry client +// diagnostic messages (e.g. "Pulled:" / "Digest:") emitted while fetching an OCI +// chart do not leak into the rendered manifest stream on stdout. See helm/helm#32215. +func TestTemplateOCIRegistryOutputNotInManifest(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + defer srv.Stop() + + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } + ociSrv.Run(t) + + store := storage.Init(driver.NewMemory()) + ref := fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart", ociSrv.RegistryURL) + cmd := fmt.Sprintf("template release-name %s --version 0.1.0 --plain-http", ref) + + _, stdout, stderr, err := executeActionCommandStdoutStderrC(store, cmd) + if err != nil { + t.Fatalf("unexpected error: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr) + } + + // The registry client emits "Pulled:" and "Digest:" lines while fetching the + // chart. These are diagnostics and must not appear in the manifest stdout. + for _, leaked := range []string{"Pulled:", "Digest:"} { + if strings.Contains(stdout, leaked) { + t.Errorf("registry diagnostic %q leaked into manifest stdout:\n%s", leaked, stdout) + } + } +} + func TestTemplateVersionCompletion(t *testing.T) { repoFile := "testdata/helmhome/helm/repositories.yaml" repoCache := "testdata/helmhome/helm/repository" From 2bedfc4bee0f520be2cb36ab57a7c20c63722a60 Mon Sep 17 00:00:00 2001 From: Seonghyun Hong Date: Sat, 20 Jun 2026 13:08:45 +0900 Subject: [PATCH 2/2] test(template): reset global CLI settings in OCI output test Add defer resetEnv()() so the test isolates the package-global settings/env, matching the other cmd tests and avoiding order-dependent flakiness. Signed-off-by: Seonghyun Hong --- pkg/cmd/template_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 869b7bc08..596cadc04 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -185,6 +185,8 @@ func TestTemplateCmd(t *testing.T) { // diagnostic messages (e.g. "Pulled:" / "Digest:") emitted while fetching an OCI // chart do not leak into the rendered manifest stream on stdout. See helm/helm#32215. func TestTemplateOCIRegistryOutputNotInManifest(t *testing.T) { + defer resetEnv()() + srv := repotest.NewTempServer( t, repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),