diff --git a/docs/index.md b/docs/index.md index 59d3442f..4c82e641 100644 --- a/docs/index.md +++ b/docs/index.md @@ -328,6 +328,9 @@ releases: reuseValues: false # set `false` to uninstall this release on sync. (default true) installed: true + # Defines the strategy to use when updating. Possible value is: + # - "reinstallIfForbidden": Performs an uninstall before the update only if the update is forbidden (e.g., due to permission issues or conflicts). + updateStrategy: "" # restores previous state in case of failed release (default false) atomic: true # when true, cleans up any new resources created during a failed release (default false) diff --git a/pkg/app/app_diff_test.go b/pkg/app/app_diff_test.go index d9f7b691..3f353e92 100644 --- a/pkg/app/app_diff_test.go +++ b/pkg/app/app_diff_test.go @@ -415,4 +415,28 @@ releases: }, }) }) + + t.Run("show diff on changed selected release with reinstall", func(t *testing.T) { + check(t, testcase{ + helmfile: ` +releases: +- name: a + chart: incubator/raw + namespace: default + updateStrategy: reinstallIfForbidden +- name: b + chart: incubator/raw + namespace: default +`, + selectors: []string{"name=a"}, + lists: map[exectest.ListKey]string{ + {Filter: "^a$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default +`, + }, + diffed: []exectest.Release{ + {Name: "a", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values"}}, + }, + }) + }) } diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index cf38aa2e..b14543f4 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -220,6 +220,97 @@ releases: } } +func TestUpdateStrategyParamValidation(t *testing.T) { + cases := []struct { + files map[string]string + updateStrategy string + isValid bool + }{ + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: reinstallIfForbidden +`}, + "reinstallIfForbidden", + true}, + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: reinstallIfForbidden +`}, + "reinstallIfForbidden", + true}, + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: +`}, + "", + true}, + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: foo +`}, + "foo", + false}, + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: reinstal +`}, + "reinstal", + false}, + {map[string]string{ + "/path/to/helmfile.yaml": `releases: +- name: zipkin + chart: stable/zipkin + updateStrategy: reinstall1 +`}, + "reinstall1", + false}, + } + + for idx, c := range cases { + fs := testhelper.NewTestFs(c.files) + app := &App{ + OverrideHelmBinary: DefaultHelmBinary, + OverrideKubeContext: "default", + Logger: newAppTestLogger(), + Namespace: "", + Env: "default", + FileOrDir: "helmfile.yaml", + } + + expectNoCallsToHelm(app) + + app = injectFs(app, fs) + + err := app.ForEachState( + Noop, + false, + SetFilter(true), + ) + + if c.isValid && err != nil { + t.Errorf("[case: %d] Unexpected error for valid case: %v", idx, err) + } else if !c.isValid { + var invalidUpdateStrategy state.InvalidUpdateStrategyError + invalidUpdateStrategy.UpdateStrategy = c.updateStrategy + if err == nil { + t.Errorf("[case: %d] Expected error for invalid case", idx) + } else if !strings.Contains(err.Error(), invalidUpdateStrategy.Error()) { + t.Errorf("[case: %d] Unexpected error returned for invalid case\ngot: %v\nexpected underlying error: %s", idx, err, invalidUpdateStrategy.Error()) + } + } + } +} + func TestVisitDesiredStatesWithReleasesFiltered_Issue1008_MissingNonDefaultEnvInBase(t *testing.T) { files := map[string]string{ "/path/to/base.yaml": ` @@ -3076,6 +3167,97 @@ baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 defau concurrency: 1, }, // + // install with upgrade with reinstallIfForbidden + // + { + name: "install-with-upgrade-with-reinstallIfForbidden", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: baz + chart: stable/mychart3 + disableValidationOnInstall: true + updateStrategy: reinstallIfForbidden +- name: foo + chart: stable/mychart1 + disableValidationOnInstall: true + needs: + - bar +- name: bar + chart: stable/mychart2 + disableValidation: true + updateStrategy: reinstallIfForbidden +`, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + {Name: "foo", Chart: "stable/mychart1", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + {Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + {Filter: "^foo$", Flags: listFlags("", "default")}: ``, + {Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default +`, + {Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default +`, + }, + upgraded: []exectest.Release{ + {Name: "baz", Flags: []string{"--kube-context", "default"}}, + {Name: "bar", Flags: []string{"--kube-context", "default"}}, + {Name: "foo", Flags: []string{"--kube-context", "default"}}, + }, + deleted: []exectest.Release{}, + concurrency: 1, + }, + // + // install with upgrade and --skip-diff-on-install with reinstallIfForbidden + // + { + name: "install-with-upgrade-with-skip-diff-on-install-with-reinstallIfForbidden", + loc: location(), + skipDiffOnInstall: true, + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: baz + chart: stable/mychart3 + disableValidationOnInstall: true + updateStrategy: reinstallIfForbidden +- name: foo + chart: stable/mychart1 + disableValidationOnInstall: true + needs: + - bar +- name: bar + chart: stable/mychart2 + disableValidation: true + updateStrategy: reinstallIfForbidden +`, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + {Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + {Filter: "^foo$", Flags: listFlags("", "default")}: ``, + {Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default +`, + {Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default +`, + }, + upgraded: []exectest.Release{ + {Name: "baz", Flags: []string{"--kube-context", "default"}}, + {Name: "bar", Flags: []string{"--kube-context", "default"}}, + {Name: "foo", Flags: []string{"--kube-context", "default"}}, + }, + concurrency: 1, + }, + // // upgrades // { @@ -3772,7 +3954,7 @@ releases: } for flagIdx := range wantDeletes[relIdx].Flags { if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] { - t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx]) + t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx]) } } } diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index be176bf9..f54413e0 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "dario.cat/mergo" "github.com/helmfile/vals" @@ -285,6 +286,18 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba } } + // Validate updateStrategy value if set in the releases + for i := range finalState.Releases { + if finalState.Releases[i].UpdateStrategy != "" { + if !slices.Contains(state.ValidUpdateStrategyValues, finalState.Releases[i].UpdateStrategy) { + return nil, &state.StateLoadError{ + Msg: fmt.Sprintf("failed to read %s", finalState.FilePath), + Cause: &state.InvalidUpdateStrategyError{UpdateStrategy: finalState.Releases[i].UpdateStrategy}, + } + } + } + } + finalState.OrginReleases = finalState.Releases return finalState, nil } diff --git a/pkg/app/testdata/app_diff_test/show_diff_on_changed_selected_release_with_reinstall b/pkg/app/testdata/app_diff_test/show_diff_on_changed_selected_release_with_reinstall new file mode 100644 index 00000000..66b419fa --- /dev/null +++ b/pkg/app/testdata/app_diff_test/show_diff_on_changed_selected_release_with_reinstall @@ -0,0 +1,11 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +1 release(s) matching name=a found in helmfile.yaml + +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/a + +processing releases in group 1/1: default/default/a +changing working directory back to "/path/to" diff --git a/pkg/app/testdata/testapply/install-with-upgrade-with-reinstallifforbidden/log b/pkg/app/testdata/testapply/install-with-upgrade-with-reinstallifforbidden/log new file mode 100644 index 00000000..22fc346b --- /dev/null +++ b/pkg/app/testdata/testapply/install-with-upgrade-with-reinstallifforbidden/log @@ -0,0 +1,35 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +3 release(s) found in helmfile.yaml + +Affected releases are: + bar (stable/mychart2) UPDATED + baz (stable/mychart3) UPDATED + foo (stable/mychart1) UPDATED + +invoking preapply hooks for 2 groups of releases in this order: +GROUP RELEASES +1 default//foo +2 default//baz, default//bar + +invoking preapply hooks for releases in group 1/2: default//foo +invoking preapply hooks for releases in group 2/2: default//baz, default//bar +processing 2 groups of releases in this order: +GROUP RELEASES +1 default//baz, default//bar +2 default//foo + +processing releases in group 1/2: default//baz, default//bar +update strategy - sync success +update strategy - sync success +processing releases in group 2/2: default//foo +getting deployed release version failed: Failed to get the version for: mychart1 + +UPDATED RELEASES: +NAME NAMESPACE CHART VERSION DURATION +baz stable/mychart3 3.1.0 0s +bar stable/mychart2 3.1.0 0s +foo stable/mychart1 0s + +changing working directory back to "/path/to" diff --git a/pkg/app/testdata/testapply/install-with-upgrade-with-skip-diff-on-install-with-reinstallifforbidden/log b/pkg/app/testdata/testapply/install-with-upgrade-with-skip-diff-on-install-with-reinstallifforbidden/log new file mode 100644 index 00000000..22fc346b --- /dev/null +++ b/pkg/app/testdata/testapply/install-with-upgrade-with-skip-diff-on-install-with-reinstallifforbidden/log @@ -0,0 +1,35 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +3 release(s) found in helmfile.yaml + +Affected releases are: + bar (stable/mychart2) UPDATED + baz (stable/mychart3) UPDATED + foo (stable/mychart1) UPDATED + +invoking preapply hooks for 2 groups of releases in this order: +GROUP RELEASES +1 default//foo +2 default//baz, default//bar + +invoking preapply hooks for releases in group 1/2: default//foo +invoking preapply hooks for releases in group 2/2: default//baz, default//bar +processing 2 groups of releases in this order: +GROUP RELEASES +1 default//baz, default//bar +2 default//foo + +processing releases in group 1/2: default//baz, default//bar +update strategy - sync success +update strategy - sync success +processing releases in group 2/2: default//foo +getting deployed release version failed: Failed to get the version for: mychart1 + +UPDATED RELEASES: +NAME NAMESPACE CHART VERSION DURATION +baz stable/mychart3 3.1.0 0s +bar stable/mychart2 3.1.0 0s +foo stable/mychart1 0s + +changing working directory back to "/path/to" diff --git a/pkg/exectest/helm.go b/pkg/exectest/helm.go index 9daaf0ec..6206e86f 100644 --- a/pkg/exectest/helm.go +++ b/pkg/exectest/helm.go @@ -56,9 +56,10 @@ type Release struct { } type Affected struct { - Upgraded []*Release - Deleted []*Release - Failed []*Release + Upgraded []*Release + Reinstalled []*Release + Deleted []*Release + Failed []*Release } func (helm *Helm) UpdateDeps(chart string) error { @@ -107,7 +108,24 @@ func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyF return nil } func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error { - if strings.Contains(name, "error") { + if strings.Contains(name, "forbidden") { + releaseExists := false + for _, release := range helm.Releases { + if release.Name == name { + releaseExists = true + } + } + releaseDeleted := false + for _, release := range helm.Deleted { + if release.Name == name { + releaseDeleted = true + } + } + // Only fail if the release is present in the helm.Releases to simulate a forbidden update if it exists + if releaseExists && !releaseDeleted { + return fmt.Errorf("cannot patch %q with kind StatefulSet: StatefulSet.apps %q is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", name, name) + } + } else if strings.Contains(name, "error") { return errors.New("error") } helm.sync(helm.ReleasesMutex, func() { diff --git a/pkg/state/create.go b/pkg/state/create.go index 97e91823..1c5a97c4 100644 --- a/pkg/state/create.go +++ b/pkg/state/create.go @@ -26,6 +26,8 @@ const ( DefaultHCLFileExtension = ".hcl" ) +var ValidUpdateStrategyValues = []string{UpdateStrategyReinstallIfForbidden} + type StateLoadError struct { Msg string Cause error @@ -43,6 +45,14 @@ func (e *UndefinedEnvError) Error() string { return fmt.Sprintf("environment \"%s\" is not defined", e.Env) } +type InvalidUpdateStrategyError struct { + UpdateStrategy string +} + +func (e *InvalidUpdateStrategyError) Error() string { + return fmt.Sprintf("updateStrategy %q is invalid, valid values are: %s or not set", e.UpdateStrategy, strings.Join(ValidUpdateStrategyValues, ", ")) +} + type StateCreator struct { logger *zap.SugaredLogger diff --git a/pkg/state/state.go b/pkg/state/state.go index d8e4013c..03f22ed4 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -43,6 +43,9 @@ const ( // This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the // --timeout flag is missingl EmptyTimeout = -1 + + // Valid enum for updateStrategy values + UpdateStrategyReinstallIfForbidden = "reinstallIfForbidden" ) // ReleaseSetSpec is release set spec @@ -277,6 +280,8 @@ type ReleaseSpec struct { Force *bool `yaml:"force,omitempty"` // Installed, when set to true, `delete --purge` the release Installed *bool `yaml:"installed,omitempty"` + // UpdateStrategy, when set, indicate the strategy to use to update the release + UpdateStrategy string `yaml:"updateStrategy,omitempty"` // Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt Atomic *bool `yaml:"atomic,omitempty"` // CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command @@ -467,6 +472,7 @@ type SetValue struct { // AffectedReleases hold the list of released that where updated, deleted, or in error type AffectedReleases struct { Upgraded []*ReleaseSpec + Reinstalled []*ReleaseSpec Deleted []*ReleaseSpec Failed []*ReleaseSpec DeleteFailed []*ReleaseSpec @@ -1037,20 +1043,24 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme } m.Unlock() } - } else if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil { - m.Lock() - affectedReleases.Failed = append(affectedReleases.Failed, release) - m.Unlock() - relErr = newReleaseFailedError(release, err) + } else if release.UpdateStrategy == UpdateStrategyReinstallIfForbidden { + relErr = st.performSyncOrReinstallOfRelease(affectedReleases, helm, context, release, chart, m, flags...) } else { - m.Lock() - affectedReleases.Upgraded = append(affectedReleases.Upgraded, release) - m.Unlock() - installedVersion, err := st.getDeployedVersion(context, helm, release) - if err != nil { // err is not really impacting so just log it - st.logger.Debugf("getting deployed release version failed: %v", err) + if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil { + m.Lock() + affectedReleases.Failed = append(affectedReleases.Failed, release) + m.Unlock() + relErr = newReleaseFailedError(release, err) } else { - release.installedVersion = installedVersion + m.Lock() + affectedReleases.Upgraded = append(affectedReleases.Upgraded, release) + m.Unlock() + installedVersion, err := st.getDeployedVersion(context, helm, release) + if err != nil { // err is not really impacting so just log it + st.logger.Debugf("getting deployed release version failed: %v", err) + } else { + release.installedVersion = installedVersion + } } } @@ -1096,6 +1106,77 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme return nil } +func (st *HelmState) performSyncOrReinstallOfRelease(affectedReleases *AffectedReleases, helm helmexec.Interface, context helmexec.HelmContext, release *ReleaseSpec, chart string, m *sync.Mutex, flags ...string) *ReleaseError { + if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil { + st.logger.Debugf("update strategy - sync failed: %s", err.Error()) + // Only fail if a different error than forbidden updates + if !strings.Contains(err.Error(), "Forbidden: updates") { + st.logger.Debugf("update strategy - sync failed not due to Forbidden updates") + m.Lock() + affectedReleases.Failed = append(affectedReleases.Failed, release) + m.Unlock() + return newReleaseFailedError(release, err) + } + } else { + st.logger.Debugf("update strategy - sync success") + m.Lock() + affectedReleases.Upgraded = append(affectedReleases.Upgraded, release) + m.Unlock() + installedVersion, err := st.getDeployedVersion(context, helm, release) + if err != nil { // err is not really impacting so just log it + st.logger.Debugf("update strategy - getting deployed release version failed: %v", err) + } else { + release.installedVersion = installedVersion + } + return nil + } + + st.logger.Infof("Failed to sync due to forbidden updates, attempting to reinstall %q allowed by update strategy", release.Name) + installed, err := st.isReleaseInstalled(context, helm, *release) + if err != nil { + return newReleaseFailedError(release, err) + } + if installed { + var args []string + if release.Namespace != "" { + args = append(args, "--namespace", release.Namespace) + } + deleteWaitFlag := true + release.DeleteWait = &deleteWaitFlag + args = st.appendDeleteWaitFlags(args, release) + deletionFlags := st.appendConnectionFlags(args, release) + m.Lock() + if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil { + affectedReleases.Failed = append(affectedReleases.Failed, release) + return newReleaseFailedError(release, err) + } else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil { + affectedReleases.Failed = append(affectedReleases.Failed, release) + return newReleaseFailedError(release, err) + } else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil { + affectedReleases.Failed = append(affectedReleases.Failed, release) + return newReleaseFailedError(release, err) + } + m.Unlock() + } + if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil { + m.Lock() + affectedReleases.Failed = append(affectedReleases.Failed, release) + m.Unlock() + return newReleaseFailedError(release, err) + } else { + m.Lock() + affectedReleases.Reinstalled = append(affectedReleases.Reinstalled, release) + m.Unlock() + installedVersion, err := st.getDeployedVersion(context, helm, release) + if err != nil { // err is not really impacting so just log it + st.logger.Debugf("update strategy - getting deployed release version failed: %v", err) + } else { + release.installedVersion = installedVersion + } + } + return nil +} + func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) { flags := st.kubeConnectionFlags(release) if release.Namespace != "" { @@ -3735,6 +3816,28 @@ func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) { } logger.Info(tbl.String()) } + if len(ar.Reinstalled) > 0 { + logger.Info("\nREINSTALLED RELEASES:") + tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"}, + prettytable.Column{Header: "NAMESPACE", MinWidth: 6}, + prettytable.Column{Header: "CHART", MinWidth: 6}, + prettytable.Column{Header: "VERSION", MinWidth: 6}, + prettytable.Column{Header: "DURATION", AlignRight: true}, + ) + tbl.Separator = " " + for _, release := range ar.Reinstalled { + modifiedChart, modErr := hideChartCredentials(release.Chart) + if modErr != nil { + logger.Warn("Could not modify chart credentials, %v", modErr) + continue + } + err := tbl.AddRow(release.Name, release.Namespace, modifiedChart, release.installedVersion, release.duration.Round(time.Second)) + if err != nil { + logger.Warn("Could not add row, %v", err) + } + } + logger.Info(tbl.String()) + } if len(ar.Deleted) > 0 { logger.Info("\nDELETED RELEASES:") tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"}, diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index a02ea593..335613bd 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -1640,6 +1640,112 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) { } } +func TestHelmState_SyncReleasesAffectedReleasesWithReinstallIfForbidden(t *testing.T) { + no := false + tests := []struct { + name string + releases []ReleaseSpec + installed []bool + wantAffected exectest.Affected + }{ + { + name: "2 new", + releases: []ReleaseSpec{ + { + Name: "releaseNameFoo-forbidden", + Chart: "foo", + UpdateStrategy: "reinstallIfForbidden", + }, + { + Name: "releaseNameBar-forbidden", + Chart: "foo", + UpdateStrategy: "reinstallIfForbidden", + }, + }, + wantAffected: exectest.Affected{ + Upgraded: []*exectest.Release{ + {Name: "releaseNameFoo-forbidden", Flags: []string{}}, + {Name: "releaseNameBar-forbidden", Flags: []string{}}, + }, + Reinstalled: nil, + Deleted: nil, + Failed: nil, + }, + }, + { + name: "1 removed, 1 new, 1 reinstalled first new", + releases: []ReleaseSpec{ + { + Name: "releaseNameFoo-forbidden", + Chart: "foo", + UpdateStrategy: "reinstallIfForbidden", + }, + { + Name: "releaseNameBar", + Chart: "foo", + UpdateStrategy: "reinstallIfForbidden", + Installed: &no, + }, + { + Name: "releaseNameFoo-forbidden", + Chart: "foo", + UpdateStrategy: "reinstallIfForbidden", + }, + }, + installed: []bool{true, true, true}, + wantAffected: exectest.Affected{ + Upgraded: []*exectest.Release{ + {Name: "releaseNameFoo-forbidden", Flags: []string{}}, + }, + Reinstalled: []*exectest.Release{ + {Name: "releaseNameFoo-forbidden", Flags: []string{}}, + }, + Deleted: []*exectest.Release{ + {Name: "releaseNameBar", Flags: []string{}}, + }, + Failed: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &HelmState{ + ReleaseSetSpec: ReleaseSetSpec{ + Releases: tt.releases, + }, + logger: logger, + valsRuntime: valsRuntime, + RenderedValues: map[string]any{}, + } + helm := &exectest.Helm{ + Lists: map[exectest.ListKey]string{}, + } + //simulate the release is already installed + for i, release := range tt.releases { + if tt.installed != nil && tt.installed[i] { + helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = release.Name + } + } + + affectedReleases := AffectedReleases{} + if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil { + if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) { + t.Errorf("HelmState.SyncReleases() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed) + } //else expected error + } + if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) { + t.Errorf("HelmState.SyncReleases() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded) + } + if !testEq(affectedReleases.Reinstalled, tt.wantAffected.Reinstalled) { + t.Errorf("HelmState.SyncReleases() reinstalled failed for [%s] = %v, want %v", tt.name, affectedReleases.Reinstalled, tt.wantAffected.Reinstalled) + } + if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) { + t.Errorf("HelmState.SyncReleases() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted) + } + }) + } +} + func testEq(a []*ReleaseSpec, b []*exectest.Release) bool { // If one is nil, the other must also be nil. if (a == nil) != (b == nil) { diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index ca3f493c..7e082d51 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-7d454b9558", + want: "foo-values-67dc97cbcb", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-59c86d55bf", + want: "foo-values-75d7c4758c", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]any{"k": "v"}, - want: "foo-values-6f87c5cd79", + want: "foo-values-685f8cf685", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-5dfd748475", + want: "foo-values-75597d9c57", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-858b9c55cc", + want: "bar-values-7b77df65ff", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-58dc9c6667", + want: "myns-foo-values-85f979545c", }) for id, n := range ids {