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 <amarkdotdev@users.noreply.github.com>

* 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 <amarkdotdev@users.noreply.github.com>

* test(cmd): drop weak addRegistryClient unit test

OCI pull coverage already asserts Pulled/Digest land on stderr, not stdout.

Signed-off-by: amarkdotdev <amarkdotdev@users.noreply.github.com>

---------

Signed-off-by: amarkdotdev <amarkdotdev@users.noreply.github.com>
Co-authored-by: amarkdotdev <amarkdotdev@users.noreply.github.com>
pull/32596/head
Aaron Mark 1 week ago committed by GitHub
parent 11e2010aeb
commit 9a3c040369
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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()
}

@ -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)

@ -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)

Loading…
Cancel
Save