From 00dace9b635e85dad1df6e5e188e8ea2c99b0eed Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Mon, 15 May 2023 13:49:33 +0800 Subject: [PATCH] Feat add cascade support (#860) * feat: add cascade support for helm v3.12.0 Signed-off-by: yxxhero --- cmd/apply.go | 1 + cmd/delete.go | 1 + cmd/destroy.go | 1 + cmd/sync.go | 1 + go.mod | 2 +- pkg/app/app.go | 6 +- pkg/app/app_test.go | 11 ++- pkg/app/config.go | 4 ++ pkg/app/destroy_test.go | 5 ++ pkg/config/apply.go | 7 ++ pkg/config/delete.go | 7 ++ pkg/config/destroy.go | 7 ++ pkg/config/sync.go | 7 ++ pkg/state/helmx.go | 18 +++++ pkg/state/helmx_test.go | 75 ++++++++++++++++++++ pkg/state/state.go | 15 +++- pkg/state/state_test.go | 2 +- pkg/state/temp_test.go | 12 ++-- pkg/{app/mocks_test.go => testutil/mocks.go} | 26 ++++++- 19 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 pkg/state/helmx_test.go rename pkg/{app/mocks_test.go => testutil/mocks.go} (83%) diff --git a/cmd/apply.go b/cmd/apply.go index 763827526..1f5b73b8f 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -65,6 +65,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`) f.BoolVar(&applyOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`) f.StringVar(&applyOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) + f.StringVar(&applyOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") return cmd } diff --git a/cmd/delete.go b/cmd/delete.go index 69f9b61d1..cac8eec1a 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -33,6 +33,7 @@ func NewDeleteCmd(globalCfg *config.GlobalImpl) *cobra.Command { f := cmd.Flags() f.StringVar(&globalCfg.GlobalOptions.Args, "args", "", "pass args to helm exec") + f.StringVar(&deleteOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") 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.SkipCharts, "skip-charts", false, "don't prepare charts when deleting releases") diff --git a/cmd/destroy.go b/cmd/destroy.go index bf36b03e6..699b6578b 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -32,6 +32,7 @@ func NewDestroyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f := cmd.Flags() f.StringVar(&globalCfg.GlobalOptions.Args, "args", "", "pass args to helm exec") + f.StringVar(&destroyOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") f.IntVar(&destroyOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited") f.BoolVar(&destroyOptions.SkipCharts, "skip-charts", false, "don't prepare charts when destroying releases") diff --git a/cmd/sync.go b/cmd/sync.go index 5f0a46f6e..42d200e81 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -45,6 +45,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`) f.BoolVar(&syncOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`) f.StringVar(&syncOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) + f.StringVar(&syncOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") return cmd } diff --git a/go.mod b/go.mod index fa66c2e61..cd9028eb1 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/a8m/envsubst v1.3.0 // indirect github.com/aws/aws-sdk-go v1.44.122 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver v3.5.1+incompatible github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fujiwara/tfstate-lookup v1.1.1 // indirect diff --git a/pkg/app/app.go b/pkg/app/app.go index a3b22b104..2af727099 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1441,7 +1441,7 @@ Do you really want to apply? subst.Releases = rs - return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency()) + return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency(), c.Cascade()) })) if len(deletionErrs) > 0 { @@ -1560,7 +1560,7 @@ Do you really want to delete? if len(releasesToDelete) > 0 { _, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toDelete, Reverse: true, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { - return subst.DeleteReleases(&affectedReleases, helm, c.Concurrency(), purge) + return subst.DeleteReleases(&affectedReleases, helm, c.Concurrency(), purge, c.Cascade()) })) if len(deletionErrs) > 0 { @@ -1832,7 +1832,7 @@ Do you really want to sync? subst.Releases = rs - return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency()) + return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency(), c.Cascade()) })) if len(deletionErrs) > 0 { diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index da9e707ca..fc3d3daa0 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -57,7 +57,7 @@ func expectNoCallsToHelmVersion(app *App) { } app.helms = map[helmKey]helmexec.Interface{ - createHelmKey(app.OverrideHelmBinary, app.OverrideKubeContext): &versionOnlyHelmExec{isHelm3: true}, + createHelmKey(app.OverrideHelmBinary, app.OverrideKubeContext): testutil.NewV3HelmExec(true), } } @@ -2185,8 +2185,9 @@ func (c configImpl) KubeVersion() string { } type applyConfig struct { - args string - values []string + args string + cascade string + values []string // TODO: Remove this function once Helmfile v0.x retainValuesFiles bool @@ -2230,6 +2231,10 @@ func (a applyConfig) Args() string { return a.args } +func (a applyConfig) Cascade() string { + return a.cascade +} + func (a applyConfig) Wait() bool { return a.wait } diff --git a/pkg/app/config.go b/pkg/app/config.go index 9cfa1585d..7c15add47 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -46,6 +46,7 @@ type ReposConfigProvider interface { type ApplyConfigProvider interface { Args() string PostRenderer() string + Cascade() string Values() []string Set() []string @@ -88,6 +89,7 @@ type ApplyConfigProvider interface { type SyncConfigProvider interface { Args() string PostRenderer() string + Cascade() string Values() []string Set() []string @@ -144,6 +146,7 @@ type DiffConfigProvider interface { // TODO: Remove this function once Helmfile v0.x type DeleteConfigProvider interface { Args() string + Cascade() string Purge() bool SkipDeps() bool @@ -156,6 +159,7 @@ type DeleteConfigProvider interface { type DestroyConfigProvider interface { Args() string + Cascade() string SkipDeps() bool SkipCharts() bool diff --git a/pkg/app/destroy_test.go b/pkg/app/destroy_test.go index 8852ea09e..79a5bdc48 100644 --- a/pkg/app/destroy_test.go +++ b/pkg/app/destroy_test.go @@ -34,6 +34,7 @@ func listFlags(namespace, kubeContext string) string { type destroyConfig struct { args string + cascade string concurrency int interactive bool skipDeps bool @@ -46,6 +47,10 @@ func (d destroyConfig) Args() string { return d.args } +func (d destroyConfig) Cascade() string { + return d.cascade +} + func (d destroyConfig) SkipCharts() bool { return d.skipCharts } diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 5862bd96a..8b2d93b47 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -56,6 +56,8 @@ type ApplyOptions struct { ResetValues bool // Propagate '--post-renderer' to helmv3 template and helm install PostRenderer string + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade string } // NewApply creates a new Apply @@ -212,3 +214,8 @@ func (a *ApplyImpl) ResetValues() bool { func (a *ApplyImpl) PostRenderer() string { return a.ApplyOptions.PostRenderer } + +// Cascade returns cascade flag +func (a *ApplyImpl) Cascade() string { + return a.ApplyOptions.Cascade +} diff --git a/pkg/config/delete.go b/pkg/config/delete.go index 31fd07685..ca0d8c9b0 100644 --- a/pkg/config/delete.go +++ b/pkg/config/delete.go @@ -9,6 +9,8 @@ type DeleteOptions struct { Purge bool // SkipCharts makes Delete skip `withPreparedCharts` SkipCharts bool + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade string } // NewDeleteOptions creates a new Apply @@ -44,3 +46,8 @@ func (c *DeleteImpl) Purge() bool { func (c *DeleteImpl) SkipCharts() bool { return c.DeleteOptions.SkipCharts } + +// Cascade returns cascade flag +func (c *DeleteImpl) Cascade() string { + return c.DeleteOptions.Cascade +} diff --git a/pkg/config/destroy.go b/pkg/config/destroy.go index 0d3b36b7e..42df8f994 100644 --- a/pkg/config/destroy.go +++ b/pkg/config/destroy.go @@ -6,6 +6,8 @@ type DestroyOptions struct { Concurrency int // SkipCharts makes Destroy skip `withPreparedCharts` SkipCharts bool + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade string } // NewDestroyOptions creates a new Apply @@ -36,3 +38,8 @@ func (c *DestroyImpl) Concurrency() int { func (c *DestroyImpl) SkipCharts() bool { return c.DestroyOptions.SkipCharts } + +// Cascade returns cascade flag +func (c *DestroyImpl) Cascade() string { + return c.DestroyOptions.Cascade +} diff --git a/pkg/config/sync.go b/pkg/config/sync.go index d4073f58c..d8858fab5 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -28,6 +28,8 @@ type SyncOptions struct { ResetValues bool // Propagate '--post-renderer' to helmv3 template and helm install PostRenderer string + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade string } // NewSyncOptions creates a new Apply @@ -118,3 +120,8 @@ func (t *SyncImpl) ResetValues() bool { func (t *SyncImpl) PostRenderer() string { return t.SyncOptions.PostRenderer } + +// Cascade returns cascade flag +func (t *SyncImpl) Cascade() string { + return t.SyncOptions.Cascade +} diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index 56f7809ca..a440b0381 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -39,6 +39,24 @@ func (st *HelmState) appendPostRenderFlags(flags []string, release *ReleaseSpec, return flags } +// append post-renderer flags to helm flags +func (st *HelmState) appendCascadeFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec, cascade string) []string { + // see https://github.com/helm/helm/releases/tag/v3.12.0 + if !helm.IsVersionAtLeast("3.12.0") { + return flags + } + switch { + // postRenderer arg comes from cmd flag. + case release.Cascade != nil && *release.Cascade != "": + flags = append(flags, "--cascade", *release.Cascade) + case cascade != "": + flags = append(flags, "--cascade", cascade) + case st.HelmDefaults.Cascade != nil && *st.HelmDefaults.Cascade != "": + flags = append(flags, "--cascade", *st.HelmDefaults.Cascade) + } + return flags +} + type Chartify struct { Opts *chartify.ChartifyOpts Clean func() diff --git a/pkg/state/helmx_test.go b/pkg/state/helmx_test.go new file mode 100644 index 000000000..0c1bfaef0 --- /dev/null +++ b/pkg/state/helmx_test.go @@ -0,0 +1,75 @@ +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/helmfile/helmfile/pkg/helmexec" + "github.com/helmfile/helmfile/pkg/testutil" +) + +func TestAppendCascadeFlags(t *testing.T) { + type args struct { + flags []string + release *ReleaseSpec + cascade string + helm helmexec.Interface + helmSpec HelmSpec + expected []string + } + tests := []struct { + name string + args args + }{ + { + name: "no cascade when helm less than 3.11.0", + args: args{ + flags: []string{}, + release: &ReleaseSpec{}, + cascade: "background", + helm: testutil.NewVersionHelmExec("3.11.0"), + expected: []string{}, + }, + }, + { + name: "cascade from release", + args: args{ + flags: []string{}, + release: &ReleaseSpec{Cascade: &[]string{"background", "background"}[0]}, + cascade: "", + helm: testutil.NewVersionHelmExec("3.12.0"), + expected: []string{"--cascade", "background"}, + }, + }, + { + name: "cascade from cmd flag", + args: args{ + flags: []string{}, + release: &ReleaseSpec{}, + cascade: "background", + helm: testutil.NewVersionHelmExec("3.12.0"), + expected: []string{"--cascade", "background"}, + }, + }, + { + name: "cascade from helm defaults", + args: args{ + flags: []string{}, + release: &ReleaseSpec{}, + helmSpec: HelmSpec{Cascade: &[]string{"background", "background"}[0]}, + cascade: "", + helm: testutil.NewVersionHelmExec("3.12.0"), + expected: []string{"--cascade", "background"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &HelmState{} + st.HelmDefaults = tt.args.helmSpec + got := st.appendCascadeFlags(tt.args.flags, tt.args.helm, tt.args.release, tt.args.cascade) + require.Equalf(t, tt.args.expected, got, "appendCascadeFlags() = %v, want %v", got, tt.args.expected) + }) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 34a6ab832..0cbaf1b60 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -187,6 +187,8 @@ type HelmSpec struct { ReuseValues bool `yaml:"reuseValues"` // Propagate '--post-renderer' to helmv3 template and helm install PostRenderer *string `yaml:"postRenderer,omitempty"` + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade *string `yaml:"cascade,omitempty"` TLS bool `yaml:"tls"` TLSCACert string `yaml:"tlsCACert,omitempty"` @@ -360,6 +362,9 @@ type ReleaseSpec struct { // Propagate '--post-renderer' to helmv3 template and helm install PostRenderer *string `yaml:"postRenderer,omitempty"` + // Cascade '--cascade' to helmv3 delete, available values: background, foreground, or orphan, default: background + Cascade *string `yaml:"cascade,omitempty"` + // Inherit is used to inherit a release template from a release or another release template Inherit Inherits `yaml:"inherit,omitempty"` } @@ -767,7 +772,7 @@ func ReleaseToID(r *ReleaseSpec) string { } // DeleteReleasesForSync deletes releases that are marked for deletion -func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int) []error { +func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int, cascade string) []error { errs := []error{} releases := st.Releases @@ -801,7 +806,9 @@ func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, h if release.Namespace != "" { args = append(args, "--namespace", release.Namespace) } - deletionFlags := st.appendConnectionFlags(args, release) + args = st.appendConnectionFlags(args, release) + deletionFlags := st.appendCascadeFlags(args, helm, release, cascade) + m.Lock() start := time.Now() if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil { @@ -2031,15 +2038,17 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [ } // DeleteReleases wrapper for executing helm delete on the releases -func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error { +func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool, cascade string) []error { return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error { st.ApplyOverrides(&release) flags := make([]string, 0) flags = st.appendConnectionFlags(flags, &release) + flags = st.appendCascadeFlags(flags, helm, &release, cascade) if release.Namespace != "" { flags = append(flags, "--namespace", release.Namespace) } + context := st.createHelmContext(&release, workerIndex) start := time.Now() diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 1d5cecf4d..404017125 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -2647,7 +2647,7 @@ func TestHelmState_Delete(t *testing.T) { helm.Lists[exectest.ListKey{Filter: "^" + name + "$", Flags: tt.flags}] = name } affectedReleases := AffectedReleases{} - errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge) + errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge, "") if errs != nil { if !tt.wantErr || len(affectedReleases.Failed) != 1 || affectedReleases.Failed[0].Name != release.Name { t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index 1bc4da3d9..35f732ce0 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) { run(testcase{ subject: "baseline", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, - want: "foo-values-76d4857b56", + want: "foo-values-6cbf8f5f9f", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-55d59fb487", + want: "foo-values-6cb9d4f956", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]interface{}{"k": "v"}, - want: "foo-values-7c8d99f9f7", + want: "foo-values-5bdffb5f4b", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-7467f76549", + want: "foo-values-6595bd68c6", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-dbdf465b7", + want: "bar-values-75698946b", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-6d4874d757", + want: "myns-foo-values-5bf477bbfb", }) for id, n := range ids { diff --git a/pkg/app/mocks_test.go b/pkg/testutil/mocks.go similarity index 83% rename from pkg/app/mocks_test.go rename to pkg/testutil/mocks.go index 685b0b5e1..d29cf251c 100644 --- a/pkg/app/mocks_test.go +++ b/pkg/testutil/mocks.go @@ -1,6 +1,7 @@ -package app +package testutil import ( + "github.com/blang/semver" "helm.sh/helm/v3/pkg/chart" "github.com/helmfile/helmfile/pkg/helmexec" @@ -9,15 +10,34 @@ import ( type noCallHelmExec struct { } -type versionOnlyHelmExec struct { +type V3HelmExec struct { *noCallHelmExec isHelm3 bool } -func (helm *versionOnlyHelmExec) IsHelm3() bool { +func NewV3HelmExec(isHelm3 bool) *V3HelmExec { + return &V3HelmExec{noCallHelmExec: &noCallHelmExec{}, isHelm3: isHelm3} +} + +type VersionHelmExec struct { + *noCallHelmExec + version string +} + +func NewVersionHelmExec(version string) *VersionHelmExec { + return &VersionHelmExec{noCallHelmExec: &noCallHelmExec{}, version: version} +} + +func (helm *V3HelmExec) IsHelm3() bool { return helm.isHelm3 } +func (helm *VersionHelmExec) IsVersionAtLeast(ver string) bool { + currentSemVer := semver.MustParse(helm.version) + verSemVer := semver.MustParse(ver) + return currentSemVer.GTE(verSemVer) +} + func (helm *noCallHelmExec) doPanic() { panic("unexpected call to helm") }