Files
Copilotandyxxhero 5b7cbe54e9 Add --sequential-helmfiles flag for optional sequential processing (#2350)
* Initial plan

* Fix helmfile.d parallel processing to respect alphabetical order

Changed multiple helmfile.d file processing from parallel to sequential
to match the documented behavior. Files are now processed in alphabetical
order as stated in the documentation, ensuring predictable deployment order.

This fixes issues where deployments would break because parallel processing
did not respect the intended ordering (e.g., databases before applications).

- Modified visitStatesWithContext to process files sequentially
- Removed unused processStateFileParallel function
- Updated test snapshots to reflect deterministic ordering

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Add --sequential-helmfiles flag for opt-in sequential processing

Based on feedback, added a flag to control processing mode instead of
forcing sequential behavior. Parallel processing is now the default for
performance, with an opt-in --sequential-helmfiles flag for when order matters.

Changes:
- Added SequentialHelmfiles field to GlobalOptions and App
- Added --sequential-helmfiles CLI flag
- Modified visitStatesWithContext to check flag and choose parallel vs sequential
- Updated documentation to clarify parallel is default, sequential is opt-in
- Restored original parallel processing as default behavior

This gives users control over the behavior based on their needs.

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Add documentation and improve code readability

- Added documentation to processStateFileParallel function
- Extracted shouldProcessInParallel variable for clarity
- All tests pass, security scan clean

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix lint issue: remove trailing whitespace

Removed trailing whitespace from line 983 in pkg/app/app.go to fix
formatting lint error.

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2026-01-08 14:28:25 -05:00

287 lines
8.1 KiB
Go

package config
import (
"errors"
"fmt"
"io"
"os"
"go.uber.org/zap"
"golang.org/x/term"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/maputil"
"github.com/helmfile/helmfile/pkg/state"
)
// GlobalOptions is the global configuration for the Helmfile CLI.
type GlobalOptions struct {
// HelmBinary is the path to the Helm binary.
HelmBinary string
// KustomizeBinary is the path to the Kustomize binary.
KustomizeBinary string
// File is the path to the Helmfile.
File string
// Environment is the name of the environment to use.
Environment string
// StateValuesSet is a list of state values to set on the command line.
StateValuesSet []string
// StateValuesSetString is a list of state values to set on the command line.
StateValuesSetString []string
// StateValuesFiles is a list of state values files to use.
StateValuesFile []string
// SkipDeps is true if the running "helm repo update" and "helm dependency build" should be skipped
SkipDeps bool
// SkipRefresh is true if the running "helm repo update" should be skipped
SkipRefresh bool
// StripArgsValuesOnExitError is true if the ARGS output on exit error should be suppressed
StripArgsValuesOnExitError bool
// DisableForceUpdate is true if force updating repos is not desirable when executing "helm repo add" (Helm 3)
DisableForceUpdate bool
// EnforcePluginVerification is true if plugin installation should fail when verification is not supported
EnforcePluginVerification bool
// HelmOCIPlainHTTP is true if Helm should use plain HTTP for OCI registries
HelmOCIPlainHTTP bool
// Quiet is true if the output should be quiet.
Quiet bool
// Kubeconfig is the path to the kubeconfig file to use.
Kubeconfig string
// KubeContext is the name of the kubectl context to use.
KubeContext string
// Debug is true if the output should be verbose.
Debug bool
// Color is true if the output should be colorized.
Color bool
// NoColor is true if the output should not be colorized.
NoColor bool
// LogLevel is the log level to use.
LogLevel string
// Namespace is the namespace to use.
Namespace string
// Chart is the chart to use.
Chart string
// Selector is a list of selectors to use.
Selector []string
// AllowNoMatchingRelease is not exit with an error code if the provided selector has no matching releases.
AllowNoMatchingRelease bool
// logger is the logger to use.
logger *zap.SugaredLogger
// EnableLiveOutput enables live output from the Helm binary stdout/stderr into Helmfile own stdout/stderr
EnableLiveOutput bool
// Interactive is true if the user should be prompted for input.
Interactive bool
// Args is the list of arguments to pass to the Helm binary.
Args string
// LogOutput is the writer to use for writing logs.
LogOutput io.Writer
// SequentialHelmfiles is true if helmfile.d files should be processed sequentially instead of in parallel.
SequentialHelmfiles bool
}
// Logger returns the logger to use.
func (g *GlobalOptions) Logger() *zap.SugaredLogger {
return g.logger
}
// GetLogLevel returns the log level to use.
func (g *GlobalOptions) SetLogger(logger *zap.SugaredLogger) {
g.logger = logger
}
// GlobalImpl is the global configuration for the Helmfile CLI.
type GlobalImpl struct {
*GlobalOptions
set map[string]any
}
// NewGlobalImpl creates a new GlobalImpl.
func NewGlobalImpl(opts *GlobalOptions) *GlobalImpl {
return &GlobalImpl{
GlobalOptions: opts,
set: make(map[string]any),
}
}
// Setset sets the set
func (g *GlobalImpl) SetSet(set map[string]any) {
g.set = maputil.MergeMaps(g.set, set)
}
// HelmBinary returns the path to the Helm binary.
func (g *GlobalImpl) HelmBinary() string {
return g.GlobalOptions.HelmBinary
}
// KustomizeBinary returns the path to the Kustomize binary.
func (g *GlobalImpl) KustomizeBinary() string {
return g.GlobalOptions.KustomizeBinary
}
// Kubeconfig returns the path to the kubeconfig file to use.
func (g *GlobalImpl) Kubeconfig() string {
return g.GlobalOptions.Kubeconfig
}
// KubeContext returns the name of the kubectl context to use.
func (g *GlobalImpl) KubeContext() string {
return g.GlobalOptions.KubeContext
}
// Namespace returns the namespace to use.
func (g *GlobalImpl) Namespace() string {
return g.GlobalOptions.Namespace
}
// Chart returns the chart to use.
func (g *GlobalImpl) Chart() string {
return g.GlobalOptions.Chart
}
// FileOrDir returns the path to the Helmfile.
func (g *GlobalImpl) FileOrDir() string {
file := g.File
if file == "" {
file = os.Getenv(envvar.FilePath)
}
return file
}
// Selectors returns the selectors to use.
func (g *GlobalImpl) Selectors() []string {
return g.Selector
}
// StateValuesSet returns the set
func (g *GlobalImpl) StateValuesSet() map[string]any {
return g.set
}
// StateValuesSet returns the set
func (g *GlobalImpl) RawStateValuesSet() []string {
return g.GlobalOptions.StateValuesSet
}
// RawStateValuesSetString returns the set
func (g *GlobalImpl) RawStateValuesSetString() []string {
return g.StateValuesSetString
}
// StateValuesFiles returns the state values files
func (g *GlobalImpl) StateValuesFiles() []string {
return g.StateValuesFile
}
// EnableLiveOutput return when to pipe the stdout and stderr from Helm live to the helmfile stdout
func (g *GlobalImpl) EnableLiveOutput() bool {
return g.GlobalOptions.EnableLiveOutput
}
// SkipDeps return if running "helm repo update" and "helm dependency build" should be skipped
func (g *GlobalImpl) SkipDeps() bool {
return g.GlobalOptions.SkipDeps
}
// SkipRefresh return if running "helm repo update"
func (g *GlobalImpl) SkipRefresh() bool {
return g.GlobalOptions.SkipRefresh
}
// StripArgsValuesOnExitError return if the ARGS output on exit error should be suppressed
func (g *GlobalImpl) StripArgsValuesOnExitError() bool {
return g.GlobalOptions.StripArgsValuesOnExitError
}
// DisableForceUpdate return when to disable forcing updates to repos upon adding (Helm 3)
func (g *GlobalImpl) DisableForceUpdate() bool {
return g.GlobalOptions.DisableForceUpdate
}
// EnforcePluginVerification return when to enforce plugin verification
func (g *GlobalImpl) EnforcePluginVerification() bool {
return g.GlobalOptions.EnforcePluginVerification
}
// HelmOCIPlainHTTP returns whether to use plain HTTP for OCI registries
func (g *GlobalImpl) HelmOCIPlainHTTP() bool {
return g.GlobalOptions.HelmOCIPlainHTTP
}
// SequentialHelmfiles returns whether to process helmfile.d files sequentially
func (g *GlobalImpl) SequentialHelmfiles() bool {
return g.GlobalOptions.SequentialHelmfiles
}
// Logger returns the logger
func (g *GlobalImpl) Logger() *zap.SugaredLogger {
return g.logger
}
func (g *GlobalImpl) Color() bool {
if c := g.GlobalOptions.Color; c {
return c
}
if g.GlobalOptions.NoColor {
return false
}
// We replicate the helm-diff behavior in helmfile
// because when helmfile calls helm-diff, helm-diff has no access to term and therefore
// we can't rely on helm-diff's ability to auto-detect term for color output.
// See https://github.com/roboll/helmfile/issues/2043
terminal := term.IsTerminal(int(os.Stdout.Fd()))
// https://github.com/databus23/helm-diff/issues/281
dumb := os.Getenv("TERM") == "dumb"
return terminal && !dumb
}
// NoColor returns the no color flag
func (g *GlobalImpl) NoColor() bool {
return g.GlobalOptions.NoColor
}
// Env returns the environment to use.
func (g *GlobalImpl) Env() string {
var env string
switch {
case g.Environment != "":
env = g.Environment
case os.Getenv("HELMFILE_ENVIRONMENT") != "":
env = os.Getenv("HELMFILE_ENVIRONMENT")
default:
env = state.DefaultEnv
}
return env
}
// ValidateConfig validates the global options.
func (g *GlobalImpl) ValidateConfig() error {
if g.NoColor() && g.Color() {
return errors.New("--color and --no-color cannot be specified at the same time")
}
return nil
}
// Interactive returns the Interactive
func (g *GlobalImpl) Interactive() bool {
if g.GlobalOptions.Interactive {
return true
}
return os.Getenv(envvar.Interactive) == "true"
}
// Args returns the args to use for helm
func (g *GlobalImpl) Args() string {
args := g.GlobalOptions.Args
enableHelmDebug := g.Debug
if enableHelmDebug {
args = fmt.Sprintf("%s %s", args, "--debug")
}
return args
}