Files
helmfile/cmd/root.go
T
Aditya Menon c63947483c fix: eliminate os.Chdir in sequential helmfiles to fix relative path resolution (#2410)
* fix: eliminate os.Chdir in sequential helmfiles to fix relative path resolution

The sequential code path used within() → os.Chdir() to change the
process-wide working directory when processing helmfile.d files.
This broke relative environment variable paths (e.g. KUBECONFIG=kubeconfig.yaml)
because they resolved from the wrong directory after chdir.

Replace the chdir-based approach with the same baseDir parameter pattern
used by the parallel code path, passing explicit directory context through
loadDesiredStateFromYamlWithBaseDir() instead of mutating global process state.

Closes #2409

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: restore within() for single-file sequential to preserve chart path format

The previous approach used baseDir for all sequential processing, which
changed chart path format in output (e.g. from "../../../../charts/raw"
to "test/integration/charts/raw"). This broke integration tests that
compare chart paths in expected output.

Now the sequential branch uses two strategies:
- Single file: use os.Chdir via within() to preserve backward-compatible
  relative chart paths in output
- Multiple files with --sequential-helmfiles: use baseDir parameter to
  avoid os.Chdir, fixing relative env var paths like KUBECONFIG (#2409)

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: revert e2e snapshot outputs to match within() behavior

The previous commit restored within() for single-file sequential
processing, which produces relative chart paths (e.g. ../../charts/raw)
and filename-only FilePath. Revert the e2e snapshot expected outputs
to match main branch since single-file behavior is now identical.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: restructure integration test for multi-file sequential processing

- Point -f at helmfile.d/ directly (not parent dir) so findDesiredStateFiles
  discovers the yaml files
- Add second helmfile to trigger baseDir path (len > 1)
- Inline environment config to avoid base file relative path issues
- Verify both releases appear in output instead of comparing with parallel
  (which may differ in ordering)

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: reduce cognitive complexity and improve accuracy of sequential helmfiles

Replace inline visitSubHelmfiles closure with calls to the existing
processNestedHelmfiles() method, matching the parallel path. This
eliminates duplicated nested logic and reduces gocognit complexity
below the CI threshold of 110. Also fixes help text and docs to
accurately describe that single-file processing still uses within(),
and adds kubeContext verification to the integration test.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* test: validate kubeContext resolution in sequential helmfiles integration test

Restructure the integration test to replicate the exact user scenario
from issue #2409:
  - Multiple files in helmfile.d/ using bases: with relative paths
    (../bases/) for environments and defaults
  - Environment values set kubeContext via .Environment.Values
  - helmDefaults.kubeContext rendered from gotmpl
  - Local chart references (../../../../charts/raw) from helmfile.d/
  - Run diff against the minikube cluster to exercise kubeContext
    resolution, which would fail with "context does not exist" if
    os.Chdir() broke relative path resolution
  - Also verify template output for both releases and relative values
    file (values/common.yaml) resolution

Fix normalizeChart() in util.go to be idempotent — skip re-prefixing
when the chart path already starts with basePath. This prevents
double-prefixing of local chart paths (e.g. helmfile.d/test/.../raw)
when normalizeChart is called multiple times (once during chart
preparation and again during diff/sync).

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

---------

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
2026-02-22 09:21:46 +08:00

160 lines
8.3 KiB
Go

package cmd
import (
stderrors "errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"go.szostok.io/version/extension"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/app"
"github.com/helmfile/helmfile/pkg/app/version"
"github.com/helmfile/helmfile/pkg/config"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/errors"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/runtime"
)
var logger *zap.SugaredLogger
var globalUsage = "Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot\n" + runtime.Info()
func toCLIError(g *config.GlobalImpl, err error) error {
if err != nil {
var exitErr helmexec.ExitError
if stderrors.As(err, &exitErr) {
return errors.NewExitError(exitErr.Error(), exitErr.ExitStatus())
}
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
noMatchingExitCode := 3
if g.AllowNoMatchingRelease {
noMatchingExitCode = 0
}
return errors.NewExitError(e.Error(), noMatchingExitCode)
case *app.MultiError:
return errors.NewExitError(e.Error(), 1)
case *app.Error:
return errors.NewExitError(e.Error(), e.Code())
default:
return errors.NewExitError(fmt.Sprintf("unexpected error: %T: %v", e, e), 1)
}
}
return err
}
// NewRootCmd creates the root command for the CLI.
func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "helmfile",
Short: globalUsage,
Long: globalUsage,
Version: version.Version(),
SilenceUsage: true,
SilenceErrors: true,
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
switch {
case globalConfig.Debug:
logLevel = "debug"
case globalConfig.Quiet:
logLevel = "warn"
}
// If the log output is not set, default to stderr.
logOut := globalConfig.LogOutput
if logOut == nil {
logOut = os.Stderr
}
logger = helmexec.NewLogger(logOut, logLevel)
globalConfig.SetLogger(logger)
return nil
},
}
flags := cmd.PersistentFlags()
// Set the global options for the root command.
setGlobalOptionsForRootCmd(flags, globalConfig)
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) == "" {
versionOpts = append(versionOpts, extension.WithUpgradeNotice("helmfile", "helmfile"))
}
cmd.AddCommand(
NewInitCmd(globalImpl),
NewApplyCmd(globalImpl),
NewBuildCmd(globalImpl),
NewCacheCmd(globalImpl),
NewDepsCmd(globalImpl),
NewDestroyCmd(globalImpl),
NewFetchCmd(globalImpl),
NewListCmd(globalImpl),
NewReposCmd(globalImpl),
NewLintCmd(globalImpl),
NewWriteValuesCmd(globalImpl),
NewTestCmd(globalImpl),
NewUnittestCmd(globalImpl),
NewTemplateCmd(globalImpl),
NewSyncCmd(globalImpl),
NewDiffCmd(globalImpl),
NewStatusCmd(globalImpl),
NewShowDAGCmd(globalImpl),
NewPrintEnvCmd(globalImpl),
extension.NewVersionCobraCmd(
versionOpts...,
),
)
return cmd, nil
}
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.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).")
fs.StringArrayVar(&globalOptions.StateValuesSetString, "state-values-set-string", nil, "set state STRING 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).")
fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file. Used to override .Values within the helmfile template (not values template).")
fs.BoolVar(&globalOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`)
fs.BoolVar(&globalOptions.SkipRefresh, "skip-refresh", false, `skip running "helm repo update"`)
fs.BoolVar(&globalOptions.StripArgsValuesOnExitError, "strip-args-values-on-exit-error", true, `Strip the potential secret values of the helm command args contained in a helmfile error message`)
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.StringVar(&globalOptions.Kubeconfig, "kubeconfig", "", "Use a particular kubeconfig file")
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", "Set kubectl context. Uses current 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.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.StringVarP(&globalOptions.Namespace, "namespace", "n", "", "Set namespace. 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.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
"--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label: "--selector name=myrelease"`)
fs.BoolVar(&globalOptions.AllowNoMatchingRelease, "allow-no-matching-release", false, `Do not exit with an error code if the provided selector has no matching releases.`)
fs.BoolVar(&globalOptions.EnableLiveOutput, "enable-live-output", globalOptions.EnableLiveOutput, `Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr.
It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still displayed only when it's execution finishes.`)
fs.BoolVarP(&globalOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters")
fs.BoolVar(&globalOptions.SequentialHelmfiles, "sequential-helmfiles", false, `Process helmfile.d files sequentially in alphabetical order instead of in parallel.
Useful when file order matters for dependencies (e.g., databases before applications).
When processing multiple files, paths are resolved without changing the process working directory,
so relative environment variables like KUBECONFIG work correctly.`)
// avoid 'pflag: help requested' error (#251)
fs.BoolP("help", "h", false, "help for helmfile")
}