diff --git a/cmd/root.go b/cmd/root.go index bb4dd64f0..4cd8bd00a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -122,6 +122,7 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO 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.AllowPartialErrors, "allow-partial-errors", false, `allow partial errors during release processing, will continue and report individual releases that failed and continue to install the others`) 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"`) fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "Silence output. Equivalent to log-level warn") diff --git a/go.mod b/go.mod index c9214a5d3..ea0e54d61 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( go.yaml.in/yaml/v2 v2.4.3 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/sync v0.17.0 - golang.org/x/term v0.36.0 + golang.org/x/term v0.37.0 helm.sh/helm/v3 v3.19.0 k8s.io/apimachinery v0.34.1 ) @@ -93,7 +93,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.13.0 // indirect google.golang.org/api v0.252.0 // indirect diff --git a/go.sum b/go.sum index 9e471bb4e..04933dce5 100644 --- a/go.sum +++ b/go.sum @@ -829,14 +829,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/pkg/app/app.go b/pkg/app/app.go index 0da37450b..e85bd01fd 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -154,10 +154,11 @@ func (a *App) Diff(c DiffConfigProvider) error { includeCRDs := !c.SkipCRDs() - prepErr := run.withPreparedCharts("diff", state.ChartPrepareOptions{ + prepErr := run.WithPreparedCharts("diff", state.ChartPrepareOptions{ SkipRepos: c.SkipRefresh() || c.SkipDeps(), SkipRefresh: c.SkipRefresh(), SkipDeps: c.SkipDeps(), + AllowPartialErrors: c.AllowPartialErrors(), IncludeCRDs: &includeCRDs, Validate: c.Validate(), Concurrency: c.Concurrency(), @@ -224,9 +225,10 @@ func (a *App) Template(c TemplateConfigProvider) error { // https://github.com/helmfile/helmfile/issues/1749 run.helm.SetExtraArgs() - prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{ + prepErr := run.WithPreparedCharts("template", state.ChartPrepareOptions{ SkipRepos: c.SkipRefresh() || c.SkipDeps(), SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), SkipDeps: c.SkipDeps(), IncludeCRDs: &includeCRDs, SkipCleanup: c.SkipCleanup(), @@ -250,12 +252,13 @@ func (a *App) Template(c TemplateConfigProvider) error { func (a *App) WriteValues(c WriteValuesConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - prepErr := run.withPreparedCharts("write-values", state.ChartPrepareOptions{ - SkipRepos: c.SkipRefresh() || c.SkipDeps(), - SkipRefresh: c.SkipRefresh(), - SkipDeps: c.SkipDeps(), - SkipCleanup: c.SkipCleanup(), - Concurrency: c.Concurrency(), + prepErr := run.WithPreparedCharts("write-values", state.ChartPrepareOptions{ + SkipRepos: c.SkipRefresh() || c.SkipDeps(), + SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), + SkipDeps: c.SkipDeps(), + SkipCleanup: c.SkipCleanup(), + Concurrency: c.Concurrency(), }, func() { ok, errs = a.writeValues(run, c) }) @@ -301,10 +304,11 @@ func (a *App) Lint(c LintConfigProvider) error { var lintErrs []error // `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here - prepErr := run.withPreparedCharts("lint", state.ChartPrepareOptions{ + prepErr := run.WithPreparedCharts("lint", state.ChartPrepareOptions{ ForceDownload: true, SkipRepos: c.SkipRefresh() || c.SkipDeps(), SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), SkipDeps: c.SkipDeps(), SkipCleanup: c.SkipCleanup(), Concurrency: c.Concurrency(), @@ -337,14 +341,15 @@ func (a *App) Lint(c LintConfigProvider) error { func (a *App) Fetch(c FetchConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - prepErr := run.withPreparedCharts("pull", state.ChartPrepareOptions{ - ForceDownload: true, - SkipRefresh: c.SkipRefresh(), - SkipRepos: c.SkipRefresh() || c.SkipDeps(), - SkipDeps: c.SkipDeps(), - OutputDir: c.OutputDir(), - OutputDirTemplate: c.OutputDirTemplate(), - Concurrency: c.Concurrency(), + prepErr := run.WithPreparedCharts("pull", state.ChartPrepareOptions{ + ForceDownload: true, + SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), + SkipRepos: c.SkipRefresh() || c.SkipDeps(), + SkipDeps: c.SkipDeps(), + OutputDir: c.OutputDir(), + OutputDirTemplate: c.OutputDirTemplate(), + Concurrency: c.Concurrency(), }, func() {}) if prepErr != nil { @@ -359,9 +364,10 @@ func (a *App) Sync(c SyncConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { includeCRDs := !c.SkipCRDs() - prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{ + prepErr := run.WithPreparedCharts("sync", state.ChartPrepareOptions{ SkipRepos: c.SkipRefresh() || c.SkipDeps(), SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), SkipDeps: c.SkipDeps(), Wait: c.Wait(), WaitRetries: c.WaitRetries(), @@ -371,7 +377,7 @@ func (a *App) Sync(c SyncConfigProvider) error { Validate: c.Validate(), Concurrency: c.Concurrency(), }, func() { - ok, errs = a.sync(run, c) + ok, errs = a.SyncRun(run, c) }) if prepErr != nil { @@ -394,9 +400,10 @@ func (a *App) Apply(c ApplyConfigProvider) error { err := a.ForEachState(func(run *Run) (ok bool, errs []error) { includeCRDs := !c.SkipCRDs() - prepErr := run.withPreparedCharts("apply", state.ChartPrepareOptions{ + prepErr := run.WithPreparedCharts("apply", state.ChartPrepareOptions{ SkipRepos: c.SkipRefresh() || c.SkipDeps(), SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), SkipDeps: c.SkipDeps(), Wait: c.Wait(), WaitRetries: c.WaitRetries(), @@ -438,7 +445,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { func (a *App) Status(c StatusesConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - err := run.withPreparedCharts("status", state.ChartPrepareOptions{ + err := run.WithPreparedCharts("status", state.ChartPrepareOptions{ SkipRepos: true, SkipDeps: true, Concurrency: c.Concurrency(), @@ -457,13 +464,14 @@ func (a *App) Status(c StatusesConfigProvider) error { func (a *App) Destroy(c DestroyConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { if !c.SkipCharts() { - err := run.withPreparedCharts("destroy", state.ChartPrepareOptions{ - SkipRepos: c.SkipRefresh() || c.SkipDeps(), - SkipRefresh: c.SkipRefresh(), - SkipDeps: c.SkipDeps(), - Concurrency: c.Concurrency(), - DeleteWait: c.DeleteWait(), - DeleteTimeout: c.DeleteTimeout(), + err := run.WithPreparedCharts("destroy", state.ChartPrepareOptions{ + SkipRepos: c.SkipRefresh() || c.SkipDeps(), + SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), + SkipDeps: c.SkipDeps(), + Concurrency: c.Concurrency(), + DeleteWait: c.DeleteWait(), + DeleteTimeout: c.DeleteTimeout(), }, func() { ok, errs = a.delete(run, true, c) }) @@ -485,11 +493,12 @@ func (a *App) Test(c TestConfigProvider) error { "or set helm.sh/hook-delete-policy\n") } - err := run.withPreparedCharts("test", state.ChartPrepareOptions{ - SkipRepos: c.SkipRefresh() || c.SkipDeps(), - SkipRefresh: c.SkipRefresh(), - SkipDeps: c.SkipDeps(), - Concurrency: c.Concurrency(), + err := run.WithPreparedCharts("test", state.ChartPrepareOptions{ + SkipRepos: c.SkipRefresh() || c.SkipDeps(), + SkipRefresh: c.SkipRefresh(), + AllowPartialErrors: c.AllowPartialErrors(), + SkipDeps: c.SkipDeps(), + Concurrency: c.Concurrency(), }, func() { errs = a.test(run, c) }) @@ -505,10 +514,11 @@ func (a *App) Test(c TestConfigProvider) error { func (a *App) PrintDAGState(c DAGConfigProvider) error { var err error return a.ForEachState(func(run *Run) (ok bool, errs []error) { - err = run.withPreparedCharts("show-dag", state.ChartPrepareOptions{ - SkipRepos: true, - SkipDeps: true, - Concurrency: 2, + err = run.WithPreparedCharts("show-dag", state.ChartPrepareOptions{ + SkipRepos: true, + SkipDeps: true, + AllowPartialErrors: true, + Concurrency: 2, }, func() { err = a.dag(run) if err != nil { @@ -521,10 +531,11 @@ func (a *App) PrintDAGState(c DAGConfigProvider) error { func (a *App) PrintState(c StateConfigProvider) error { return a.ForEachState(func(run *Run) (_ bool, errs []error) { - err := run.withPreparedCharts("build", state.ChartPrepareOptions{ - SkipRepos: true, - SkipDeps: true, - Concurrency: 2, + err := run.WithPreparedCharts("build", state.ChartPrepareOptions{ + SkipRepos: true, + SkipDeps: true, + AllowPartialErrors: true, + Concurrency: 2, }, func() { if c.EmbedValues() { for i := range run.state.Releases { @@ -593,10 +604,11 @@ func (a *App) ListReleases(c ListConfigProvider) error { var err error if !c.SkipCharts() { - err = run.withPreparedCharts("list", state.ChartPrepareOptions{ - SkipRepos: true, - SkipDeps: true, - Concurrency: 2, + err = run.WithPreparedCharts("list", state.ChartPrepareOptions{ + SkipRepos: true, + SkipDeps: true, + AllowPartialErrors: true, + Concurrency: 2, }, func() { rel, err := a.list(run) if err != nil { @@ -1344,42 +1356,54 @@ func (a *App) getSelectedReleases(r *Run, includeTransitiveNeeds bool) ([]state. return selected, deduplicated, nil } -func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { - st := r.state - helm := r.helm - - helm.SetExtraArgs(GetArgs(c.Args(), r.state)...) - - selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds()) +func (a *App) GetPlannedAndSelectedReleasesWithNeeds(r *Run, skipNeeds bool, includeNeeds bool, includeTransitiveNeeds bool) ([]state.ReleaseSpec, []state.ReleaseSpec, error) { + selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, includeTransitiveNeeds) if err != nil { - return false, false, []error{err} + return nil, nil, err } if len(selectedReleases) == 0 { - return false, false, nil + return nil, nil, nil } // This is required when you're trying to deduplicate releases by the selector. // Without this, `PlanReleases` conflates duplicates and return both in `batches`, // even if we provided `SelectedReleases: selectedReleases`. // See https://github.com/roboll/helmfile/issues/1818 for more context. - st.Releases = selectedAndNeededReleases + r.state.Releases = selectedAndNeededReleases - plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}) + batches, err := r.state.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: skipNeeds, IncludeNeeds: includeNeeds, IncludeTransitiveNeeds: includeTransitiveNeeds}) if err != nil { - return false, false, []error{err} + return nil, nil, err } - var toApplyWithNeeds []state.ReleaseSpec + var releasesWithNeeds []state.ReleaseSpec - for _, rs := range plan { + for _, rs := range batches { for _, r := range rs { - toApplyWithNeeds = append(toApplyWithNeeds, r.ReleaseSpec) + releasesWithNeeds = append(releasesWithNeeds, r.ReleaseSpec) } } + return releasesWithNeeds, selectedAndNeededReleases, nil +} + +func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { + st := r.state + helm := r.helm + + helm.SetExtraArgs(GetArgs(c.Args(), r.state)...) + + releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds()) + if err != nil { + return false, false, []error{err} + } + if len(releasesWithNeeds) == 0 { + return false, false, nil + } + // Do build deps and prepare only on selected releases so that we won't waste time // on running various helm commands on unnecessary releases - st.Releases = toApplyWithNeeds + st.Releases = releasesWithNeeds // helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly detailedExitCode := true @@ -1402,27 +1426,27 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { TakeOwnership: c.TakeOwnership(), } - infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts) - if len(errs) > 0 { - return false, false, errs + infoMsg, releasesToUpdate, releasesToDelete, diffErrs := r.diff(false, detailedExitCode, c, diffOpts) + if len(diffErrs) > 0 { + return false, false, diffErrs } var toDelete []state.ReleaseSpec - for _, r := range releasesToBeDeleted { + for _, r := range releasesToDelete { toDelete = append(toDelete, r) } var toUpdate []state.ReleaseSpec - for _, r := range releasesToBeUpdated { + for _, r := range releasesToUpdate { toUpdate = append(toUpdate, r) } releasesWithNoChange := map[string]state.ReleaseSpec{} - for _, r := range toApplyWithNeeds { + for _, r := range releasesWithNeeds { release := r id := state.ReleaseToID(&release) - _, uninstalled := releasesToBeDeleted[id] - _, updated := releasesToBeUpdated[id] + _, uninstalled := releasesToDelete[id] + _, updated := releasesToUpdate[id] if !uninstalled && !updated { releasesWithNoChange[id] = release } @@ -1444,15 +1468,15 @@ Do you really want to apply? a.Logger.Debug(infoMsgStr) } - var applyErrs []error - - affectedReleases := state.AffectedReleases{} + var errs []error // Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies st.Releases = selectedAndNeededReleases + affectedReleases := state.AffectedReleases{} + if !interactive || interactive && r.askForConfirmation(confMsg) { - if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: toApplyWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { + if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: releasesWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { for _, r := range subst.Releases { release := r if _, err := st.TriggerPreapplyEvent(&release, "apply"); err != nil { @@ -1466,13 +1490,13 @@ Do you really want to apply? } // We deleted releases by traversing the DAG in reverse order - if len(releasesToBeDeleted) > 0 { + if len(releasesToDelete) > 0 { _, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{Reverse: true, SelectedReleases: toDelete, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { var rs []state.ReleaseSpec for _, r := range subst.Releases { release := r - if r2, ok := releasesToBeDeleted[state.ReleaseToID(&release)]; ok { + if r2, ok := releasesToDelete[state.ReleaseToID(&release)]; ok { rs = append(rs, r2) } } @@ -1483,18 +1507,18 @@ Do you really want to apply? })) if len(deletionErrs) > 0 { - applyErrs = append(applyErrs, deletionErrs...) + errs = append(errs, deletionErrs...) } } // We upgrade releases by traversing the DAG - if len(releasesToBeUpdated) > 0 { - _, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, Reverse: false, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { + if len(releasesToUpdate) > 0 { + _, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { var rs []state.ReleaseSpec for _, r := range subst.Releases { release := r - if r2, ok := releasesToBeUpdated[state.ReleaseToID(&release)]; ok { + if r2, ok := releasesToUpdate[state.ReleaseToID(&release)]; ok { rs = append(rs, r2) } } @@ -1502,27 +1526,27 @@ Do you really want to apply? subst.Releases = rs syncOpts := &state.SyncOpts{ + HideNotes: c.HideNotes(), + PostRenderer: c.PostRenderer(), + PostRendererArgs: c.PostRendererArgs(), + ResetValues: c.ResetValues(), + ReuseValues: c.ReuseValues(), Set: c.Set(), SkipCleanup: c.SkipCleanup(), SkipCRDs: c.SkipCRDs(), - Wait: c.Wait(), - WaitRetries: c.WaitRetries(), - WaitForJobs: c.WaitForJobs(), - ReuseValues: c.ReuseValues(), - ResetValues: c.ResetValues(), - PostRenderer: c.PostRenderer(), - PostRendererArgs: c.PostRendererArgs(), SkipSchemaValidation: c.SkipSchemaValidation(), SyncArgs: c.SyncArgs(), - HideNotes: c.HideNotes(), - TakeOwnership: c.TakeOwnership(), SyncReleaseLabels: c.SyncReleaseLabels(), + TakeOwnership: c.TakeOwnership(), + Wait: c.Wait(), + WaitForJobs: c.WaitForJobs(), + WaitRetries: c.WaitRetries(), } return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts) })) if len(updateErrs) > 0 { - applyErrs = append(applyErrs, updateErrs...) + errs = append(errs, updateErrs...) } } } @@ -1535,11 +1559,11 @@ Do you really want to apply? a.Logger.Warnf("warn: %v\n", err) } } - if releasesToBeDeleted == nil && releasesToBeUpdated == nil { + if releasesToDelete == nil && releasesToUpdate == nil { return true, false, nil } - return true, true, applyErrs + return true, true, errs } func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) { @@ -1748,42 +1772,25 @@ func (a *App) status(r *Run, c StatusesConfigProvider) (bool, []error) { return true, errs } -func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { +func (a *App) SyncRun(r *Run, c SyncConfigProvider) (bool, []error) { st := r.state helm := r.helm - selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds()) - if err != nil { - return false, []error{err} - } - if len(selectedReleases) == 0 { - return false, nil - } - - // This is required when you're trying to deduplicate releases by the selector. - // Without this, `PlanReleases` conflates duplicates and return both in `batches`, - // even if we provided `SelectedReleases: selectedReleases`. - // See https://github.com/roboll/helmfile/issues/1818 for more context. - st.Releases = selectedAndNeededReleases + helm.SetExtraArgs(GetArgs(c.Args(), r.state)...) - batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), SkipNeeds: c.SkipNeeds()}) + releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds()) if err != nil { return false, []error{err} } - - var toSyncWithNeeds []state.ReleaseSpec - - for _, rs := range batches { - for _, r := range rs { - toSyncWithNeeds = append(toSyncWithNeeds, r.ReleaseSpec) - } + if len(releasesWithNeeds) == 0 { + return false, nil } // Do build deps and prepare only on selected releases so that we won't waste time // on running various helm commands on unnecessary releases - st.Releases = toSyncWithNeeds + st.Releases = releasesWithNeeds - toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, toSyncWithNeeds) + toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, releasesWithNeeds) if err != nil { return false, []error{err} } @@ -1796,7 +1803,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { } var toUpdate []state.ReleaseSpec - for _, r := range toSyncWithNeeds { + for _, r := range releasesWithNeeds { release := r if _, deleted := releasesToDelete[state.ReleaseToID(&release)]; !deleted { if r.Desired() { @@ -1816,7 +1823,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { } releasesWithNoChange := map[string]state.ReleaseSpec{} - for _, r := range toSyncWithNeeds { + for _, r := range releasesWithNeeds { release := r id := state.ReleaseToID(&release) _, uninstalled := releasesToDelete[id] @@ -1826,13 +1833,6 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { } } - for id := range releasesWithNoChange { - r := releasesWithNoChange[id] - if _, err := st.TriggerCleanupEvent(&r, "sync"); err != nil { - a.Logger.Warnf("warn: %v\n", err) - } - } - names := []string{} for _, r := range releasesToUpdate { names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart)) @@ -1860,8 +1860,6 @@ Do you really want to sync? var errs []error - r.helm.SetExtraArgs(GetArgs(c.Args(), r.state)...) - // Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies st.Releases = selectedAndNeededReleases @@ -1902,23 +1900,23 @@ Do you really want to sync? subst.Releases = rs - opts := &state.SyncOpts{ - Set: c.Set(), - SkipCRDs: c.SkipCRDs(), - Wait: c.Wait(), - WaitRetries: c.WaitRetries(), - WaitForJobs: c.WaitForJobs(), - ReuseValues: c.ReuseValues(), - ResetValues: c.ResetValues(), + syncOpts := &state.SyncOpts{ + HideNotes: c.HideNotes(), PostRenderer: c.PostRenderer(), PostRendererArgs: c.PostRendererArgs(), - SyncArgs: c.SyncArgs(), - HideNotes: c.HideNotes(), - TakeOwnership: c.TakeOwnership(), + ResetValues: c.ResetValues(), + ReuseValues: c.ReuseValues(), + Set: c.Set(), + SkipCRDs: c.SkipCRDs(), SkipSchemaValidation: c.SkipSchemaValidation(), + SyncArgs: c.SyncArgs(), SyncReleaseLabels: c.SyncReleaseLabels(), + TakeOwnership: c.TakeOwnership(), + Wait: c.Wait(), + WaitForJobs: c.WaitForJobs(), + WaitRetries: c.WaitRetries(), } - return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts) + return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts) })) if len(syncErrs) > 0 { @@ -1926,7 +1924,16 @@ Do you really want to sync? } } } + affectedReleases.DisplayAffectedReleases(c.Logger()) + + for id := range releasesWithNoChange { + r := releasesWithNoChange[id] + if _, err := st.TriggerCleanupEvent(&r, "sync"); err != nil { + a.Logger.Warnf("warn: %v\n", err) + } + } + return true, errs } diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index b14543f40..98929a4f1 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2194,6 +2194,7 @@ type configImpl struct { skipTests bool skipSchemaValidation bool skipRefresh bool + allowPartialErrors bool skipNeeds bool includeNeeds bool @@ -2238,6 +2239,10 @@ func (c configImpl) SkipRefresh() bool { return c.skipRefresh } +func (c configImpl) AllowPartialErrors() bool { + return c.allowPartialErrors +} + func (c configImpl) SkipNeeds() bool { return c.skipNeeds } @@ -2317,6 +2322,7 @@ type applyConfig struct { skipCRDs bool skipDeps bool skipRefresh bool + allowPartialErrors bool skipNeeds bool includeNeeds bool includeTransitiveNeeds bool @@ -2405,6 +2411,10 @@ func (a applyConfig) SkipRefresh() bool { return a.skipRefresh } +func (a applyConfig) AllowPartialErrors() bool { + return a.allowPartialErrors +} + func (a applyConfig) SkipNeeds() bool { return a.skipNeeds } diff --git a/pkg/app/config.go b/pkg/app/config.go index e699d86f1..886f5a7da 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -11,6 +11,7 @@ type ConfigProvider interface { DisableForceUpdate() bool SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool FileOrDir() string KubeContext() string @@ -53,6 +54,7 @@ type ApplyConfigProvider interface { SkipCRDs() bool SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool Wait() bool WaitRetries() int WaitForJobs() bool @@ -104,6 +106,7 @@ type SyncConfigProvider interface { SkipCRDs() bool SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool Wait() bool WaitRetries() int WaitForJobs() bool @@ -138,6 +141,7 @@ type DiffConfigProvider interface { SkipCRDs() bool SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool IncludeTests() bool @@ -169,6 +173,7 @@ type DestroyConfigProvider interface { SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool SkipCharts() bool DeleteWait() bool DeleteTimeout() int @@ -183,6 +188,7 @@ type TestConfigProvider interface { SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool Timeout() int Cleanup() bool Logs() bool @@ -197,6 +203,7 @@ type LintConfigProvider interface { Set() []string SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool SkipCleanup() bool DAGConfig @@ -207,6 +214,7 @@ type LintConfigProvider interface { type FetchConfigProvider interface { SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool OutputDir() string OutputDirTemplate() string @@ -225,6 +233,7 @@ type TemplateConfigProvider interface { Validate() bool SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool SkipCleanup() bool SkipTests() bool OutputDir() string @@ -250,6 +259,7 @@ type WriteValuesConfigProvider interface { OutputFileTemplate() string SkipDeps() bool SkipRefresh() bool + AllowPartialErrors() bool SkipCleanup() bool IncludeTransitiveNeeds() bool diff --git a/pkg/app/destroy_test.go b/pkg/app/destroy_test.go index 4d578851b..64033e642 100644 --- a/pkg/app/destroy_test.go +++ b/pkg/app/destroy_test.go @@ -39,6 +39,7 @@ type destroyConfig struct { interactive bool skipDeps bool skipRefresh bool + allowPartialErrors bool logger *zap.SugaredLogger includeTransitiveNeeds bool skipCharts bool @@ -78,6 +79,10 @@ func (d destroyConfig) SkipRefresh() bool { return d.skipRefresh } +func (d destroyConfig) AllowPartialErrors() bool { + return d.allowPartialErrors +} + func (d destroyConfig) IncludeTransitiveNeeds() bool { return d.includeTransitiveNeeds } diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index 18dde7fc5..631a5286e 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -25,6 +25,7 @@ type diffConfig struct { skipCRDs bool skipDeps bool skipRefresh bool + allowPartialErrors bool includeTests bool skipNeeds bool includeNeeds bool @@ -81,6 +82,10 @@ func (a diffConfig) SkipRefresh() bool { return a.skipRefresh } +func (a diffConfig) AllowPartialErrors() bool { + return a.allowPartialErrors +} + func (a diffConfig) IncludeTests() bool { return a.includeTests } diff --git a/pkg/app/run.go b/pkg/app/run.go index 2beab11ae..60726d9df 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -51,13 +51,19 @@ func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurre releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, concurrency, helmfileCommand, opts) if len(errs) > 0 { - return nil, fmt.Errorf("%v", errs) + if !opts.AllowPartialErrors { + // abort on first error + return nil, fmt.Errorf("%v", errs) + } else { + // return partial results with errors for the failed ones + return releaseToChart, fmt.Errorf("%v", errs) + } } return releaseToChart, nil } -func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error { +func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error { if r.ReleaseToChart != nil { panic("Run.PrepareCharts can be called only once") } @@ -90,8 +96,11 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare } releaseToChart, err := r.prepareChartsIfNeeded(helmfileCommand, dir, opts.Concurrency, opts) - if err != nil { - return err + // IMPORTANT: on opts.AllowPartialErrors: do not abort on error here, just forward it to the caller in order to allow for partial results + if !opts.AllowPartialErrors { + if err != nil { + return err + } } for i := range r.state.Releases { @@ -115,8 +124,17 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare f() - _, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand) - return err + _, cleanupErr := r.state.TriggerGlobalCleanupEvent(helmfileCommand) + if !opts.AllowPartialErrors { + // return directly on first error + return cleanupErr + } else { + // merge the two errors into a single error output + if err != nil || cleanupErr != nil { + return fmt.Errorf("prepare charts error: %v; cleanup error: %v", err, cleanupErr) + } + return nil + } } func (r *Run) Deps(c DepsConfigProvider) []error { @@ -222,3 +240,15 @@ func (r *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfig return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil } + +// Hack to get access to helmfile’s API +// -rluba, 2025-05-19 +func (r *Run) GetState() *state.HelmState { + return r.state +} + +// Hack to get access to helmfile’s API +// -rluba, 2025-05-19 +func (r *Run) GetHelm() helmexec.Interface { + return r.helm +} diff --git a/pkg/config/global.go b/pkg/config/global.go index fe9061954..97e7af932 100644 --- a/pkg/config/global.go +++ b/pkg/config/global.go @@ -34,6 +34,8 @@ type GlobalOptions struct { SkipDeps bool // SkipRefresh is true if the running "helm repo update" should be skipped SkipRefresh bool + // AllowPartialErrors is true if partial errors during release processing are allowed + AllowPartialErrors 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" @@ -181,6 +183,11 @@ func (g *GlobalImpl) SkipRefresh() bool { return g.GlobalOptions.SkipRefresh } +// AllowPartialErrors return if partial errors during release processing are allowed +func (g *GlobalImpl) AllowPartialErrors() bool { + return g.GlobalOptions.AllowPartialErrors +} + // StripArgsValuesOnExitError return if the ARGS output on exit error should be suppressed func (g *GlobalImpl) StripArgsValuesOnExitError() bool { return g.GlobalOptions.StripArgsValuesOnExitError diff --git a/pkg/state/state.go b/pkg/state/state.go index 03f22ed4b..e697d49fd 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1231,12 +1231,13 @@ func filterReleasesForBuild(releases []ReleaseSpec) []ReleaseSpec { } type ChartPrepareOptions struct { - ForceDownload bool - SkipRepos bool - SkipDeps bool - SkipRefresh bool - SkipResolve bool - SkipCleanup bool + ForceDownload bool + SkipRepos bool + SkipDeps bool + SkipRefresh bool + SkipResolve bool + SkipCleanup bool + AllowPartialErrors bool // Validate is a helm-3-only option. When it is set to true, it configures chartify to pass --validate to helm-template run by it. // It's required when one of your chart relies on Capabilities.APIVersions in a template Validate bool @@ -1568,7 +1569,12 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre if downloadRes.err != nil { errs = append(errs, downloadRes.err) - return + if !opts.AllowPartialErrors { + return + } else { + // continue processing other releases even if one fails + continue + } } func() { prepareChartInfoMutex.Lock() @@ -1588,7 +1594,15 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre ) if len(errs) > 0 { - return nil, errs + if !opts.AllowPartialErrors { + return nil, errs + } else { + // continue processing other releases even if one fails, return partial results with errors for the failed ones + st.logger.Warnf("Some charts failed to prepare:\n") + for _, err := range errs { + st.logger.Warnf(" - %v\n", err) + } + } } if len(builds) > 0 {