From 6ce7bb7686b32368969323036110e7f4578069b6 Mon Sep 17 00:00:00 2001 From: Angel Montero Date: Tue, 18 Aug 2026 16:38:14 +0000 Subject: [PATCH] fix: route OCI pull metadata to stderr instead of stdout This addresses the issue where helm template/show included registry pull metadata in machine-readable output, by routing it to stderr via cmd.ErrOrStderr(). Signed-off-by: Angel Montero --- pkg/cmd/helpers_test.go | 30 +++++++++++++++++++----------- pkg/cmd/show.go | 20 ++++++++++---------- pkg/cmd/show_test.go | 38 ++++++++++++++++++++++++++++++++++++++ pkg/cmd/template.go | 2 +- pkg/cmd/template_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 22 deletions(-) diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 611d1a54c..207dc2863 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -81,27 +81,25 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, return executeActionCommandStdinC(store, nil, cmd) } -func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { +func executeActionCommandWithWriters(store *storage.Storage, in *os.File, outWriter, errWriter io.Writer, cmd string) (*cobra.Command, error) { args, err := shellwords.Parse(cmd) if err != nil { - return nil, "", err + return nil, err } - buf := new(bytes.Buffer) - actionConfig := &action.Configuration{ Releases: store, KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, Capabilities: common.DefaultCapabilities, } - root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) + root, err := newRootCmdWithConfig(actionConfig, outWriter, args, SetupLogging) if err != nil { - return nil, "", err + return nil, err } - root.SetOut(buf) - root.SetErr(buf) + root.SetOut(outWriter) + root.SetErr(errWriter) root.SetArgs(args) oldStdin := os.Stdin @@ -118,10 +116,13 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) mem.SetNamespace(settings.Namespace()) } c, err := root.ExecuteC() + return c, err +} - result := buf.String() - - return c, result, err +func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { + buf := new(bytes.Buffer) + c, err := executeActionCommandWithWriters(store, in, buf, buf, cmd) + return c, buf.String(), err } // cmdTestCase describes a test case that works with releases. @@ -301,3 +302,10 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) { }) } } + +func executeActionCommandErr(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, string, error) { + bufOut := new(bytes.Buffer) + bufErr := new(bytes.Buffer) + c, err := executeActionCommandWithWriters(store, in, bufOut, bufErr, cmd) + return c, bufOut.String(), bufErr.String(), err +} diff --git a/pkg/cmd/show.go b/pkg/cmd/show.go index 8252d933e..d9aac9126 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 } diff --git a/pkg/cmd/show_test.go b/pkg/cmd/show_test.go index 61c4f8b10..6eaf15ccc 100644 --- a/pkg/cmd/show_test.go +++ b/pkg/cmd/show_test.go @@ -19,6 +19,7 @@ package cmd import ( "fmt" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -86,6 +87,43 @@ func TestShowPreReleaseChart(t *testing.T) { } } +func TestShowOCIChartRegistryPullMetadataGoesToStderr(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) + + contentTmp := t.TempDir() + outdir := srv.Root() + cmdStr := fmt.Sprintf("show chart oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --registry-config %s --content-cache %s --plain-http", + ociSrv.RegistryURL, + filepath.Join(outdir, "config.json"), + contentTmp, + ) + + _, outStr, errStr, err := executeActionCommandErr(storageFixture(), nil, cmdStr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if strings.Contains(outStr, "Pulled: ") || strings.Contains(outStr, "Digest: ") { + t.Fatalf("expected stdout to exclude registry pull metadata, got: %s", outStr) + } + if !strings.Contains(errStr, "Pulled: ") { + t.Fatalf("expected stderr to contain registry pull metadata, got: %s", errStr) + } + if !strings.Contains(outStr, "apiVersion:") { + t.Fatalf("expected chart metadata in output, got: %s", outStr) + } +} + func TestShowVersionCompletion(t *testing.T) { repoFile := "testdata/helmhome/helm/repositories.yaml" repoCache := "testdata/helmhome/helm/repository" 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) diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index d508a0505..9e671daae 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -19,7 +19,10 @@ package cmd import ( "fmt" "path/filepath" + "strings" "testing" + + "helm.sh/helm/v4/pkg/repo/v1/repotest" ) var chartPath = "testdata/testcharts/subchart" @@ -176,6 +179,43 @@ func TestTemplateCmd(t *testing.T) { runTestCmd(t, tests) } +func TestTemplateOCIChartRegistryPullMetadataGoesToStderr(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) + + contentTmp := t.TempDir() + outdir := srv.Root() + cmdStr := fmt.Sprintf("template --generate-name oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --registry-config %s --content-cache %s --plain-http", + ociSrv.RegistryURL, + filepath.Join(outdir, "config.json"), + contentTmp, + ) + + _, outStr, errStr, err := executeActionCommandErr(storageFixture(), nil, cmdStr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if strings.Contains(outStr, "Pulled: ") || strings.Contains(outStr, "Digest: ") { + t.Fatalf("expected stdout to exclude registry pull metadata, got: %s", outStr) + } + if !strings.Contains(errStr, "Pulled: ") { + t.Fatalf("expected stderr to contain registry pull metadata, got: %s", errStr) + } + if !strings.Contains(outStr, "# Source:") { + t.Fatalf("expected rendered manifests in output, got: %s", outStr) + } +} + func TestTemplateVersionCompletion(t *testing.T) { repoFile := "testdata/helmhome/helm/repositories.yaml" repoCache := "testdata/helmhome/helm/repository"