From 9a3c040369361678aef1ab251e83a38b455f5159 Mon Sep 17 00:00:00 2001 From: Aaron Mark <64331623+amarkdotdev@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:50:46 +0300 Subject: [PATCH] fix(template): regression - route registry messages to stderr in template and show (#32217) * fix: route registry messages to stderr in template and show When pulling an OCI chart, the registry client prints "Pulled: ..." and "Digest: ..." status lines (and deprecation/underscore warnings) to its configured output writer. Since v4.2.1 (introduced by #32056), these messages leaked into the stdout output of helm template and helm show, breaking downstream consumers such as cdk8s and other YAML parsers. Fix by passing the command's stderr to the registry client in the template and show commands instead of stdout. This keeps stdout clean for machine-readable YAML while still surfacing registry warnings and status messages on stderr for troubleshooting, rather than discarding them. The pull/push commands continue to print these messages on their normal output writer. The show command's addRegistryClient writer parameter is renamed to registryOut and wired through to the registry client, so it is no longer a no-op. Fixes #32215 Signed-off-by: amarkdotdev * test(cmd): cover registry client stderr routing via OCI pull Exercise helm template and helm show against an in-process OCI registry (repotest.NewOCIServer) and assert Pulled:/Digest: status lines appear on stderr only, keeping stdout free of registry noise. Signed-off-by: amarkdotdev * test(cmd): drop weak addRegistryClient unit test OCI pull coverage already asserts Pulled/Digest land on stderr, not stdout. Signed-off-by: amarkdotdev --------- Signed-off-by: amarkdotdev Co-authored-by: amarkdotdev --- pkg/cmd/registry_output_test.go | 109 ++++++++++++++++++++++++++++++++ pkg/cmd/show.go | 24 +++---- pkg/cmd/template.go | 2 +- 3 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 pkg/cmd/registry_output_test.go diff --git a/pkg/cmd/registry_output_test.go b/pkg/cmd/registry_output_test.go new file mode 100644 index 000000000..77682a4c4 --- /dev/null +++ b/pkg/cmd/registry_output_test.go @@ -0,0 +1,109 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/chart/common" + kubefake "helm.sh/helm/v4/pkg/kube/fake" + "helm.sh/helm/v4/pkg/repo/v1/repotest" +) + +func TestTemplateOCIRegistryMessagesNotOnStdout(t *testing.T) { + defer resetEnv()() + + stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string { + return []string{ + "template", "release-name", ref, + "--version", "0.1.0", + "--plain-http", + "--registry-config", registryConfig, + "--content-cache", contentCache, + } + }) + + require.NotEmpty(t, stdout) + require.NotContains(t, stdout, "Pulled:") + require.NotContains(t, stdout, "Digest:") + require.Contains(t, stderr, "Pulled:") + require.Contains(t, stderr, "Digest:") +} + +func TestShowOCIRegistryMessagesNotOnStdout(t *testing.T) { + defer resetEnv()() + + stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string { + return []string{ + "show", "chart", ref, + "--version", "0.1.0", + "--plain-http", + "--registry-config", registryConfig, + "--content-cache", contentCache, + } + }) + + require.NotEmpty(t, stdout) + require.Contains(t, stdout, "name: oci-dependent-chart") + require.NotContains(t, stdout, "Pulled:") + require.NotContains(t, stdout, "Digest:") + require.Contains(t, stderr, "Pulled:") + require.Contains(t, stderr, "Digest:") +} + +func runOCIChartCommand(t *testing.T, argsFn func(ref, registryConfig, contentCache string) []string) (string, string) { + t.Helper() + + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + t.Cleanup(func() { srv.Stop() }) + + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + require.NoError(t, err) + ociSrv.Run(t) + + ref := fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart", ociSrv.RegistryURL) + registryConfig := filepath.Join(srv.Root(), "config.json") + contentCache := t.TempDir() + args := argsFn(ref, registryConfig, contentCache) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + actionConfig := &action.Configuration{ + Releases: storageFixture(), + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: common.DefaultCapabilities, + } + + root, err := newRootCmdWithConfig(actionConfig, stdout, args, SetupLogging) + require.NoError(t, err) + root.SetOut(stdout) + root.SetErr(stderr) + root.SetArgs(args) + + require.NoError(t, root.Execute(), "stdout:\n%s\nstderr:\n%s", stdout.String(), stderr.String()) + return stdout.String(), stderr.String() +} diff --git a/pkg/cmd/show.go b/pkg/cmd/show.go index 8252d933e..161a38723 100644 --- a/pkg/cmd/show.go +++ b/pkg/cmd/show.go @@ -82,9 +82,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: showAllDesc, Args: require.ExactArgs(1), ValidArgsFunction: validArgsFunc, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowAll - err := addRegistryClient(out, client) + err := addRegistryClient(cmd.ErrOrStderr(), client) if err != nil { return err } @@ -103,9 +103,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: showValuesDesc, Args: require.ExactArgs(1), ValidArgsFunction: validArgsFunc, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowValues - err := addRegistryClient(out, client) + err := addRegistryClient(cmd.ErrOrStderr(), client) if err != nil { return err } @@ -124,9 +124,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: showChartDesc, Args: require.ExactArgs(1), ValidArgsFunction: validArgsFunc, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowChart - err := addRegistryClient(out, client) + err := addRegistryClient(cmd.ErrOrStderr(), client) if err != nil { return err } @@ -145,9 +145,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: readmeChartDesc, Args: require.ExactArgs(1), ValidArgsFunction: validArgsFunc, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowReadme - err := addRegistryClient(out, client) + err := addRegistryClient(cmd.ErrOrStderr(), client) if err != nil { return err } @@ -166,9 +166,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: showCRDsDesc, Args: require.ExactArgs(1), ValidArgsFunction: validArgsFunc, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowCRDs - err := addRegistryClient(out, client) + err := addRegistryClient(cmd.ErrOrStderr(), client) if err != nil { return err } @@ -224,8 +224,8 @@ func runShow(args []string, client *action.Show) (string, error) { return client.Run(cp) } -func addRegistryClient(out io.Writer, client *action.Show) error { - registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, +func addRegistryClient(registryOut io.Writer, client *action.Show) error { + registryClient, err := newRegistryClient(registryOut, 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.go b/pkg/cmd/template.go index 29839b871..bb0f9036a 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -85,7 +85,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.KubeVersion = parsedKubeVersion } - registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, + 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)