pull/32691/merge
Abdullah Al Mahmud 3 days ago committed by GitHub
commit a867188925
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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") 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) { 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( cmd.Flags().Var(
newWaitValue(kube.HookOnlyStrategy, wait), newWaitValue(kube.HookOnlyStrategy, wait, logger),
"wait", "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'.", "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 *ws = defaultValue
return (*waitValue)(ws) return &waitValue{strategy: ws, logger: logger}
} }
func (ws *waitValue) String() string { func (ws *waitValue) String() string {
if ws == nil { if ws == nil || ws.strategy == nil {
return "" 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 { func (ws *waitValue) Set(s string) error {
switch s { switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy): case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s) *ws.strategy = kube.WaitStrategy(s)
return nil return nil
case "true": case "true":
slog.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher") ws.log().Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher")
*ws = waitValue(kube.StatusWatcherStrategy) *ws.strategy = kube.StatusWatcherStrategy
return nil return nil
case "false": case "false":
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly") ws.log().Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly")
*ws = waitValue(kube.HookOnlyStrategy) *ws.strategy = kube.HookOnlyStrategy
return nil return nil
default: default:
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy) return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy)

@ -17,14 +17,19 @@ limitations under the License.
package cmd package cmd
import ( import (
"bytes"
"fmt" "fmt"
"log/slog"
"testing" "testing"
"time" "time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common" "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" 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 // Set the plugin name again to a different value is not ok
require.Error(t, str.Set("cat")) 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")
}

@ -42,13 +42,13 @@ func addDryRunFlag(cmd *cobra.Command) {
// Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of) // Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of)
// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted // 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") f := cmd.Flag("dry-run")
v := f.Value.String() v := f.Value.String()
switch v { switch v {
case f.NoOptDefVal: 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 return action.DryRunClient, nil
case string(action.DryRunClient): case string(action.DryRunClient):
return action.DryRunClient, nil return action.DryRunClient, nil
@ -76,7 +76,7 @@ func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRu
if b { if b {
result = action.DryRunClient 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 return result, nil
} }

@ -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) { 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) args, err := shellwords.Parse(cmd)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -95,7 +101,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string)
Capabilities: common.DefaultCapabilities, Capabilities: common.DefaultCapabilities,
} }
root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) root, err := newRootCmdWithConfig(actionConfig, buf, args, newLogger)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -272,7 +278,6 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
logBuf := new(bytes.Buffer) logBuf := new(bytes.Buffer)
logger := slog.New(slog.NewJSONHandler(logBuf, nil)) logger := slog.New(slog.NewJSONHandler(logBuf, nil))
slog.SetDefault(logger)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "helm", Use: "helm",
@ -281,7 +286,7 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) {
cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg}) cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg})
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate) dryRunStrategy, err := cmdGetDryRunFlagStrategy(logger, cmd, tc.IsTemplate)
if tc.ExpectedError { if tc.ExpectedError {
require.Error(t, err) require.Error(t, err)
} else { } else {

@ -151,13 +151,13 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, false)
if err != nil { if err != nil {
return err return err
} }
client.DryRunStrategy = dryRunStrategy client.DryRunStrategy = dryRunStrategy
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(cfg.Logger(), args, client, valueOpts, out)
if err != nil { if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err) return fmt.Errorf("INSTALLATION FAILED: %w", err)
} }
@ -173,7 +173,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() 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 // hide-secret is not available in all places the install flags are used so
// it is added separately // it is added separately
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") 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 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.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-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") 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) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
AddWaitFlag(cmd, &client.WaitStrategy) addWaitFlag(cmd, &client.WaitStrategy, logger)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "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) { func runInstall(logger *slog.Logger, args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) {
slog.Debug("Original chart version", "version", client.Version) logger.Debug("Original chart version", "version", client.Version)
if client.Version == "" && client.Devel { 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" client.Version = ">0.0.0-0"
} }
@ -270,7 +270,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
return nil, err return nil, err
} }
slog.Debug("Chart path", "path", cp) logger.Debug("Chart path", "path", cp)
p := getter.All(settings) p := getter.All(settings)
vals, err := valueOpts.MergeValues(p) vals, err := valueOpts.MergeValues(p)
@ -294,7 +294,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
if ac.Deprecated() { if ac.Deprecated() {
slog.Warn("this chart is deprecated") logger.Warn("this chart is deprecated")
} }
if req := ac.MetaDependencies(); len(req) > 0 { if req := ac.MetaDependencies(); len(req) > 0 {

@ -52,7 +52,7 @@ const (
// This follows a different pattern than the other commands because it has // This follows a different pattern than the other commands because it has
// to inspect its environment and then add commands to the base command // to inspect its environment and then add commands to the base command
// as it finds them. // 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 HELM_NO_PLUGINS is set to 1, do not load plugins.
if os.Getenv("HELM_NO_PLUGINS") == "1" { if os.Getenv("HELM_NO_PLUGINS") == "1" {
return return
@ -64,7 +64,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
} }
found, err := plugin.FindPlugins(dirs, descriptor) found, err := plugin.FindPlugins(dirs, descriptor)
if err != nil { 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 return
} }
@ -136,7 +136,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
for _, cmd := range baseCmd.Commands() { for _, cmd := range baseCmd.Commands() {
if cmd.Name() == c.Name() { 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 return
} }
} }
@ -151,7 +151,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
if (err == nil && if (err == nil &&
((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) ||
/* for the tests */ subCmd == baseCmd.Root() { /* 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 // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin
// and add the dynamic completion hook to call the optional plugin.complete // 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 // Parse the yaml file providing the plugin's sub-commands and flags
cmds, err := loadFile(plug.Dir() + string(filepath.Separator) + pluginStaticCompletionFile) cmds, err := loadFile(plug.Dir() + string(filepath.Separator) + pluginStaticCompletionFile)
if err != nil { if err != nil {
// The file could be missing or invalid. No static completion for this plugin. // 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. // Continue to setup dynamic completion.
cmds = &pluginCommand{} cmds = &pluginCommand{}
} }
@ -231,18 +231,18 @@ func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) {
// Preserve the Usage string specified for the plugin // Preserve the Usage string specified for the plugin
cmds.Name = pluginCmd.Use cmds.Name = pluginCmd.Use
addPluginCommands(plug, pluginCmd, cmds) addPluginCommands(logger, plug, pluginCmd, cmds)
} }
// addPluginCommands is a recursive method that adds each different level // addPluginCommands is a recursive method that adds each different level
// of sub-commands and flags for the plugins that have provided such information // 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 { if cmds == nil {
return return
} }
if cmds.Name == "" { 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 return
} }
@ -311,7 +311,7 @@ func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginC
Run: func(_ *cobra.Command, _ []string) {}, Run: func(_ *cobra.Command, _ []string) {},
} }
baseCmd.AddCommand(subCmd) baseCmd.AddCommand(subCmd)
addPluginCommands(plug, subCmd, &cmd) addPluginCommands(logger, plug, subCmd, &cmd)
} }
} }

@ -17,6 +17,7 @@ package cmd
import ( import (
"io" "io"
"log/slog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -27,17 +28,17 @@ const pluginHelp = `
Manage client-side Helm plugins. 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{ cmd := &cobra.Command{
Use: "plugin", Use: "plugin",
Short: "install, list, or uninstall Helm plugins", Short: "install, list, or uninstall Helm plugins",
Long: pluginHelp, Long: pluginHelp,
} }
cmd.AddCommand( cmd.AddCommand(
newPluginInstallCmd(out), newPluginInstallCmd(logger, out),
newPluginListCmd(out), newPluginListCmd(logger, out),
newPluginUninstallCmd(out), newPluginUninstallCmd(logger, out),
newPluginUpdateCmd(out), newPluginUpdateCmd(logger, out),
newPluginPackageCmd(out), newPluginPackageCmd(out),
newPluginVerifyCmd(out), newPluginVerifyCmd(out),
) )

@ -45,6 +45,8 @@ type pluginInstallOptions struct {
plainHTTP bool plainHTTP bool
password string password string
username string username string
logger *slog.Logger
} }
const pluginInstallDesc = ` 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). Use --verify=false to explicitly skip signature verification (NOT recommended).
` `
func newPluginInstallCmd(out io.Writer) *cobra.Command { func newPluginInstallCmd(logger *slog.Logger, out io.Writer) *cobra.Command {
o := &pluginInstallOptions{} o := &pluginInstallOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install [options] <path|url>", Use: "install [options] <path|url>",
Short: "install a Helm plugin", 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) 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()) p, err := plugin.LoadDir(i.Path())
if err != nil { if err != nil {
return fmt.Errorf("plugin is installed but unusable: %w", err) return fmt.Errorf("plugin is installed but unusable: %w", err)

@ -29,7 +29,7 @@ import (
"helm.sh/helm/v4/internal/plugin/schema" "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 var pluginType string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
@ -37,7 +37,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
Short: "list installed Helm plugins", Short: "list installed Helm plugins",
ValidArgsFunction: noMoreArgsCompFunc, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
slog.Debug("pluginDirs", "directory", settings.PluginsDirectory) logger.Debug("pluginDirs", "directory", settings.PluginsDirectory)
dirs := filepath.SplitList(settings.PluginsDirectory) dirs := filepath.SplitList(settings.PluginsDirectory)
descriptor := plugin.Descriptor{ descriptor := plugin.Descriptor{
Type: pluginType, Type: pluginType,

@ -88,7 +88,7 @@ func TestLoadCLIPlugins(t *testing.T) {
out bytes.Buffer out bytes.Buffer
cmd cobra.Command cmd cobra.Command
) )
loadCLIPlugins(&cmd, &out) loadCLIPlugins(&cmd, NewLogger(false), &out)
fullEnvOutput := strings.Join([]string{ fullEnvOutput := strings.Join([]string{
"HELM_PLUGIN_NAME=fullenv", "HELM_PLUGIN_NAME=fullenv",
@ -155,7 +155,7 @@ func TestLoadPluginsWithSpace(t *testing.T) {
out bytes.Buffer out bytes.Buffer
cmd cobra.Command cmd cobra.Command
) )
loadCLIPlugins(&cmd, &out) loadCLIPlugins(&cmd, NewLogger(false), &out)
envs := strings.Join([]string{ envs := strings.Join([]string{
"fullenv", "fullenv",
@ -220,7 +220,7 @@ func TestLoadCLIPluginsForCompletion(t *testing.T) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "completion", Use: "completion",
} }
loadCLIPlugins(cmd, &out) loadCLIPlugins(cmd, NewLogger(false), &out)
tests := []staticCompletionDetails{ tests := []staticCompletionDetails{
{"args", []string{}, []string{}, []staticCompletionDetails{}}, {"args", []string{}, []string{}, []staticCompletionDetails{}},
@ -313,7 +313,7 @@ func TestLoadCLIPlugins_HelmNoPlugins(t *testing.T) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
cmd := &cobra.Command{} cmd := &cobra.Command{}
loadCLIPlugins(cmd, out) loadCLIPlugins(cmd, NewLogger(false), out)
plugins := cmd.Commands() plugins := cmd.Commands()
require.Empty(t, plugins, "Expected 0 plugins, got %d", len(plugins)) require.Empty(t, plugins, "Expected 0 plugins, got %d", len(plugins))

@ -30,10 +30,11 @@ import (
type pluginUninstallOptions struct { type pluginUninstallOptions struct {
names []string names []string
logger *slog.Logger
} }
func newPluginUninstallCmd(out io.Writer) *cobra.Command { func newPluginUninstallCmd(logger *slog.Logger, out io.Writer) *cobra.Command {
o := &pluginUninstallOptions{} o := &pluginUninstallOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uninstall <plugin>...", Use: "uninstall <plugin>...",
@ -61,7 +62,7 @@ func (o *pluginUninstallOptions) complete(args []string) error {
} }
func (o *pluginUninstallOptions) run(out io.Writer) 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) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc)
if err != nil { if err != nil {
return err return err
@ -69,7 +70,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
var errorPlugins []error var errorPlugins []error
for _, name := range o.names { for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil { 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)) errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%w)", name, err))
} else { } else {
fmt.Fprintf(out, "Uninstalled plugin: %s\n", name) fmt.Fprintf(out, "Uninstalled plugin: %s\n", name)
@ -84,7 +85,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
return nil 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 { if err := os.RemoveAll(p.Dir()); err != nil {
return err return err
} }
@ -102,18 +103,18 @@ func uninstallPlugin(p plugin.Plugin) error {
// Remove tarball file // Remove tarball file
tarballPath := filepath.Join(pluginsDir, versionedBasename) tarballPath := filepath.Join(pluginsDir, versionedBasename)
if _, err := os.Stat(tarballPath); err == nil { 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 { 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 // Remove provenance file
provPath := filepath.Join(pluginsDir, versionedBasename+".prov") provPath := filepath.Join(pluginsDir, versionedBasename+".prov")
if _, err := os.Stat(provPath); err == nil { 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 { 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)
} }
} }
} }

@ -30,10 +30,11 @@ import (
type pluginUpdateOptions struct { type pluginUpdateOptions struct {
names []string names []string
logger *slog.Logger
} }
func newPluginUpdateCmd(out io.Writer) *cobra.Command { func newPluginUpdateCmd(logger *slog.Logger, out io.Writer) *cobra.Command {
o := &pluginUpdateOptions{} o := &pluginUpdateOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update <plugin>...", Use: "update <plugin>...",
@ -61,7 +62,7 @@ func (o *pluginUpdateOptions) complete(args []string) error {
} }
func (o *pluginUpdateOptions) run(out io.Writer) 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) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc)
if err != nil { if err != nil {
return err return err
@ -70,7 +71,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
for _, name := range o.names { for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil { 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)) errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%w)", name, err))
} else { } else {
fmt.Fprintf(out, "Updated plugin: %s\n", name) fmt.Fprintf(out, "Updated plugin: %s\n", name)
@ -85,7 +86,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
return nil return nil
} }
func updatePlugin(p plugin.Plugin) error { func updatePlugin(logger *slog.Logger, p plugin.Plugin) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir()) exactLocation, err := filepath.EvalSymlinks(p.Dir())
if err != nil { if err != nil {
return err return err
@ -103,7 +104,7 @@ func updatePlugin(p plugin.Plugin) error {
return err return err
} }
slog.Debug("loading plugin", "path", i.Path()) logger.Debug("loading plugin", "path", i.Path())
updatedPlugin, err := plugin.LoadDir(i.Path()) updatedPlugin, err := plugin.LoadDir(i.Path())
if err != nil { if err != nil {
return err return err

@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"log/slog"
"github.com/spf13/cobra" "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 { RunE: func(_ *cobra.Command, args []string) error {
client.Settings = settings client.Settings = settings
if client.Version == "" && client.Devel { 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" client.Version = ">0.0.0-0"
} }

@ -63,7 +63,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
hostname := args[0] 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 { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
} }
// Adapted from https://github.com/oras-project/oras // 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 var err error
username := usernameOpt username := usernameOpt
password := passwordOpt password := passwordOpt
@ -128,7 +128,7 @@ func getUsernamePassword(usernameOpt, passwordOpt string, passwordFromStdinOpt b
} }
} }
default: 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 return username, password, nil

@ -98,7 +98,7 @@ func runOCIChartCommand(t *testing.T, argsFn func(ref, registryConfig, contentCa
Capabilities: common.DefaultCapabilities, Capabilities: common.DefaultCapabilities,
} }
root, err := newRootCmdWithConfig(actionConfig, stdout, args, SetupLogging) root, err := newRootCmdWithConfig(actionConfig, stdout, args, NewLogger)
require.NoError(t, err) require.NoError(t, err)
root.SetOut(stdout) root.SetOut(stdout)
root.SetErr(stderr) root.SetErr(stderr)

@ -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) 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 { if err != nil {
return err 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.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") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")
addDryRunFlag(cmd) addDryRunFlag(cmd)
AddWaitFlag(cmd, &client.WaitStrategy) addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -103,9 +103,44 @@ By default, the default directories depend on the Operating System. The defaults
var settings = cli.New() 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) { 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() actionConfig := action.NewConfiguration()
cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup) cmd, err := newRootCmdWithConfig(actionConfig, out, args, newLogger)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -122,6 +157,13 @@ func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Comma
return cmd, nil 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. // SetupLogging sets up Helm logging used by the Helm client.
// This function is passed to the NewRootCmd function to enable logging. Any other // 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 // 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 // enables applications using Helm commands to integrate with their existing logging
// system. // system.
// The debug argument is the value if Helm is set for debugging (i.e. --debug flag) // 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) { func SetupLogging(debug bool) {
logger := logging.NewLogger(func() bool { return debug }) slog.SetDefault(NewLogger(debug))
slog.SetDefault(logger)
} }
// configureColorOutput configures the color output based on the ColorMode setting // 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{ cmd := &cobra.Command{
Use: "helm", Use: "helm",
Short: "The Helm package manager for Kubernetes.", 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.ParseErrorsAllowlist.UnknownFlags = true
flags.Parse(args) flags.Parse(args)
logSetup(settings.Debug) var logger *slog.Logger
if newLogger != nil {
// newRootCmdWithConfig is only called from NewRootCmd. NewRootCmd sets up logger = newLogger(settings.Debug)
// 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 if logger == nil {
// the Helm client. This means the actionConfig logger is different from the slog logger = NewLogger(settings.Debug)
// 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())
} }
// 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 // Validate color mode setting
switch settings.ColorMode { switch settings.ColorMode {
case "never", "auto", "always": case "never", "auto", "always":
@ -272,7 +316,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
newLintCmd(out), newLintCmd(out),
newPackageCmd(out), newPackageCmd(out),
newRepoCmd(out), newRepoCmd(out),
newSearchCmd(out), newSearchCmd(logger, out),
newVerifyCmd(out), newVerifyCmd(out),
// release commands // release commands
@ -289,7 +333,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
newCompletionCmd(out), newCompletionCmd(out),
newEnvCmd(out), newEnvCmd(out),
newPluginCmd(out), newPluginCmd(logger, out),
newVersionCmd(out), newVersionCmd(out),
// Hidden documentation generator command: 'helm docs' // Hidden documentation generator command: 'helm docs'
@ -302,7 +346,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
) )
// Find and add CLI plugins // Find and add CLI plugins
loadCLIPlugins(cmd, out) loadCLIPlugins(cmd, logger, out)
// Check for expired repositories // Check for expired repositories
checkForExpiredRepos(settings.RepositoryConfig) checkForExpiredRepos(settings.RepositoryConfig)

@ -18,6 +18,7 @@ package cmd
import ( import (
"bytes" "bytes"
"io"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@ -26,10 +27,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/logging"
"helm.sh/helm/v4/internal/test/ensure" "helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/helmpath/xdg" "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) { func TestRootCmd(t *testing.T) {
@ -128,10 +132,13 @@ func TestUnknownSubCmd(t *testing.T) {
// } // }
func TestRootCmdLogger(t *testing.T) { func TestRootCmdLogger(t *testing.T) {
origDefault := slog.Default()
t.Cleanup(func() { slog.SetDefault(origDefault) })
args := []string{} args := []string{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
actionConfig := action.NewConfiguration() actionConfig := action.NewConfiguration()
_, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) _, err := newRootCmdWithConfig(actionConfig, buf, args, loggerFromSetup(SetupLogging))
require.NoError(t, err) require.NoError(t, err)
l1 := actionConfig.Logger() 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") 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")
}

@ -18,6 +18,7 @@ package cmd
import ( import (
"io" "io"
"log/slog"
"github.com/spf13/cobra" "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. 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{ cmd := &cobra.Command{
Use: "search [keyword]", Use: "search [keyword]",
Short: "search for a keyword in charts", Short: "search for a keyword in charts",
Long: searchDesc, Long: searchDesc,
} }
cmd.AddCommand(newSearchHubCmd(out)) cmd.AddCommand(newSearchHubCmd(logger, out))
cmd.AddCommand(newSearchRepoCmd(out)) cmd.AddCommand(newSearchRepoCmd(logger, out))
return cmd return cmd
} }

@ -57,10 +57,11 @@ type searchHubOptions struct {
outputFormat output.Format outputFormat output.Format
listRepoURL bool listRepoURL bool
failOnNoResult bool failOnNoResult bool
logger *slog.Logger
} }
func newSearchHubCmd(out io.Writer) *cobra.Command { func newSearchHubCmd(logger *slog.Logger, out io.Writer) *cobra.Command {
o := &searchHubOptions{} o := &searchHubOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "hub [KEYWORD]", Use: "hub [KEYWORD]",
@ -91,7 +92,7 @@ func (o *searchHubOptions) run(ctx context.Context, out io.Writer, args []string
q := strings.Join(args, " ") q := strings.Join(args, " ")
results, err := c.SearchWithContext(ctx, q) results, err := c.SearchWithContext(ctx, q)
if err != nil { 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) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
} }

@ -73,10 +73,11 @@ type searchRepoOptions struct {
repoCacheDir string repoCacheDir string
outputFormat output.Format outputFormat output.Format
failOnNoResult bool failOnNoResult bool
logger *slog.Logger
} }
func newSearchRepoCmd(out io.Writer) *cobra.Command { func newSearchRepoCmd(logger *slog.Logger, out io.Writer) *cobra.Command {
o := &searchRepoOptions{} o := &searchRepoOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "repo [keyword]", Use: "repo [keyword]",
@ -131,17 +132,17 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error {
} }
func (o *searchRepoOptions) setupSearchedVersion() { func (o *searchRepoOptions) setupSearchedVersion() {
slog.Debug("original chart version", "version", o.version) o.logger.Debug("original chart version", "version", o.version)
if o.version != "" { if o.version != "" {
return return
} }
if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). 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" o.version = ">0.0.0-0"
} else { // search only for stable releases, prerelease versions will be skipped } 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" o.version = ">0.0.0"
} }
} }
@ -190,7 +191,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n)) f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f) ind, err := repo.LoadIndexFile(f)
if err != nil { 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 continue
} }

@ -88,7 +88,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(cfg.Logger(), args, client)
if err != nil { if err != nil {
return err return err
} }
@ -109,7 +109,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(cfg.Logger(), args, client)
if err != nil { if err != nil {
return err return err
} }
@ -130,7 +130,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(cfg.Logger(), args, client)
if err != nil { if err != nil {
return err return err
} }
@ -151,7 +151,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(cfg.Logger(), args, client)
if err != nil { if err != nil {
return err return err
} }
@ -172,7 +172,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(cfg.Logger(), args, client)
if err != nil { if err != nil {
return err return err
} }
@ -210,10 +210,10 @@ func addShowFlags(subCmd *cobra.Command, client *action.Show) {
} }
} }
func runShow(args []string, client *action.Show) (string, error) { func runShow(logger *slog.Logger, args []string, client *action.Show) (string, error) {
slog.Debug("original chart version", "version", client.Version) logger.Debug("original chart version", "version", client.Version)
if client.Version == "" && client.Devel { 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" client.Version = ">0.0.0-0"
} }

@ -92,7 +92,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, true)
if err != nil { if err != nil {
return err return err
} }
@ -105,7 +105,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Replace = true // Skip the name check client.Replace = true // Skip the name check
client.APIVersions = common.VersionSet(extraAPIs) client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds 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 err != nil && !settings.Debug {
if rel != nil { if rel != nil {
@ -207,7 +207,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() 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.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.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "deprecated") f.BoolVar(&validate, "validate", false, "deprecated")

@ -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.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.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") f.StringVar(&client.Description, "description", "", "add a custom description")
AddWaitFlag(cmd, &client.WaitStrategy) addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
return cmd return cmd
} }

@ -113,7 +113,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cfg.Logger(), cmd, false)
if err != nil { if err != nil {
return err return err
} }
@ -126,7 +126,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
histClient := action.NewHistory(cfg) histClient := action.NewHistory(cfg)
histClient.Max = 1 histClient.Max = 1
versions, err := histClient.Run(args[0]) 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 // Only print this to stdout for table output
if outfmt == output.Table { if outfmt == output.Table {
fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) 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.ForceConflicts = client.ForceConflicts
instClient.ServerSideApply = client.ServerSideApply != "false" instClient.ServerSideApply = client.ServerSideApply != "false"
if isReleaseUninstalled(versions) { if isReleaseUninstalled(cfg.Logger(), versions) {
instClient.Replace = true instClient.Replace = true
} }
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(cfg.Logger(), args, instClient, valueOpts, out)
if err != nil { if err != nil {
return err return err
} }
@ -180,7 +180,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if client.Version == "" && client.Devel { 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" client.Version = ">0.0.0-0"
} }
@ -239,7 +239,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if ac.Deprecated() { if ac.Deprecated() {
slog.Warn("this chart is deprecated") cfg.Logger().Warn("this chart is deprecated")
} }
// Create context and prepare the handle of SIGTERM // 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) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings) bindPostRenderFlag(cmd, &client.PostRenderer, settings)
AddWaitFlag(cmd, &client.WaitStrategy) addWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
@ -329,10 +329,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd return cmd
} }
func isReleaseUninstalled(versionsi []ri.Releaser) bool { func isReleaseUninstalled(logger *slog.Logger, versionsi []ri.Releaser) bool {
versions, err := releaseListToV1List(versionsi) versions, err := releaseListToV1List(versionsi)
if err != nil { 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 false
} }
return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled

Loading…
Cancel
Save