From 4a903393a4ff78b3ba3202817e0602c5afef8010 Mon Sep 17 00:00:00 2001 From: Abdullah Al Mahmud Date: Sat, 26 Sep 2026 11:50:31 -0500 Subject: [PATCH] feat(cmd): let CLI embedders inject a logger instead of setting slog default NewRootCmd hands logSetup the --debug value and expects it to install a process-wide slog default, and the commands in pkg/cmd log through the package-level slog functions. An application embedding the Helm CLI has two choices today: let SetupLogging replace its own default logger, or keep its logger and lose Helm's log format and --debug handling. Add NewRootCmdWithLogger, which takes a func(debug bool) *slog.Logger and never calls slog.SetDefault. The logger is set on the action configuration (and so on the Kubernetes client and storage drivers) and is passed to every command in pkg/cmd, which now log through it instead of the global slog functions. Export NewLogger so embedders can keep the Helm CLI's log format without touching the global logger. NewRootCmd and SetupLogging keep their signatures and behavior: logSetup still runs first and Helm logs through whatever default it installed. The exported AddWaitFlag still logs to the slog default logger; commands built by the root command use an unexported variant that takes the injected logger. Refs #31724 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Abdullah Al Mahmud --- pkg/cmd/flags.go | 40 ++++++++--- pkg/cmd/flags_test.go | 50 +++++++++++++ pkg/cmd/helpers.go | 6 +- pkg/cmd/helpers_test.go | 11 ++- pkg/cmd/install.go | 20 +++--- pkg/cmd/load_plugins.go | 20 +++--- pkg/cmd/plugin.go | 11 +-- pkg/cmd/plugin_install.go | 8 ++- pkg/cmd/plugin_list.go | 4 +- pkg/cmd/plugin_test.go | 8 +-- pkg/cmd/plugin_uninstall.go | 21 +++--- pkg/cmd/plugin_update.go | 15 ++-- pkg/cmd/pull.go | 3 +- pkg/cmd/registry_login.go | 6 +- pkg/cmd/registry_output_test.go | 2 +- pkg/cmd/rollback.go | 4 +- pkg/cmd/root.go | 78 +++++++++++++++----- pkg/cmd/root_test.go | 122 +++++++++++++++++++++++++++++++- pkg/cmd/search.go | 7 +- pkg/cmd/search_hub.go | 7 +- pkg/cmd/search_repo.go | 13 ++-- pkg/cmd/show.go | 16 ++--- pkg/cmd/template.go | 6 +- pkg/cmd/uninstall.go | 2 +- pkg/cmd/upgrade.go | 18 ++--- 25 files changed, 371 insertions(+), 127 deletions(-) diff --git a/pkg/cmd/flags.go b/pkg/cmd/flags.go index 61db07883..3a51a0c17 100644 --- a/pkg/cmd/flags.go +++ b/pkg/cmd/flags.go @@ -56,9 +56,17 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line") } +// AddWaitFlag adds the --wait flag to cmd, storing the selected strategy in wait. +// Deprecation warnings for boolean values are logged to the slog default logger. func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { + addWaitFlag(cmd, wait, nil) +} + +// addWaitFlag is AddWaitFlag with the logger that receives deprecation +// warnings. A nil logger means the slog default logger at the time of logging. +func addWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy, logger *slog.Logger) { cmd.Flags().Var( - newWaitValue(kube.HookOnlyStrategy, wait), + newWaitValue(kube.HookOnlyStrategy, wait, logger), "wait", "wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy'. Default when flag is omitted: 'hookOnly'.", ) @@ -85,32 +93,42 @@ func defaultCLIWaitOptions() []kube.WaitOption { } } -type waitValue kube.WaitStrategy +type waitValue struct { + strategy *kube.WaitStrategy + logger *slog.Logger +} -func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue { +func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy, logger *slog.Logger) *waitValue { *ws = defaultValue - return (*waitValue)(ws) + return &waitValue{strategy: ws, logger: logger} } func (ws *waitValue) String() string { - if ws == nil { + if ws == nil || ws.strategy == nil { return "" } - return string(*ws) + return string(*ws.strategy) +} + +func (ws *waitValue) log() *slog.Logger { + if ws.logger != nil { + return ws.logger + } + return slog.Default() } func (ws *waitValue) Set(s string) error { switch s { case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy): - *ws = waitValue(s) + *ws.strategy = kube.WaitStrategy(s) return nil case "true": - slog.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher") - *ws = waitValue(kube.StatusWatcherStrategy) + ws.log().Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher") + *ws.strategy = kube.StatusWatcherStrategy return nil case "false": - slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly") - *ws = waitValue(kube.HookOnlyStrategy) + ws.log().Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly") + *ws.strategy = kube.HookOnlyStrategy return nil default: return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy) diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index 09dc61638..7e30216be 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -17,14 +17,19 @@ limitations under the License. package cmd import ( + "bytes" "fmt" + "log/slog" "testing" "time" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -118,3 +123,48 @@ func TestPostRendererFlagSetOnce(t *testing.T) { // Set the plugin name again to a different value is not ok require.Error(t, str.Set("cat")) } + +func TestWaitValueLogsToInjectedLogger(t *testing.T) { + tests := []struct { + value string + want kube.WaitStrategy + wantLog string + }{ + {value: "true", want: kube.StatusWatcherStrategy, wantLog: "--wait=true is deprecated"}, + {value: "false", want: kube.HookOnlyStrategy, wantLog: "--wait=false is deprecated"}, + {value: "legacy", want: kube.LegacyStrategy}, + } + + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + var logBuf bytes.Buffer + var ws kube.WaitStrategy + v := newWaitValue(kube.HookOnlyStrategy, &ws, slog.New(slog.NewTextHandler(&logBuf, nil))) + assert.Equal(t, kube.HookOnlyStrategy, ws) + + require.NoError(t, v.Set(tt.value)) + assert.Equal(t, tt.want, ws) + assert.Equal(t, string(tt.want), v.String()) + if tt.wantLog == "" { + assert.Empty(t, logBuf.String()) + } else { + assert.Contains(t, logBuf.String(), tt.wantLog) + } + }) + } +} + +func TestAddWaitFlagLogsToDefaultLogger(t *testing.T) { + origDefault := slog.Default() + t.Cleanup(func() { slog.SetDefault(origDefault) }) + var logBuf bytes.Buffer + slog.SetDefault(slog.New(slog.NewTextHandler(&logBuf, nil))) + + cmd := &cobra.Command{Use: "test"} + var ws kube.WaitStrategy + AddWaitFlag(cmd, &ws) + require.NoError(t, cmd.Flags().Set("wait", "true")) + + assert.Equal(t, kube.StatusWatcherStrategy, ws) + assert.Contains(t, logBuf.String(), "--wait=true is deprecated") +} diff --git a/pkg/cmd/helpers.go b/pkg/cmd/helpers.go index 8866b8650..218ac9876 100644 --- a/pkg/cmd/helpers.go +++ b/pkg/cmd/helpers.go @@ -42,13 +42,13 @@ func addDryRunFlag(cmd *cobra.Command) { // Determine the `action.DryRunStrategy` given -dry-run=` flag (or absence of) // Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted -func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { +func cmdGetDryRunFlagStrategy(logger *slog.Logger, cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { f := cmd.Flag("dry-run") v := f.Value.String() switch v { case f.NoOptDefVal: - slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`) + logger.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`) return action.DryRunClient, nil case string(action.DryRunClient): return action.DryRunClient, nil @@ -76,7 +76,7 @@ func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRu if b { result = action.DryRunClient } - slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result)) + logger.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result)) return result, nil } diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 611d1a54c..dfbfa1f0e 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -82,6 +82,12 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, } func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { + return executeActionCommandWithLoggerC(store, in, NewLogger, cmd) +} + +// executeActionCommandWithLoggerC runs cmd against store with the root command +// logging through the logger returned by newLogger. +func executeActionCommandWithLoggerC(store *storage.Storage, in *os.File, newLogger func(bool) *slog.Logger, cmd string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err @@ -95,7 +101,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) Capabilities: common.DefaultCapabilities, } - root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) + root, err := newRootCmdWithConfig(actionConfig, buf, args, newLogger) if err != nil { return nil, "", err } @@ -272,7 +278,6 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) { for name, tc := range testCases { logBuf := new(bytes.Buffer) logger := slog.New(slog.NewJSONHandler(logBuf, nil)) - slog.SetDefault(logger) cmd := &cobra.Command{ Use: "helm", @@ -281,7 +286,7 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) { cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg}) t.Run(name, func(t *testing.T) { - dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate) + dryRunStrategy, err := cmdGetDryRunFlagStrategy(logger, cmd, tc.IsTemplate) if tc.ExpectedError { require.Error(t, err) } else { diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 182b4a7ff..5a73f8be3 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -151,13 +151,13 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, false) if err != nil { return err } client.DryRunStrategy = dryRunStrategy - rel, err := runInstall(args, client, valueOpts, out) + rel, err := runInstall(cfg.Logger(), args, client, valueOpts, out) if err != nil { return fmt.Errorf("INSTALLATION FAILED: %w", err) } @@ -173,7 +173,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } f := cmd.Flags() - addInstallFlags(cmd, f, client, valueOpts) + addInstallFlags(cmd, f, client, valueOpts, cfg.Logger()) // hide-secret is not available in all places the install flags are used so // it is added separately f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") @@ -184,7 +184,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return cmd } -func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { +func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options, logger *slog.Logger) { f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force", false, "deprecated") @@ -233,7 +233,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) - AddWaitFlag(cmd, &client.WaitStrategy) + addWaitFlag(cmd, &client.WaitStrategy, logger) cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") @@ -252,10 +252,10 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal } } -func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { - slog.Debug("Original chart version", "version", client.Version) +func runInstall(logger *slog.Logger, args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { + logger.Debug("Original chart version", "version", client.Version) if client.Version == "" && client.Devel { - slog.Debug("setting version to >0.0.0-0") + logger.Debug("setting version to >0.0.0-0") client.Version = ">0.0.0-0" } @@ -270,7 +270,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } - slog.Debug("Chart path", "path", cp) + logger.Debug("Chart path", "path", cp) p := getter.All(settings) vals, err := valueOpts.MergeValues(p) @@ -294,7 +294,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options } if ac.Deprecated() { - slog.Warn("this chart is deprecated") + logger.Warn("this chart is deprecated") } if req := ac.MetaDependencies(); len(req) > 0 { diff --git a/pkg/cmd/load_plugins.go b/pkg/cmd/load_plugins.go index 0c74ba51d..ec8eaa909 100644 --- a/pkg/cmd/load_plugins.go +++ b/pkg/cmd/load_plugins.go @@ -52,7 +52,7 @@ const ( // This follows a different pattern than the other commands because it has // to inspect its environment and then add commands to the base command // as it finds them. -func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { +func loadCLIPlugins(baseCmd *cobra.Command, logger *slog.Logger, out io.Writer) { // If HELM_NO_PLUGINS is set to 1, do not load plugins. if os.Getenv("HELM_NO_PLUGINS") == "1" { return @@ -64,7 +64,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { } found, err := plugin.FindPlugins(dirs, descriptor) if err != nil { - slog.Error("failed to load plugins", slog.String("error", err.Error())) + logger.Error("failed to load plugins", slog.String("error", err.Error())) return } @@ -136,7 +136,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { for _, cmd := range baseCmd.Commands() { if cmd.Name() == c.Name() { - slog.Error("failed to load plugins: name conflicts", slog.String("name", c.Name())) + logger.Error("failed to load plugins: name conflicts", slog.String("name", c.Name())) return } } @@ -151,7 +151,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { if (err == nil && ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || /* for the tests */ subCmd == baseCmd.Root() { - loadCompletionForPlugin(c, plug) + loadCompletionForPlugin(logger, c, plug) } } } @@ -218,12 +218,12 @@ type pluginCommand struct { // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin // and add the dynamic completion hook to call the optional plugin.complete -func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) { +func loadCompletionForPlugin(logger *slog.Logger, pluginCmd *cobra.Command, plug plugin.Plugin) { // Parse the yaml file providing the plugin's sub-commands and flags cmds, err := loadFile(plug.Dir() + string(filepath.Separator) + pluginStaticCompletionFile) if err != nil { // The file could be missing or invalid. No static completion for this plugin. - slog.Debug("plugin completion file loading", slog.String("error", err.Error())) + logger.Debug("plugin completion file loading", slog.String("error", err.Error())) // Continue to setup dynamic completion. cmds = &pluginCommand{} } @@ -231,18 +231,18 @@ func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) { // Preserve the Usage string specified for the plugin cmds.Name = pluginCmd.Use - addPluginCommands(plug, pluginCmd, cmds) + addPluginCommands(logger, plug, pluginCmd, cmds) } // addPluginCommands is a recursive method that adds each different level // of sub-commands and flags for the plugins that have provided such information -func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { +func addPluginCommands(logger *slog.Logger, plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { if cmds == nil { return } if cmds.Name == "" { - slog.Debug("sub-command name field missing", slog.String("commandPath", baseCmd.CommandPath())) + logger.Debug("sub-command name field missing", slog.String("commandPath", baseCmd.CommandPath())) return } @@ -311,7 +311,7 @@ func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginC Run: func(_ *cobra.Command, _ []string) {}, } baseCmd.AddCommand(subCmd) - addPluginCommands(plug, subCmd, &cmd) + addPluginCommands(logger, plug, subCmd, &cmd) } } diff --git a/pkg/cmd/plugin.go b/pkg/cmd/plugin.go index ba904ef5f..a07efacc4 100644 --- a/pkg/cmd/plugin.go +++ b/pkg/cmd/plugin.go @@ -17,6 +17,7 @@ package cmd import ( "io" + "log/slog" "github.com/spf13/cobra" @@ -27,17 +28,17 @@ const pluginHelp = ` Manage client-side Helm plugins. ` -func newPluginCmd(out io.Writer) *cobra.Command { +func newPluginCmd(logger *slog.Logger, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "plugin", Short: "install, list, or uninstall Helm plugins", Long: pluginHelp, } cmd.AddCommand( - newPluginInstallCmd(out), - newPluginListCmd(out), - newPluginUninstallCmd(out), - newPluginUpdateCmd(out), + newPluginInstallCmd(logger, out), + newPluginListCmd(logger, out), + newPluginUninstallCmd(logger, out), + newPluginUpdateCmd(logger, out), newPluginPackageCmd(out), newPluginVerifyCmd(out), ) diff --git a/pkg/cmd/plugin_install.go b/pkg/cmd/plugin_install.go index c248ed818..dc91f288d 100644 --- a/pkg/cmd/plugin_install.go +++ b/pkg/cmd/plugin_install.go @@ -45,6 +45,8 @@ type pluginInstallOptions struct { plainHTTP bool password string username string + + logger *slog.Logger } const pluginInstallDesc = ` @@ -58,8 +60,8 @@ treated as "local dev" and do not require signatures. Use --verify=false to explicitly skip signature verification (NOT recommended). ` -func newPluginInstallCmd(out io.Writer) *cobra.Command { - o := &pluginInstallOptions{} +func newPluginInstallCmd(logger *slog.Logger, out io.Writer) *cobra.Command { + o := &pluginInstallOptions{logger: logger} cmd := &cobra.Command{ Use: "install [options] ", Short: "install a Helm plugin", @@ -169,7 +171,7 @@ func (o *pluginInstallOptions) run(out io.Writer) error { fmt.Fprintf(out, "Plugin Hash Verified: %s\n", verifyResult.FileHash) } - slog.Debug("loading plugin", "path", i.Path()) + o.logger.Debug("loading plugin", "path", i.Path()) p, err := plugin.LoadDir(i.Path()) if err != nil { return fmt.Errorf("plugin is installed but unusable: %w", err) diff --git a/pkg/cmd/plugin_list.go b/pkg/cmd/plugin_list.go index 74e969e04..3a480d8fc 100644 --- a/pkg/cmd/plugin_list.go +++ b/pkg/cmd/plugin_list.go @@ -29,7 +29,7 @@ import ( "helm.sh/helm/v4/internal/plugin/schema" ) -func newPluginListCmd(out io.Writer) *cobra.Command { +func newPluginListCmd(logger *slog.Logger, out io.Writer) *cobra.Command { var pluginType string cmd := &cobra.Command{ Use: "list", @@ -37,7 +37,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command { Short: "list installed Helm plugins", ValidArgsFunction: noMoreArgsCompFunc, RunE: func(_ *cobra.Command, _ []string) error { - slog.Debug("pluginDirs", "directory", settings.PluginsDirectory) + logger.Debug("pluginDirs", "directory", settings.PluginsDirectory) dirs := filepath.SplitList(settings.PluginsDirectory) descriptor := plugin.Descriptor{ Type: pluginType, diff --git a/pkg/cmd/plugin_test.go b/pkg/cmd/plugin_test.go index 6a7980119..325ace67f 100644 --- a/pkg/cmd/plugin_test.go +++ b/pkg/cmd/plugin_test.go @@ -88,7 +88,7 @@ func TestLoadCLIPlugins(t *testing.T) { out bytes.Buffer cmd cobra.Command ) - loadCLIPlugins(&cmd, &out) + loadCLIPlugins(&cmd, NewLogger(false), &out) fullEnvOutput := strings.Join([]string{ "HELM_PLUGIN_NAME=fullenv", @@ -155,7 +155,7 @@ func TestLoadPluginsWithSpace(t *testing.T) { out bytes.Buffer cmd cobra.Command ) - loadCLIPlugins(&cmd, &out) + loadCLIPlugins(&cmd, NewLogger(false), &out) envs := strings.Join([]string{ "fullenv", @@ -220,7 +220,7 @@ func TestLoadCLIPluginsForCompletion(t *testing.T) { cmd := &cobra.Command{ Use: "completion", } - loadCLIPlugins(cmd, &out) + loadCLIPlugins(cmd, NewLogger(false), &out) tests := []staticCompletionDetails{ {"args", []string{}, []string{}, []staticCompletionDetails{}}, @@ -313,7 +313,7 @@ func TestLoadCLIPlugins_HelmNoPlugins(t *testing.T) { out := bytes.NewBuffer(nil) cmd := &cobra.Command{} - loadCLIPlugins(cmd, out) + loadCLIPlugins(cmd, NewLogger(false), out) plugins := cmd.Commands() require.Empty(t, plugins, "Expected 0 plugins, got %d", len(plugins)) diff --git a/pkg/cmd/plugin_uninstall.go b/pkg/cmd/plugin_uninstall.go index 81d9b31ee..018833899 100644 --- a/pkg/cmd/plugin_uninstall.go +++ b/pkg/cmd/plugin_uninstall.go @@ -29,11 +29,12 @@ import ( ) type pluginUninstallOptions struct { - names []string + names []string + logger *slog.Logger } -func newPluginUninstallCmd(out io.Writer) *cobra.Command { - o := &pluginUninstallOptions{} +func newPluginUninstallCmd(logger *slog.Logger, out io.Writer) *cobra.Command { + o := &pluginUninstallOptions{logger: logger} cmd := &cobra.Command{ Use: "uninstall ...", @@ -61,7 +62,7 @@ func (o *pluginUninstallOptions) complete(args []string) error { } func (o *pluginUninstallOptions) run(out io.Writer) error { - slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory) + o.logger.Debug("loading installer plugins", "dir", settings.PluginsDirectory) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) if err != nil { return err @@ -69,7 +70,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error { var errorPlugins []error for _, name := range o.names { if found := findPlugin(plugins, name); found != nil { - if err := uninstallPlugin(found); err != nil { + if err := uninstallPlugin(o.logger, found); err != nil { errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%w)", name, err)) } else { fmt.Fprintf(out, "Uninstalled plugin: %s\n", name) @@ -84,7 +85,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error { return nil } -func uninstallPlugin(p plugin.Plugin) error { +func uninstallPlugin(logger *slog.Logger, p plugin.Plugin) error { if err := os.RemoveAll(p.Dir()); err != nil { return err } @@ -102,18 +103,18 @@ func uninstallPlugin(p plugin.Plugin) error { // Remove tarball file tarballPath := filepath.Join(pluginsDir, versionedBasename) if _, err := os.Stat(tarballPath); err == nil { - slog.Debug("removing versioned tarball", "path", tarballPath) + logger.Debug("removing versioned tarball", "path", tarballPath) if err := os.Remove(tarballPath); err != nil { - slog.Debug("failed to remove tarball file", "path", tarballPath, "error", err) + logger.Debug("failed to remove tarball file", "path", tarballPath, "error", err) } } // Remove provenance file provPath := filepath.Join(pluginsDir, versionedBasename+".prov") if _, err := os.Stat(provPath); err == nil { - slog.Debug("removing versioned provenance", "path", provPath) + logger.Debug("removing versioned provenance", "path", provPath) if err := os.Remove(provPath); err != nil { - slog.Debug("failed to remove provenance file", "path", provPath, "error", err) + logger.Debug("failed to remove provenance file", "path", provPath, "error", err) } } } diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 1cba7ffa9..6922e21e0 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -29,11 +29,12 @@ import ( ) type pluginUpdateOptions struct { - names []string + names []string + logger *slog.Logger } -func newPluginUpdateCmd(out io.Writer) *cobra.Command { - o := &pluginUpdateOptions{} +func newPluginUpdateCmd(logger *slog.Logger, out io.Writer) *cobra.Command { + o := &pluginUpdateOptions{logger: logger} cmd := &cobra.Command{ Use: "update ...", @@ -61,7 +62,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { } func (o *pluginUpdateOptions) run(out io.Writer) error { - slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) + o.logger.Debug("loading installed plugins", "path", settings.PluginsDirectory) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) if err != nil { return err @@ -70,7 +71,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { for _, name := range o.names { if found := findPlugin(plugins, name); found != nil { - if err := updatePlugin(found); err != nil { + if err := updatePlugin(o.logger, found); err != nil { errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%w)", name, err)) } else { fmt.Fprintf(out, "Updated plugin: %s\n", name) @@ -85,7 +86,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { return nil } -func updatePlugin(p plugin.Plugin) error { +func updatePlugin(logger *slog.Logger, p plugin.Plugin) error { exactLocation, err := filepath.EvalSymlinks(p.Dir()) if err != nil { return err @@ -103,7 +104,7 @@ func updatePlugin(p plugin.Plugin) error { return err } - slog.Debug("loading plugin", "path", i.Path()) + logger.Debug("loading plugin", "path", i.Path()) updatedPlugin, err := plugin.LoadDir(i.Path()) if err != nil { return err diff --git a/pkg/cmd/pull.go b/pkg/cmd/pull.go index 9f5af462b..b430614d6 100644 --- a/pkg/cmd/pull.go +++ b/pkg/cmd/pull.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "log" - "log/slog" "github.com/spf13/cobra" @@ -61,7 +60,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(_ *cobra.Command, args []string) error { client.Settings = settings if client.Version == "" && client.Devel { - slog.Debug("setting version to >0.0.0-0") + cfg.Logger().Debug("setting version to >0.0.0-0") client.Version = ">0.0.0-0" } diff --git a/pkg/cmd/registry_login.go b/pkg/cmd/registry_login.go index 605717e22..51a001062 100644 --- a/pkg/cmd/registry_login.go +++ b/pkg/cmd/registry_login.go @@ -63,7 +63,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman RunE: func(_ *cobra.Command, args []string) error { hostname := args[0] - username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt) + username, password, err := getUsernamePassword(cfg.Logger(), o.username, o.password, o.passwordFromStdinOpt) if err != nil { return err } @@ -91,7 +91,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman } // Adapted from https://github.com/oras-project/oras -func getUsernamePassword(usernameOpt, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { +func getUsernamePassword(logger *slog.Logger, usernameOpt, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { var err error username := usernameOpt password := passwordOpt @@ -128,7 +128,7 @@ func getUsernamePassword(usernameOpt, passwordOpt string, passwordFromStdinOpt b } } default: - slog.Warn("using --password via the CLI is insecure. Use --password-stdin") + logger.Warn("using --password via the CLI is insecure. Use --password-stdin") } return username, password, nil diff --git a/pkg/cmd/registry_output_test.go b/pkg/cmd/registry_output_test.go index 77682a4c4..49a8ea50d 100644 --- a/pkg/cmd/registry_output_test.go +++ b/pkg/cmd/registry_output_test.go @@ -98,7 +98,7 @@ func runOCIChartCommand(t *testing.T, argsFn func(ref, registryConfig, contentCa Capabilities: common.DefaultCapabilities, } - root, err := newRootCmdWithConfig(actionConfig, stdout, args, SetupLogging) + root, err := newRootCmdWithConfig(actionConfig, stdout, args, NewLogger) require.NoError(t, err) root.SetOut(stdout) root.SetErr(stderr) diff --git a/pkg/cmd/rollback.go b/pkg/cmd/rollback.go index 62d0be4af..3b7c79f89 100644 --- a/pkg/cmd/rollback.go +++ b/pkg/cmd/rollback.go @@ -73,7 +73,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return fmt.Errorf("description must be %d characters or less, got %d", action.MaxDescriptionLength, descLen) } - dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, false) if err != nil { return err } @@ -101,7 +101,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") addDryRunFlag(cmd) - AddWaitFlag(cmd, &client.WaitStrategy) + addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger()) cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index dbedca026..60c5d6651 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -103,9 +103,44 @@ By default, the default directories depend on the Operating System. The defaults var settings = cli.New() +// NewRootCmd creates the root Helm command. +// +// logSetup is called with the value of the --debug flag before any command is +// created. It is expected to install the process-wide slog default logger (see +// SetupLogging); Helm then logs through slog.Default(). Applications embedding +// the Helm CLI that do not want Helm to depend on or change the global logger +// should use NewRootCmdWithLogger instead. func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { + return newRootCmd(out, args, loggerFromSetup(logSetup)) +} + +// loggerFromSetup adapts a NewRootCmd logSetup function: it runs logSetup and +// then uses whatever slog default logger that left in place. +func loggerFromSetup(logSetup func(bool)) func(bool) *slog.Logger { + return func(debug bool) *slog.Logger { + logSetup(debug) + return slog.Default() + } +} + +// NewRootCmdWithLogger creates the root Helm command, like NewRootCmd, but +// routes Helm's log output to the logger returned by newLogger instead of the +// slog default logger. newLogger is called with the value of the --debug flag. +// The logger is set on the action configuration and handed to every Helm +// command, and slog.SetDefault is never called. +// +// Pass NewLogger to keep the Helm CLI's own log format. If newLogger is nil or +// returns nil, NewLogger is used. +// +// A few library packages that do not accept a logger yet still log through +// the slog default logger. +func NewRootCmdWithLogger(out io.Writer, args []string, newLogger func(debug bool) *slog.Logger) (*cobra.Command, error) { + return newRootCmd(out, args, newLogger) +} + +func newRootCmd(out io.Writer, args []string, newLogger func(debug bool) *slog.Logger) (*cobra.Command, error) { actionConfig := action.NewConfiguration() - cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup) + cmd, err := newRootCmdWithConfig(actionConfig, out, args, newLogger) if err != nil { return nil, err } @@ -122,6 +157,13 @@ func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Comma return cmd, nil } +// NewLogger returns the logger used by the Helm client: a text handler that +// writes to stderr without timestamps and emits debug records only when debug +// is true. +func NewLogger(debug bool) *slog.Logger { + return logging.NewLogger(func() bool { return debug }) +} + // SetupLogging sets up Helm logging used by the Helm client. // This function is passed to the NewRootCmd function to enable logging. Any other // application that uses the NewRootCmd function to setup all the Helm commands may @@ -129,9 +171,11 @@ func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Comma // enables applications using Helm commands to integrate with their existing logging // system. // The debug argument is the value if Helm is set for debugging (i.e. --debug flag) +// +// SetupLogging replaces the process-wide slog default logger. To give Helm a +// logger without changing the global one, use NewRootCmdWithLogger. func SetupLogging(debug bool) { - logger := logging.NewLogger(func() bool { return debug }) - slog.SetDefault(logger) + slog.SetDefault(NewLogger(debug)) } // configureColorOutput configures the color output based on the ColorMode setting @@ -148,7 +192,7 @@ func configureColorOutput(settings *cli.EnvSettings) { } } -func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { +func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, args []string, newLogger func(debug bool) *slog.Logger) (*cobra.Command, error) { cmd := &cobra.Command{ Use: "helm", Short: "The Helm package manager for Kubernetes.", @@ -178,18 +222,18 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg flags.ParseErrorsAllowlist.UnknownFlags = true flags.Parse(args) - logSetup(settings.Debug) - - // newRootCmdWithConfig is only called from NewRootCmd. NewRootCmd sets up - // NewConfiguration without a custom logger. So, the slog default is used. logSetup - // can change the default logger to the one in the logger package. This happens for - // the Helm client. This means the actionConfig logger is different from the slog - // default logger. If they are different we sync the actionConfig logger to the slog - // current default one. - if actionConfig.Logger() != slog.Default() { - actionConfig.SetLogger(slog.Default().Handler()) + var logger *slog.Logger + if newLogger != nil { + logger = newLogger(settings.Debug) + } + if logger == nil { + logger = NewLogger(settings.Debug) } + // The action configuration, and through it the Kubernetes client and the + // storage drivers, log through the same logger as the commands below. + actionConfig.SetLogger(logger.Handler()) + // Validate color mode setting switch settings.ColorMode { case "never", "auto", "always": @@ -272,7 +316,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg newLintCmd(out), newPackageCmd(out), newRepoCmd(out), - newSearchCmd(out), + newSearchCmd(logger, out), newVerifyCmd(out), // release commands @@ -289,7 +333,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg newCompletionCmd(out), newEnvCmd(out), - newPluginCmd(out), + newPluginCmd(logger, out), newVersionCmd(out), // Hidden documentation generator command: 'helm docs' @@ -302,7 +346,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg ) // Find and add CLI plugins - loadCLIPlugins(cmd, out) + loadCLIPlugins(cmd, logger, out) // Check for expired repositories checkForExpiredRepos(settings.RepositoryConfig) diff --git a/pkg/cmd/root_test.go b/pkg/cmd/root_test.go index b07bdbbf6..98ae9e637 100644 --- a/pkg/cmd/root_test.go +++ b/pkg/cmd/root_test.go @@ -18,6 +18,7 @@ package cmd import ( "bytes" + "io" "log/slog" "os" "path/filepath" @@ -26,10 +27,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/internal/test/ensure" "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath/xdg" + "helm.sh/helm/v4/pkg/storage" + "helm.sh/helm/v4/pkg/storage/driver" ) func TestRootCmd(t *testing.T) { @@ -128,10 +132,13 @@ func TestUnknownSubCmd(t *testing.T) { // } func TestRootCmdLogger(t *testing.T) { + origDefault := slog.Default() + t.Cleanup(func() { slog.SetDefault(origDefault) }) + args := []string{} buf := new(bytes.Buffer) actionConfig := action.NewConfiguration() - _, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) + _, err := newRootCmdWithConfig(actionConfig, buf, args, loggerFromSetup(SetupLogging)) require.NoError(t, err) l1 := actionConfig.Logger() @@ -139,3 +146,116 @@ func TestRootCmdLogger(t *testing.T) { assert.Equal(t, l2.Handler(), l1.Handler(), "expected actionConfig logger to be the slog default logger") } + +func TestRootCmdWithLoggerLeavesDefaultUntouched(t *testing.T) { + origDefault := slog.Default() + t.Cleanup(func() { slog.SetDefault(origDefault) }) + sentinel := slog.New(slog.DiscardHandler) + slog.SetDefault(sentinel) + + var gotDebug bool + injected := slog.New(slog.NewTextHandler(new(bytes.Buffer), nil)) + actionConfig := action.NewConfiguration() + _, err := newRootCmdWithConfig(actionConfig, io.Discard, []string{"--debug"}, func(debug bool) *slog.Logger { + gotDebug = debug + return injected + }) + require.NoError(t, err) + + assert.True(t, gotDebug, "expected the --debug flag value to be passed to the logger constructor") + assert.Same(t, sentinel, slog.Default(), "expected the slog default logger to be left untouched") + assert.Equal(t, injected.Handler(), actionConfig.Logger().Handler(), "expected actionConfig to use the injected logger") +} + +func TestRootCmdWithNilLogger(t *testing.T) { + origDefault := slog.Default() + t.Cleanup(func() { slog.SetDefault(origDefault) }) + sentinel := slog.New(slog.DiscardHandler) + slog.SetDefault(sentinel) + + tests := map[string]func(bool) *slog.Logger{ + "nil constructor": nil, + "constructor return nil": func(bool) *slog.Logger { return nil }, + } + for name, newLogger := range tests { + t.Run(name, func(t *testing.T) { + actionConfig := action.NewConfiguration() + _, err := newRootCmdWithConfig(actionConfig, io.Discard, []string{}, newLogger) + require.NoError(t, err) + + assert.Same(t, sentinel, slog.Default(), "expected the slog default logger to be left untouched") + assert.IsType(t, &logging.DebugCheckHandler{}, actionConfig.Logger().Handler(), "expected the Helm CLI logger as the fallback") + }) + } +} + +func TestRootCmdWithLoggerRoutesCommandLogs(t *testing.T) { + defer resetEnv()() + + // A record logged through the slog default logger instead of the injected + // one ends up in globalBuf. + origDefault := slog.Default() + t.Cleanup(func() { slog.SetDefault(origDefault) }) + var globalBuf bytes.Buffer + sentinel := slog.New(slog.NewTextHandler(&globalBuf, &slog.HandlerOptions{Level: slog.LevelDebug})) + slog.SetDefault(sentinel) + + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + tests := []struct { + name string + cmd string + wantLog string + }{ + { + name: "install --wait=true deprecation warning", + cmd: "install aeneas testdata/testcharts/empty --wait=true", + wantLog: "--wait=true is deprecated", + }, + { + name: "template --dry-run deprecation warning", + cmd: "template testdata/testcharts/empty --dry-run", + wantLog: "--dry-run is deprecated", + }, + { + name: "upgrade --install debug output", + cmd: "upgrade --install funny-bunny testdata/testcharts/empty --dry-run=true", + wantLog: "Original chart version", + }, + { + name: "search repo debug output", + cmd: "search repo alpine --repository-config " + repoFile + " --repository-cache " + repoCache, + wantLog: "original chart version", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + globalBuf.Reset() + var injectedBuf bytes.Buffer + injected := slog.New(slog.NewTextHandler(&injectedBuf, &slog.HandlerOptions{Level: slog.LevelDebug})) + + // Mirror action.Configuration.Init, which hands the configuration's + // logger to the storage driver. + mem := driver.NewMemory() + mem.SetLogger(injected.Handler()) + store := storage.Init(mem) + + _, _, err := executeActionCommandWithLoggerC(store, nil, func(bool) *slog.Logger { return injected }, tt.cmd) + require.NoError(t, err) + + assert.Contains(t, injectedBuf.String(), tt.wantLog) + assert.NotContains(t, globalBuf.String(), tt.wantLog, "expected the record not to go through the slog default logger") + assert.Same(t, sentinel, slog.Default(), "expected the slog default logger to be left untouched") + }) + } +} + +func TestNewLogger(t *testing.T) { + ctx := t.Context() + + assert.False(t, NewLogger(false).Enabled(ctx, slog.LevelDebug), "expected debug records to be dropped without --debug") + assert.True(t, NewLogger(false).Enabled(ctx, slog.LevelWarn)) + assert.True(t, NewLogger(true).Enabled(ctx, slog.LevelDebug), "expected debug records with --debug") +} diff --git a/pkg/cmd/search.go b/pkg/cmd/search.go index eb6c6e0b5..a8c685497 100644 --- a/pkg/cmd/search.go +++ b/pkg/cmd/search.go @@ -18,6 +18,7 @@ package cmd import ( "io" + "log/slog" "github.com/spf13/cobra" ) @@ -28,15 +29,15 @@ they can be stored including the Artifact Hub and repositories you have added. Use search subcommands to search different locations for charts. ` -func newSearchCmd(out io.Writer) *cobra.Command { +func newSearchCmd(logger *slog.Logger, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "search [keyword]", Short: "search for a keyword in charts", Long: searchDesc, } - cmd.AddCommand(newSearchHubCmd(out)) - cmd.AddCommand(newSearchRepoCmd(out)) + cmd.AddCommand(newSearchHubCmd(logger, out)) + cmd.AddCommand(newSearchRepoCmd(logger, out)) return cmd } diff --git a/pkg/cmd/search_hub.go b/pkg/cmd/search_hub.go index 3f7afa3f4..98145d617 100644 --- a/pkg/cmd/search_hub.go +++ b/pkg/cmd/search_hub.go @@ -57,10 +57,11 @@ type searchHubOptions struct { outputFormat output.Format listRepoURL bool failOnNoResult bool + logger *slog.Logger } -func newSearchHubCmd(out io.Writer) *cobra.Command { - o := &searchHubOptions{} +func newSearchHubCmd(logger *slog.Logger, out io.Writer) *cobra.Command { + o := &searchHubOptions{logger: logger} cmd := &cobra.Command{ Use: "hub [KEYWORD]", @@ -91,7 +92,7 @@ func (o *searchHubOptions) run(ctx context.Context, out io.Writer, args []string q := strings.Join(args, " ") results, err := c.SearchWithContext(ctx, q) if err != nil { - slog.Debug("search failed", slog.Any("error", err)) + o.logger.Debug("search failed", slog.Any("error", err)) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) } diff --git a/pkg/cmd/search_repo.go b/pkg/cmd/search_repo.go index 2d9d967f8..4268407ea 100644 --- a/pkg/cmd/search_repo.go +++ b/pkg/cmd/search_repo.go @@ -73,10 +73,11 @@ type searchRepoOptions struct { repoCacheDir string outputFormat output.Format failOnNoResult bool + logger *slog.Logger } -func newSearchRepoCmd(out io.Writer) *cobra.Command { - o := &searchRepoOptions{} +func newSearchRepoCmd(logger *slog.Logger, out io.Writer) *cobra.Command { + o := &searchRepoOptions{logger: logger} cmd := &cobra.Command{ Use: "repo [keyword]", @@ -131,17 +132,17 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error { } func (o *searchRepoOptions) setupSearchedVersion() { - slog.Debug("original chart version", "version", o.version) + o.logger.Debug("original chart version", "version", o.version) if o.version != "" { return } if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). - slog.Debug("setting version to >0.0.0-0") + o.logger.Debug("setting version to >0.0.0-0") o.version = ">0.0.0-0" } else { // search only for stable releases, prerelease versions will be skipped - slog.Debug("setting version to >0.0.0") + o.logger.Debug("setting version to >0.0.0") o.version = ">0.0.0" } } @@ -190,7 +191,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) { f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n)) ind, err := repo.LoadIndexFile(f) if err != nil { - slog.Warn("repo is corrupt or missing", slog.String("repo", n), slog.Any("error", err)) + o.logger.Warn("repo is corrupt or missing", slog.String("repo", n), slog.Any("error", err)) continue } diff --git a/pkg/cmd/show.go b/pkg/cmd/show.go index 161a38723..2f15b84ee 100644 --- a/pkg/cmd/show.go +++ b/pkg/cmd/show.go @@ -88,7 +88,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - output, err := runShow(args, client) + output, err := runShow(cfg.Logger(), args, client) if err != nil { return err } @@ -109,7 +109,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - output, err := runShow(args, client) + output, err := runShow(cfg.Logger(), args, client) if err != nil { return err } @@ -130,7 +130,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - output, err := runShow(args, client) + output, err := runShow(cfg.Logger(), args, client) if err != nil { return err } @@ -151,7 +151,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - output, err := runShow(args, client) + output, err := runShow(cfg.Logger(), args, client) if err != nil { return err } @@ -172,7 +172,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - output, err := runShow(args, client) + output, err := runShow(cfg.Logger(), args, client) if err != nil { return err } @@ -210,10 +210,10 @@ func addShowFlags(subCmd *cobra.Command, client *action.Show) { } } -func runShow(args []string, client *action.Show) (string, error) { - slog.Debug("original chart version", "version", client.Version) +func runShow(logger *slog.Logger, args []string, client *action.Show) (string, error) { + logger.Debug("original chart version", "version", client.Version) if client.Version == "" && client.Devel { - slog.Debug("setting version to >0.0.0-0") + logger.Debug("setting version to >0.0.0-0") client.Version = ">0.0.0-0" } diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 5645ab33d..6e3e0ec74 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -92,7 +92,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true) + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, true) if err != nil { return err } @@ -105,7 +105,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Replace = true // Skip the name check client.APIVersions = common.VersionSet(extraAPIs) client.IncludeCRDs = includeCrds - rel, err := runInstall(args, client, valueOpts, out) + rel, err := runInstall(cfg.Logger(), args, client, valueOpts, out) if err != nil && !settings.Debug { if rel != nil { @@ -207,7 +207,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } f := cmd.Flags() - addInstallFlags(cmd, f, client, valueOpts) + addInstallFlags(cmd, f, client, valueOpts, cfg.Logger()) f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") f.BoolVar(&validate, "validate", false, "deprecated") diff --git a/pkg/cmd/uninstall.go b/pkg/cmd/uninstall.go index 315c29a8d..7d4790dda 100644 --- a/pkg/cmd/uninstall.go +++ b/pkg/cmd/uninstall.go @@ -82,7 +82,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background. Use \"foreground\" with --wait to ensure resources with finalizers are fully deleted before returning.") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.StringVar(&client.Description, "description", "", "add a custom description") - AddWaitFlag(cmd, &client.WaitStrategy) + addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger()) return cmd } diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 688842141..47252bbb3 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -113,7 +113,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, false) if err != nil { return err } @@ -126,7 +126,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { histClient := action.NewHistory(cfg) histClient.Max = 1 versions, err := histClient.Run(args[0]) - if errors.Is(err, driver.ErrReleaseNotFound) || isReleaseUninstalled(versions) { + if errors.Is(err, driver.ErrReleaseNotFound) || isReleaseUninstalled(cfg.Logger(), versions) { // Only print this to stdout for table output if outfmt == output.Table { fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) @@ -159,11 +159,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.ForceConflicts = client.ForceConflicts instClient.ServerSideApply = client.ServerSideApply != "false" - if isReleaseUninstalled(versions) { + if isReleaseUninstalled(cfg.Logger(), versions) { instClient.Replace = true } - rel, err := runInstall(args, instClient, valueOpts, out) + rel, err := runInstall(cfg.Logger(), args, instClient, valueOpts, out) if err != nil { return err } @@ -180,7 +180,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if client.Version == "" && client.Devel { - slog.Debug("setting version to >0.0.0-0") + cfg.Logger().Debug("setting version to >0.0.0-0") client.Version = ">0.0.0-0" } @@ -239,7 +239,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if ac.Deprecated() { - slog.Warn("this chart is deprecated") + cfg.Logger().Warn("this chart is deprecated") } // Create context and prepare the handle of SIGTERM @@ -312,7 +312,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer, settings) - AddWaitFlag(cmd, &client.WaitStrategy) + addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger()) cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") @@ -329,10 +329,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return cmd } -func isReleaseUninstalled(versionsi []ri.Releaser) bool { +func isReleaseUninstalled(logger *slog.Logger, versionsi []ri.Releaser) bool { versions, err := releaseListToV1List(versionsi) if err != nil { - slog.Error("cannot convert release list to v1 release list", "error", err) + logger.Error("cannot convert release list to v1 release list", "error", err) return false } return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled