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"