diff --git a/cmd/root.go b/cmd/root.go index c47f8688..6089aa8a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,6 +48,8 @@ func toCLIError(g *config.GlobalImpl, err error) error { // NewRootCmd creates the root command for the CLI. func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { + globalImpl := config.NewGlobalImpl(globalConfig) + cmd := &cobra.Command{ Use: "helmfile", Short: globalUsage, @@ -58,11 +60,11 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { PersistentPreRunE: func(c *cobra.Command, args []string) error { // Valid levels: // https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126 - logLevel := globalConfig.LogLevel + logLevel := globalImpl.LogLevel() switch { - case globalConfig.Debug: + case globalImpl.Debug(): logLevel = "debug" - case globalConfig.Quiet: + case globalImpl.Quiet(): logLevel = "warn" } @@ -83,8 +85,6 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { flags.ParseErrorsAllowlist.UnknownFlags = true - globalImpl := config.NewGlobalImpl(globalConfig) - // when set environment HELMFILE_UPGRADE_NOTICE_DISABLED any value, skip upgrade notice. var versionOpts []extension.CobraOption if os.Getenv(envvar.UpgradeNoticeDisabled) == "" { @@ -121,8 +121,8 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { } func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) { - fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", app.DefaultHelmBinary, "Path to the helm binary") - fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", app.DefaultKustomizeBinary, "Path to the kustomize binary") + fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", "", fmt.Sprintf(`Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default %q)`, app.DefaultHelmBinary)) + fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", "", fmt.Sprintf(`Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default %q)`, app.DefaultKustomizeBinary)) fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to \"`helmfile.yaml`\" or \"helmfile.yaml.gotmpl\" or \"helmfile.d\" (means \"helmfile.d/*.yaml\" or \"helmfile.d/*.yaml.gotmpl\") in this preference. Specify - to load the config from the standard input.") fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"`) fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).") @@ -134,13 +134,13 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO fs.BoolVar(&globalOptions.DisableForceUpdate, "disable-force-update", false, `do not force helm repos to update when executing "helm repo add" (Helm 3 only)`) fs.BoolVar(&globalOptions.EnforcePluginVerification, "enforce-plugin-verification", false, `fail plugin installation if verification is not supported (for security purposes)`) fs.BoolVar(&globalOptions.HelmOCIPlainHTTP, "oci-plain-http", false, `use plain HTTP for OCI registries (required for local/insecure registries in Helm 4)`) - fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "Silence output. Equivalent to log-level warn") + fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, `Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified`) fs.StringVar(&globalOptions.Kubeconfig, "kubeconfig", "", "Use a particular kubeconfig file") fs.StringVar(&globalOptions.KubeContext, "kube-context", "", `Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default`) - fs.BoolVar(&globalOptions.Debug, "debug", false, "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect") + fs.BoolVar(&globalOptions.Debug, "debug", false, `Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect. Overrides "HELMFILE_DEBUG" OS environment variable when specified`) fs.BoolVar(&globalOptions.Color, "color", false, "Output with color") - fs.BoolVar(&globalOptions.NoColor, "no-color", false, "Output without color") - fs.StringVar(&globalOptions.LogLevel, "log-level", "info", "Set log level, default info") + fs.BoolVar(&globalOptions.NoColor, "no-color", false, `Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified`) + fs.StringVar(&globalOptions.LogLevel, "log-level", "", `Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")`) fs.StringVarP(&globalOptions.Namespace, "namespace", "n", "", `Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}`) fs.StringVarP(&globalOptions.Chart, "chart", "c", "", "Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}") fs.StringArrayVarP(&globalOptions.Selector, "selector", "l", nil, `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar. diff --git a/pkg/config/global.go b/pkg/config/global.go index d96655a5..7c42a37e 100644 --- a/pkg/config/global.go +++ b/pkg/config/global.go @@ -109,12 +109,63 @@ func (g *GlobalImpl) SetSet(set map[string]any) { // HelmBinary returns the path to the Helm binary. func (g *GlobalImpl) HelmBinary() string { - return g.GlobalOptions.HelmBinary + var helmBinary string + + switch { + case g.GlobalOptions.HelmBinary != "": + helmBinary = g.GlobalOptions.HelmBinary + case os.Getenv("HELMFILE_HELM_BINARY") != "": + helmBinary = os.Getenv("HELMFILE_HELM_BINARY") + default: + helmBinary = state.DefaultHelmBinary + } + return helmBinary } // KustomizeBinary returns the path to the Kustomize binary. func (g *GlobalImpl) KustomizeBinary() string { - return g.GlobalOptions.KustomizeBinary + var kustomizeBinary string + + switch { + case g.GlobalOptions.KustomizeBinary != "": + kustomizeBinary = g.GlobalOptions.KustomizeBinary + case os.Getenv("HELMFILE_KUSTOMIZE_BINARY") != "": + kustomizeBinary = os.Getenv("HELMFILE_KUSTOMIZE_BINARY") + default: + kustomizeBinary = state.DefaultKustomizeBinary + } + return kustomizeBinary +} + +// LogLevel returns the log level to use. +func (g *GlobalImpl) LogLevel() string { + var logLevel string + + switch { + case g.GlobalOptions.LogLevel != "": + logLevel = g.GlobalOptions.LogLevel + case os.Getenv("HELMFILE_LOG_LEVEL") != "": + logLevel = os.Getenv("HELMFILE_LOG_LEVEL") + default: + logLevel = "info" + } + return logLevel +} + +// Debug returns whether debug output is enabled. +func (g *GlobalImpl) Debug() bool { + if g.GlobalOptions.Debug { + return true + } + return os.Getenv(envvar.Debug) == "true" +} + +// Quiet returns whether quiet output is enabled. +func (g *GlobalImpl) Quiet() bool { + if g.GlobalOptions.Quiet { + return true + } + return os.Getenv(envvar.Quiet) == "true" } // Kubeconfig returns the path to the kubeconfig file to use. @@ -259,7 +310,14 @@ func (g *GlobalImpl) Color() bool { // NoColor returns the no color flag func (g *GlobalImpl) NoColor() bool { - return g.GlobalOptions.NoColor + if g.GlobalOptions.NoColor { + return true + } + if os.Getenv(envvar.NoColor) == "true" { + return true + } + // Honor the de-facto https://no-color.org/ standard: any non-empty value disables color. + return os.Getenv("NO_COLOR") != "" } // Env returns the environment to use. @@ -296,7 +354,7 @@ func (g *GlobalImpl) Interactive() bool { // Args returns the args to use for helm func (g *GlobalImpl) Args() string { args := g.GlobalOptions.Args - enableHelmDebug := g.Debug + enableHelmDebug := g.Debug() if enableHelmDebug { args = fmt.Sprintf("%s %s", args, "--debug") diff --git a/pkg/config/global_test.go b/pkg/config/global_test.go index e5b91f4c..a1aeb923 100644 --- a/pkg/config/global_test.go +++ b/pkg/config/global_test.go @@ -119,3 +119,254 @@ func TestNamespace(t *testing.T) { } os.Unsetenv(envvar.Namespace) } + +// TestHelmBinary tests the helm-binary flag and HELMFILE_HELM_BINARY env var fallback +func TestHelmBinary(t *testing.T) { + tests := []struct { + opts GlobalOptions + env string + expected string + }{ + { + opts: GlobalOptions{}, + env: "", + expected: "helm", + }, + { + opts: GlobalOptions{}, + env: "envset", + expected: "envset", + }, + { + opts: GlobalOptions{HelmBinary: "flagset"}, + env: "", + expected: "flagset", + }, + { + opts: GlobalOptions{HelmBinary: "flagset"}, + env: "envset", + expected: "flagset", + }, + } + + for _, test := range tests { + os.Setenv(envvar.HelmBinary, test.env) + received := NewGlobalImpl(&test.opts).HelmBinary() + require.Equalf(t, test.expected, received, "HelmBinary expected %s, received %s", test.expected, received) + } + os.Unsetenv(envvar.HelmBinary) +} + +// TestKustomizeBinary tests the kustomize-binary flag and HELMFILE_KUSTOMIZE_BINARY env var fallback +func TestKustomizeBinary(t *testing.T) { + tests := []struct { + opts GlobalOptions + env string + expected string + }{ + { + opts: GlobalOptions{}, + env: "", + expected: "kustomize", + }, + { + opts: GlobalOptions{}, + env: "envset", + expected: "envset", + }, + { + opts: GlobalOptions{KustomizeBinary: "flagset"}, + env: "", + expected: "flagset", + }, + { + opts: GlobalOptions{KustomizeBinary: "flagset"}, + env: "envset", + expected: "flagset", + }, + } + + for _, test := range tests { + os.Setenv(envvar.KustomizeBinary, test.env) + received := NewGlobalImpl(&test.opts).KustomizeBinary() + require.Equalf(t, test.expected, received, "KustomizeBinary expected %s, received %s", test.expected, received) + } + os.Unsetenv(envvar.KustomizeBinary) +} + +// TestLogLevel tests the log-level flag and HELMFILE_LOG_LEVEL env var fallback +func TestLogLevel(t *testing.T) { + tests := []struct { + opts GlobalOptions + env string + expected string + }{ + { + opts: GlobalOptions{}, + env: "", + expected: "info", + }, + { + opts: GlobalOptions{}, + env: "envset", + expected: "envset", + }, + { + opts: GlobalOptions{LogLevel: "flagset"}, + env: "", + expected: "flagset", + }, + { + opts: GlobalOptions{LogLevel: "flagset"}, + env: "envset", + expected: "flagset", + }, + } + + for _, test := range tests { + os.Setenv(envvar.LogLevel, test.env) + received := NewGlobalImpl(&test.opts).LogLevel() + require.Equalf(t, test.expected, received, "LogLevel expected %s, received %s", test.expected, received) + } + os.Unsetenv(envvar.LogLevel) +} + +// TestDebug tests the debug flag and HELMFILE_DEBUG env var fallback +func TestDebug(t *testing.T) { + tests := []struct { + opts GlobalOptions + env string + expected bool + }{ + { + opts: GlobalOptions{}, + env: "", + expected: false, + }, + { + opts: GlobalOptions{}, + env: "true", + expected: true, + }, + { + opts: GlobalOptions{}, + env: "anything", + expected: false, + }, + { + opts: GlobalOptions{Debug: true}, + env: "", + expected: true, + }, + { + opts: GlobalOptions{Debug: true}, + env: "true", + expected: true, + }, + } + + for _, test := range tests { + os.Setenv(envvar.Debug, test.env) + received := NewGlobalImpl(&test.opts).Debug() + require.Equalf(t, test.expected, received, "Debug expected %t, received %t", test.expected, received) + } + os.Unsetenv(envvar.Debug) +} + +// TestQuiet tests the quiet flag and HELMFILE_QUIET env var fallback +func TestQuiet(t *testing.T) { + tests := []struct { + opts GlobalOptions + env string + expected bool + }{ + { + opts: GlobalOptions{}, + env: "", + expected: false, + }, + { + opts: GlobalOptions{}, + env: "true", + expected: true, + }, + { + opts: GlobalOptions{}, + env: "anything", + expected: false, + }, + { + opts: GlobalOptions{Quiet: true}, + env: "", + expected: true, + }, + { + opts: GlobalOptions{Quiet: true}, + env: "true", + expected: true, + }, + } + + for _, test := range tests { + os.Setenv(envvar.Quiet, test.env) + received := NewGlobalImpl(&test.opts).Quiet() + require.Equalf(t, test.expected, received, "Quiet expected %t, received %t", test.expected, received) + } + os.Unsetenv(envvar.Quiet) +} + +// TestNoColor tests the no-color flag, HELMFILE_NO_COLOR and NO_COLOR env var fallbacks +func TestNoColor(t *testing.T) { + tests := []struct { + opts GlobalOptions + helmfileEnv string + standardEnv string + expected bool + }{ + { + opts: GlobalOptions{}, + helmfileEnv: "", + standardEnv: "", + expected: false, + }, + { + opts: GlobalOptions{}, + helmfileEnv: "true", + standardEnv: "", + expected: true, + }, + { + opts: GlobalOptions{}, + helmfileEnv: "anything", + standardEnv: "", + expected: false, + }, + { + opts: GlobalOptions{}, + helmfileEnv: "", + standardEnv: "1", + expected: true, + }, + { + opts: GlobalOptions{}, + helmfileEnv: "", + standardEnv: "anything", + expected: true, + }, + { + opts: GlobalOptions{NoColor: true}, + helmfileEnv: "", + standardEnv: "", + expected: true, + }, + } + + for _, test := range tests { + os.Setenv(envvar.NoColor, test.helmfileEnv) + os.Setenv("NO_COLOR", test.standardEnv) + received := NewGlobalImpl(&test.opts).NoColor() + require.Equalf(t, test.expected, received, "NoColor expected %t, received %t", test.expected, received) + } + os.Unsetenv(envvar.NoColor) + os.Unsetenv("NO_COLOR") +} diff --git a/pkg/envvar/const.go b/pkg/envvar/const.go index d6f1575f..7a2e4542 100644 --- a/pkg/envvar/const.go +++ b/pkg/envvar/const.go @@ -11,6 +11,12 @@ const ( Environment = "HELMFILE_ENVIRONMENT" KubeContext = "HELMFILE_KUBE_CONTEXT" Namespace = "HELMFILE_NAMESPACE" + HelmBinary = "HELMFILE_HELM_BINARY" + KustomizeBinary = "HELMFILE_KUSTOMIZE_BINARY" + LogLevel = "HELMFILE_LOG_LEVEL" + Debug = "HELMFILE_DEBUG" + Quiet = "HELMFILE_QUIET" + NoColor = "HELMFILE_NO_COLOR" FilePath = "HELMFILE_FILE_PATH" TempDir = "HELMFILE_TEMPDIR" UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"