mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 07:51:01 +02:00
feat: support HELMFILE_* env vars for more global flags (#2606)
* feat: support more HELMFILE_* env vars as flag fallbacks
Adds env-var fallbacks for global flags, mirroring the existing
HELMFILE_ENVIRONMENT / HELMFILE_KUBE_CONTEXT pattern:
* --helm-binary -> HELMFILE_HELM_BINARY
* --kustomize-binary -> HELMFILE_KUSTOMIZE_BINARY
* --log-level -> HELMFILE_LOG_LEVEL
* --debug -> HELMFILE_DEBUG (expecting "true" lower case)
* --quiet -> HELMFILE_QUIET (expecting "true" lower case)
* --no-color -> HELMFILE_NO_COLOR (expecting "true" lower case),
additionally honors NO_COLOR per no-color.org
(any non-empty value disables color)
Flag values still take precedence; env vars are consulted only when the
flag is unset. The string-flag default values ("helm", "kustomize",
"info") move into the accessor methods so the env-var fallback can
actually trigger when no flag is passed.
Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
* docs: mention new HELMFILE_* env vars in cli.md and templating.md
Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
* fix: make Color/NoColor/env interaction consistent
Two issues with the env-aware NoColor() introduced together with
HELMFILE_NO_COLOR / NO_COLOR support:
1. Color() consulted the raw GlobalOptions.NoColor field instead of
NoColor(), so in a TTY with only the env set, Color() fell through
to terminal autodetect and ValidateConfig() spuriously errored with
"--color and --no-color cannot be specified at the same time".
2. NoColor() returned true via env even when --color was explicitly
passed, so `helmfile --color` with NO_COLOR (or HELMFILE_NO_COLOR=true)
in the environment hit the same ValidateConfig() error. A flag should
always win over an env var.
Fix both by routing Color() through NoColor() and giving NoColor() an
explicit --color short-circuit. Regression tests added for both paths.
Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
---------
Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
This commit is contained in:
+67
-5
@@ -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.
|
||||
@@ -242,7 +293,7 @@ func (g *GlobalImpl) Color() bool {
|
||||
return c
|
||||
}
|
||||
|
||||
if g.GlobalOptions.NoColor {
|
||||
if g.NoColor() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -259,7 +310,18 @@ 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
|
||||
}
|
||||
// Explicit --color short-circuits env-derived no-color: a flag must win over an env var.
|
||||
if g.GlobalOptions.Color {
|
||||
return false
|
||||
}
|
||||
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 +358,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")
|
||||
|
||||
@@ -119,3 +119,304 @@ 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")
|
||||
}
|
||||
|
||||
// TestColorRespectsNoColorEnv guards against ValidateConfig() firing when
|
||||
// HELMFILE_NO_COLOR / NO_COLOR is set without an explicit --color/--no-color flag.
|
||||
// Color() must consult NoColor() (which is env-aware) before falling back to TTY autodetect.
|
||||
func TestColorRespectsNoColorEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
helmfileEnv string
|
||||
standardEnv string
|
||||
}{
|
||||
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
|
||||
{name: "NO_COLOR set", standardEnv: "1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(envvar.NoColor, test.helmfileEnv)
|
||||
t.Setenv("NO_COLOR", test.standardEnv)
|
||||
g := NewGlobalImpl(&GlobalOptions{})
|
||||
require.True(t, g.NoColor(), "NoColor() should be true when env is set")
|
||||
require.False(t, g.Color(), "Color() should be false when NoColor() is true via env")
|
||||
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error from env-only no-color")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestColorFlagOverridesNoColorEnv guards against ValidateConfig() firing when
|
||||
// --color is explicitly passed but HELMFILE_NO_COLOR / NO_COLOR is set in the
|
||||
// environment. The flag must win over the env var.
|
||||
func TestColorFlagOverridesNoColorEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
helmfileEnv string
|
||||
standardEnv string
|
||||
}{
|
||||
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
|
||||
{name: "NO_COLOR set", standardEnv: "1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(envvar.NoColor, test.helmfileEnv)
|
||||
t.Setenv("NO_COLOR", test.standardEnv)
|
||||
g := NewGlobalImpl(&GlobalOptions{Color: true})
|
||||
require.True(t, g.Color(), "Color() should be true when --color is set")
|
||||
require.False(t, g.NoColor(), "NoColor() should be false when --color is set, even if env says otherwise")
|
||||
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error when --color overrides env no-color")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user