From 9284d1764e5def9e865e086045793e9847f29721 Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Sun, 28 Aug 2022 09:36:07 +0800 Subject: [PATCH] Add --interactive option to sync, delete and destroy / Remove --interactive from global options (#328) * add interactive in sync & remove --interactive in global options Signed-off-by: yxxhero * fix unittest Signed-off-by: yxxhero * same behave as apply when in interactive Signed-off-by: yxxhero Signed-off-by: yxxhero --- cmd/apply.go | 47 ++++++++++----------- cmd/delete.go | 1 + cmd/destroy.go | 1 + cmd/root.go | 1 - cmd/sync.go | 1 + docs/index.md | 3 +- pkg/app/app.go | 95 ++++++++++++++++++++++++------------------- pkg/app/config.go | 1 + pkg/config/apply.go | 7 ++++ pkg/config/delete.go | 7 ++++ pkg/config/destroy.go | 7 ++++ pkg/config/global.go | 7 ---- pkg/config/sync.go | 7 ++++ pkg/state/state.go | 9 ++-- 14 files changed, 115 insertions(+), 79 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index 8ab372ff..f67f5d79 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -31,29 +31,30 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { } f := cmd.Flags() - f.StringArrayVar(&applyImpl.ApplyOptions.Set, "set", nil, "additional values to be merged into the command") - f.StringArrayVar(&applyImpl.ApplyOptions.Values, "values", nil, "additional value files to be merged into the command") - f.IntVar(&applyImpl.ApplyOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") - f.BoolVar(&applyImpl.ApplyOptions.Validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. Note that this requires access to a Kubernetes cluster to obtain information necessary for validating, like the list of available API versions") - f.IntVar(&applyImpl.ApplyOptions.Context, "context", 0, "output NUM lines of context around changes") - f.StringVar(&applyImpl.ApplyOptions.Output, "output", "", "output format for diff plugin") - f.BoolVar(&applyImpl.ApplyOptions.DetailedExitcode, "detailed-exitcode", false, "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully") - f.StringVar(&applyImpl.ApplyOptions.Args, "args", "", "pass args to helm exec") - f.BoolVar(&applyImpl.ApplyOptions.RetainValuesFiles, "retain-values-files", false, "DEPRECATED: Use skip-cleanup instead") - f.BoolVar(&applyImpl.ApplyOptions.SkipCleanup, "skip-cleanup", false, "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security") - f.BoolVar(&applyImpl.ApplyOptions.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed on sync. By default, CRDs are installed if not already present") - f.BoolVar(&applyImpl.ApplyOptions.SkipNeeds, "skip-needs", false, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) - f.BoolVar(&applyImpl.ApplyOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`) - f.BoolVar(&applyImpl.ApplyOptions.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`) - f.BoolVar(&applyImpl.ApplyOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all0") - f.BoolVar(&applyImpl.ApplyOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks") - f.StringArrayVar(&applyImpl.ApplyOptions.Suppress, "suppress", nil, "suppress specified Kubernetes objects in the diff output. Can be provided multiple times. For example: --suppress KeycloakClient --suppress VaultSecret") - f.BoolVar(&applyImpl.ApplyOptions.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases") - f.BoolVar(&applyImpl.ApplyOptions.ShowSecrets, "show-secrets", false, "do not redact secret values in the diff output. should be used for debug purpose only") - f.BoolVar(&applyImpl.ApplyOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output. Usable in new installs") - f.BoolVar(&applyImpl.ApplyOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) - f.Bool("wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) - f.Bool("wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) + f.StringArrayVar(&applyOptions.Set, "set", nil, "additional values to be merged into the command") + f.StringArrayVar(&applyOptions.Values, "values", nil, "additional value files to be merged into the command") + f.IntVar(&applyOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") + f.BoolVar(&applyOptions.Validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. Note that this requires access to a Kubernetes cluster to obtain information necessary for validating, like the list of available API versions") + f.IntVar(&applyOptions.Context, "context", 0, "output NUM lines of context around changes") + f.StringVar(&applyOptions.Output, "output", "", "output format for diff plugin") + f.BoolVar(&applyOptions.DetailedExitcode, "detailed-exitcode", false, "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully") + f.StringVar(&applyOptions.Args, "args", "", "pass args to helm exec") + f.BoolVar(&applyOptions.RetainValuesFiles, "retain-values-files", false, "DEPRECATED: Use skip-cleanup instead") + f.BoolVar(&applyOptions.SkipCleanup, "skip-cleanup", false, "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security") + f.BoolVar(&applyOptions.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed on sync. By default, CRDs are installed if not already present") + f.BoolVar(&applyOptions.SkipNeeds, "skip-needs", false, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`) + f.BoolVar(&applyOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`) + f.BoolVar(&applyOptions.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`) + f.BoolVar(&applyOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all0") + f.BoolVar(&applyOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks") + f.StringArrayVar(&applyOptions.Suppress, "suppress", nil, "suppress specified Kubernetes objects in the diff output. Can be provided multiple times. For example: --suppress KeycloakClient --suppress VaultSecret") + f.BoolVar(&applyOptions.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases") + f.BoolVar(&applyOptions.ShowSecrets, "show-secrets", false, "do not redact secret values in the diff output. should be used for debug purpose only") + f.BoolVar(&applyOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output. Usable in new installs") + f.BoolVar(&applyOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) + f.BoolVarP(&applyOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters") + f.BoolVar(&applyOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) + f.BoolVar(&applyOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) return cmd } diff --git a/cmd/delete.go b/cmd/delete.go index 09d4c105..867d205d 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -35,6 +35,7 @@ func NewDeleteCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.IntVar(&deleteOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") f.BoolVar(&deleteOptions.Purge, "purge", false, "purge releases i.e. free release names and histories") f.BoolVar(&deleteOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) + f.BoolVarP(&deleteOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters") return cmd } diff --git a/cmd/destroy.go b/cmd/destroy.go index c2ba93d7..eaf24c73 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -34,6 +34,7 @@ func NewDestroyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.StringVar(&destroyOptions.Args, "args", "", "pass args to helm exec") f.IntVar(&destroyOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") f.BoolVar(&destroyOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) + f.BoolVarP(&destroyOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters") return cmd } diff --git a/cmd/root.go b/cmd/root.go index c176ba7a..75444fe8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -121,7 +121,6 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO --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.BoolVarP(&globalOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters") // avoid 'pflag: help requested' error (#251) fs.BoolP("help", "h", false, "help for helmfile") } diff --git a/cmd/sync.go b/cmd/sync.go index 08fa2494..dc013d55 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -43,6 +43,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`) f.BoolVar(&syncOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) f.BoolVar(&syncOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) + f.BoolVarP(&syncOptions.Interactive, "interactive", "i", false, "Request confirmation before attempting to modify clusters") return cmd } diff --git a/docs/index.md b/docs/index.md index d5a9fd26..882c46bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -522,7 +522,6 @@ Flags: -f, --file helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference -b, --helm-binary string Path to the helm binary (default "helm") -h, --help help for helmfile - -i, --interactive Request confirmation before attempting to modify clusters --kube-context string Set kubectl context. Uses current context by default --log-level string Set log level, default info (default "info") -n, --namespace string Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }} @@ -1355,7 +1354,7 @@ Please see #203 for more context. ## Running Helmfile interactively -`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster. +`helmfile --interactive [apply|destroy|delete|sync]` requests confirmation from you before actually modifying your cluster. Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts. diff --git a/pkg/app/app.go b/pkg/app/app.go index d68202bc..9e48a55e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1297,7 +1297,7 @@ Do you really want to apply? a.Logger.Debug(*infoMsg) } - syncErrs := []error{} + applyErrs := []error{} affectedReleases := state.AffectedReleases{} @@ -1325,7 +1325,7 @@ Do you really want to apply? })) if len(deletionErrs) > 0 { - syncErrs = append(syncErrs, deletionErrs...) + applyErrs = append(applyErrs, deletionErrs...) } } @@ -1354,13 +1354,13 @@ Do you really want to apply? })) if len(updateErrs) > 0 { - syncErrs = append(syncErrs, updateErrs...) + applyErrs = append(applyErrs, updateErrs...) } } } affectedReleases.DisplayAffectedReleases(c.Logger()) - return true, true, syncErrs + return true, true, applyErrs } func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) { @@ -1663,7 +1663,16 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { %s `, strings.Join(names, "\n")) - a.Logger.Info(infoMsg) + confMsg := fmt.Sprintf(`%s +Do you really want to sync? + Helmfile will sync all your releases, as shown above. + +`, infoMsg) + + interactive := c.Interactive() + if !interactive { + a.Logger.Debug(infoMsg) + } var errs []error @@ -1674,51 +1683,53 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { affectedReleases := state.AffectedReleases{} - 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 + if !interactive || interactive && r.askForConfirmation(confMsg) { + 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 := releasesToDelete[state.ReleaseToID(&release)]; ok { - rs = append(rs, r2) + for _, r := range subst.Releases { + release := r + if r2, ok := releasesToDelete[state.ReleaseToID(&release)]; ok { + rs = append(rs, r2) + } } + + subst.Releases = rs + + return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency()) + })) + + if len(deletionErrs) > 0 { + errs = append(errs, deletionErrs...) } - - subst.Releases = rs - - return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency()) - })) - - if len(deletionErrs) > 0 { - errs = append(errs, deletionErrs...) } - } - if len(releasesToUpdate) > 0 { - _, syncErrs := 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 + if len(releasesToUpdate) > 0 { + _, syncErrs := 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 _, ok := releasesToDelete[state.ReleaseToID(&release)]; !ok { - rs = append(rs, release) + for _, r := range subst.Releases { + release := r + if _, ok := releasesToDelete[state.ReleaseToID(&release)]; !ok { + rs = append(rs, release) + } } + + subst.Releases = rs + + opts := &state.SyncOpts{ + Set: c.Set(), + SkipCRDs: c.SkipCRDs(), + Wait: c.Wait(), + WaitForJobs: c.WaitForJobs(), + } + return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts) + })) + + if len(syncErrs) > 0 { + errs = append(errs, syncErrs...) } - - subst.Releases = rs - - opts := &state.SyncOpts{ - Set: c.Set(), - SkipCRDs: c.SkipCRDs(), - Wait: c.Wait(), - WaitForJobs: c.WaitForJobs(), - } - return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts) - })) - - if len(syncErrs) > 0 { - errs = append(errs, syncErrs...) } } affectedReleases.DisplayAffectedReleases(c.Logger()) diff --git a/pkg/app/config.go b/pkg/app/config.go index a9b59596..99cc8723 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -91,6 +91,7 @@ type SyncConfigProvider interface { DAGConfig concurrencyConfig + interactive loggingConfig } diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 3bae4765..a31d4031 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -50,6 +50,8 @@ type ApplyOptions struct { Wait bool // WaitForJobs is true if the helm command should wait for the jobs to be completed WaitForJobs bool + // Interactive is true if the user should be prompted for input. + Interactive bool } // NewApply creates a new Apply @@ -195,3 +197,8 @@ func (a *ApplyImpl) Wait() bool { func (a *ApplyImpl) WaitForJobs() bool { return a.ApplyOptions.WaitForJobs } + +// Interactive returns the Interactive. +func (a *ApplyImpl) Interactive() bool { + return a.ApplyOptions.Interactive +} diff --git a/pkg/config/delete.go b/pkg/config/delete.go index 3778512f..fcb4fcd3 100644 --- a/pkg/config/delete.go +++ b/pkg/config/delete.go @@ -10,6 +10,8 @@ type DeleteOptions struct { Purge bool // SkipDeps is the skip deps flag SkipDeps bool + // Interactive is true if the user should be prompted for input. + Interactive bool } // NewDeleteOptions creates a new Apply @@ -50,3 +52,8 @@ func (c *DeleteImpl) Purge() bool { func (c *DeleteImpl) SkipDeps() bool { return c.DeleteOptions.SkipDeps } + +// Interactive returns the Interactive +func (c *DeleteImpl) Interactive() bool { + return c.DeleteOptions.Interactive +} diff --git a/pkg/config/destroy.go b/pkg/config/destroy.go index 5cfbaf26..41ab6eea 100644 --- a/pkg/config/destroy.go +++ b/pkg/config/destroy.go @@ -8,6 +8,8 @@ type DestroyOptions struct { Concurrency int // SkipDeps is the skip deps flag SkipDeps bool + // Interactive is true if the user should be prompted for input. + Interactive bool } // NewDestroyOptions creates a new Apply @@ -43,3 +45,8 @@ func (c *DestroyImpl) Args() string { func (c *DestroyImpl) SkipDeps() bool { return c.DestroyOptions.SkipDeps } + +// Interactive returns the Interactive +func (c *DestroyImpl) Interactive() bool { + return c.DestroyOptions.Interactive +} diff --git a/pkg/config/global.go b/pkg/config/global.go index 671a7aaf..5fa34837 100644 --- a/pkg/config/global.go +++ b/pkg/config/global.go @@ -42,8 +42,6 @@ type GlobalOptions struct { Selector []string // AllowNoMatchingRelease is not exit with an error code if the provided selector has no matching releases. AllowNoMatchingRelease bool - // Interactive is true if the user should be prompted for input. - Interactive bool // logger is the logger to use. logger *zap.SugaredLogger } @@ -122,11 +120,6 @@ func (g *GlobalImpl) StateValuesFiles() []string { return g.GlobalOptions.StateValuesFile } -// Interactive return interactive mode -func (g *GlobalImpl) Interactive() bool { - return g.GlobalOptions.Interactive -} - // Logger returns the logger func (g *GlobalImpl) Logger() *zap.SugaredLogger { return g.GlobalOptions.logger diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 5932f3b4..61569710 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -26,6 +26,8 @@ type SyncOptions struct { Wait bool // WaitForJobs is the wait for jobs flag WaitForJobs bool + // Interactive is true if the user should be prompted for input. + Interactive bool } // NewSyncOptions creates a new Apply @@ -110,3 +112,8 @@ func (t *SyncImpl) Wait() bool { func (t *SyncImpl) WaitForJobs() bool { return t.SyncOptions.WaitForJobs } + +// Interactive returns the Interactive +func (t *SyncImpl) Interactive() bool { + return t.SyncOptions.Interactive +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 38d00e5d..902c9ec7 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -610,7 +610,7 @@ func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helme } func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) { - detected := []ReleaseSpec{} + deleted := []ReleaseSpec{} for i := range releases { release := releases[i] @@ -618,14 +618,15 @@ func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, r installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release) if err != nil { return nil, err - } else if installed { + } + if installed { // Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554) r := release - detected = append(detected, r) + deleted = append(deleted, r) } } } - return detected, nil + return deleted, nil } func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {