From 870cc03c700d43b8bc26099663858f7d2a5eae47 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Fri, 10 Apr 2020 08:22:33 +0900 Subject: [PATCH] feat: `helmfile diff --detailed-exitcode` should also detect deletions (#1186) Resolves #499 Resolves #1072 --- pkg/app/app.go | 98 +-- pkg/app/app_test.go | 12 + pkg/app/destroy_test.go | 2 + pkg/app/diff_test.go | 1308 +++++++++++++++++++++++++++++++++++++++ pkg/app/run.go | 129 +++- pkg/state/state.go | 10 +- 6 files changed, 1479 insertions(+), 80 deletions(-) create mode 100644 pkg/app/diff_test.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 3d37c343..bc1162ec 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -116,12 +116,20 @@ func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { } func (a *App) Diff(c DiffConfigProvider) error { - var deferredErrs []error + var allDiffDetectedErrs []error - err := a.ForEachStateFiltered(func(run *Run) []error { + var affectedAny bool + + err := a.ForEachState(func(run *Run) (bool, []error) { var criticalErrs []error - errs := run.Diff(c) + msg, matched, affected, errs := run.Diff(c) + + if msg != nil { + a.Logger.Info(*msg) + } + + affectedAny = affectedAny || affected for i := range errs { switch e := errs[i].(type) { @@ -129,7 +137,7 @@ func (a *App) Diff(c DiffConfigProvider) error { switch e.Code { case 2: // See https://github.com/roboll/helmfile/issues/874 - deferredErrs = append(deferredErrs, e) + allDiffDetectedErrs = append(allDiffDetectedErrs, e) default: criticalErrs = append(criticalErrs, e) } @@ -138,14 +146,14 @@ func (a *App) Diff(c DiffConfigProvider) error { } } - return criticalErrs + return matched, criticalErrs }) if err != nil { return err } - if len(deferredErrs) > 0 { + if c.DetailedExitcode() && (len(allDiffDetectedErrs) > 0 || affectedAny) { // We take the first release error w/ exit status 2 (although all the defered errs should have exit status 2) // to just let helmfile itself to exit with 2 // See https://github.com/roboll/helmfile/issues/749 @@ -806,84 +814,28 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { Set: c.Set(), } - var changedReleases []state.ReleaseSpec - var deletingReleases []state.ReleaseSpec - var planningErrs []error + infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts) + if len(errs) > 0 { + return false, false, errs + } - // TODO Better way to detect diff on only filtered releases - { - changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), false, diffOpts) - - var err error - deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases) - if err != nil { - planningErrs = append(planningErrs, err) + if releasesToBeDeleted == nil && releasesToBeUpdated == nil { + if infoMsg != nil { + logger := c.Logger() + logger.Infof("") + logger.Infof(*infoMsg) } - } - - fatalErrs := []error{} - - for _, e := range planningErrs { - switch err := e.(type) { - case *state.ReleaseError: - if err.Code != 2 { - fatalErrs = append(fatalErrs, e) - } - default: - fatalErrs = append(fatalErrs, e) - } - } - - if len(fatalErrs) > 0 { - return false, false, fatalErrs - } - - releasesToBeDeleted := map[string]state.ReleaseSpec{} - for _, r := range deletingReleases { - id := state.ReleaseToID(&r) - releasesToBeDeleted[id] = r - } - - releasesToBeUpdated := map[string]state.ReleaseSpec{} - for _, r := range changedReleases { - id := state.ReleaseToID(&r) - - // If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade` - if _, ok := releasesToBeDeleted[id]; !ok { - releasesToBeUpdated[id] = r - } - } - - // sync only when there are changes - if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 { - // TODO better way to get the logger - logger := c.Logger() - logger.Infof("") - logger.Infof("No affected releases") return true, false, nil } - names := []string{} - for _, r := range releasesToBeUpdated { - names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart)) - } - for _, r := range releasesToBeDeleted { - names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart)) - } - // Make the output deterministic for testing purpose - sort.Strings(names) - - infoMsg := fmt.Sprintf(`Affected releases are: -%s -`, strings.Join(names, "\n")) confMsg := fmt.Sprintf(`%s Do you really want to apply? Helmfile will apply all your changes, as shown above. -`, infoMsg) +`, *infoMsg) interactive := c.Interactive() if !interactive { - a.Logger.Debug(infoMsg) + a.Logger.Debug(*infoMsg) } syncErrs := []error{} diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index d35d7865..ddcbcce7 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2524,6 +2524,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 54: merged environment: &{default map[] map[]} +10 release(s) found in helmfile.yaml + worker 1/1 started worker 1/1 finished worker 1/1 started @@ -2711,6 +2713,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 10: merged environment: &{default map[] map[]} +3 release(s) found in helmfile.yaml + worker 1/1 started worker 1/1 finished worker 1/1 started @@ -2989,6 +2993,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 12: merged environment: &{default map[] map[]} +2 release(s) found in helmfile.yaml + worker 1/1 started worker 1/1 finished worker 1/1 started @@ -3335,6 +3341,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 23: merged environment: &{default map[] map[]} +2 release(s) matching app=test found in helmfile.yaml + worker 1/1 started worker 1/1 finished worker 1/1 started @@ -3473,6 +3481,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 23: merged environment: &{default map[] map[]} +0 release(s) matching app=test_non_existent found in helmfile.yaml + `, }, // @@ -3535,6 +3545,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 9: merged environment: &{default map[] map[]} +2 release(s) found in helmfile.yaml + worker 1/1 started worker 1/1 finished worker 1/1 started diff --git a/pkg/app/destroy_test.go b/pkg/app/destroy_test.go index 4fd21b8e..c7157348 100644 --- a/pkg/app/destroy_test.go +++ b/pkg/app/destroy_test.go @@ -284,6 +284,8 @@ second-pass rendering result of "helmfile.yaml.part.0": 54: merged environment: &{default map[] map[]} +10 release(s) found in helmfile.yaml + processing 5 groups of releases in this order: GROUP RELEASES 1 frontend-v3, frontend-v2, frontend-v1 diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go new file mode 100644 index 00000000..6abf91be --- /dev/null +++ b/pkg/app/diff_test.go @@ -0,0 +1,1308 @@ +package app + +import ( + "bufio" + "bytes" + "github.com/roboll/helmfile/pkg/exectest" + "github.com/roboll/helmfile/pkg/helmexec" + "github.com/roboll/helmfile/pkg/testhelper" + "github.com/variantdev/vals" + "go.uber.org/zap" + "io" + "path/filepath" + "sync" + "testing" +) + +type diffConfig struct { + args string + values []string + retainValuesFiles bool + set []string + validate bool + skipDeps bool + includeTests bool + suppressSecrets bool + suppressDiff bool + noColor bool + context int + concurrency int + detailedExitcode bool + interactive bool + logger *zap.SugaredLogger +} + +func (a diffConfig) Args() string { + return a.args +} + +func (a diffConfig) Values() []string { + return a.values +} + +func (a diffConfig) Set() []string { + return a.set +} + +func (a diffConfig) Validate() bool { + return a.validate +} + +func (a diffConfig) SkipDeps() bool { + return a.skipDeps +} + +func (a diffConfig) IncludeTests() bool { + return a.includeTests +} + +func (a diffConfig) SuppressSecrets() bool { + return a.suppressSecrets +} + +func (a diffConfig) SuppressDiff() bool { + return a.suppressDiff +} + +func (a diffConfig) NoColor() bool { + return a.noColor +} + +func (a diffConfig) Context() int { + return a.context +} + +func (a diffConfig) Concurrency() int { + return a.concurrency +} + +func (a diffConfig) DetailedExitcode() bool { + return a.detailedExitcode +} + +func (a diffConfig) Interactive() bool { + return a.interactive +} + +func (a diffConfig) Logger() *zap.SugaredLogger { + return a.logger +} + +func (a diffConfig) RetainValuesFiles() bool { + return a.retainValuesFiles +} + +func TestDiff(t *testing.T) { + testcases := []struct { + name string + loc string + ns string + concurrency int + detailedExitcode bool + error string + files map[string]string + selectors []string + lists map[exectest.ListKey]string + diffs map[exectest.DiffKey]error + upgraded []exectest.Release + deleted []exectest.Release + log string + }{ + // + // complex test cases for smoke testing + // + { + name: "smoke", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: database + chart: charts/mysql + needs: + - logging +- name: frontend-v1 + chart: charts/frontend + installed: false + needs: + - servicemesh + - logging + - backend-v1 +- name: frontend-v2 + chart: charts/frontend + needs: + - servicemesh + - logging + - backend-v2 +- name: frontend-v3 + chart: charts/frontend + needs: + - servicemesh + - logging + - backend-v2 +- name: backend-v1 + chart: charts/backend + installed: false + needs: + - servicemesh + - logging + - database + - anotherbackend +- name: backend-v2 + chart: charts/backend + needs: + - servicemesh + - logging + - database + - anotherbackend +- name: anotherbackend + chart: charts/anotherbackend + needs: + - servicemesh + - logging + - database +- name: servicemesh + chart: charts/istio + needs: + - logging +- name: logging + chart: charts/fluent-bit +- name: front-proxy + chart: stable/envoy +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + // noop on frontend-v2 + exectest.DiffKey{Name: "frontend-v2", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: nil, + // install frontend-v3 + exectest.DiffKey{Name: "frontend-v3", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + // upgrades + exectest.DiffKey{Name: "logging", Chart: "charts/fluent-bit", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "front-proxy", Chart: "stable/envoy", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "servicemesh", Chart: "charts/istio", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "database", Chart: "charts/mysql", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "backend-v2", Chart: "charts/backend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "anotherbackend", Chart: "charts/anotherbackend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + // delete frontend-v1 and backend-v1 + exectest.ListKey{Filter: "^frontend-v1$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +frontend-v1 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^backend-v1$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +backend-v1 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default +`, + }, + // Disable concurrency to avoid in-deterministic result + concurrency: 1, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: database + 3: chart: charts/mysql + 4: needs: + 5: - logging + 6: - name: frontend-v1 + 7: chart: charts/frontend + 8: installed: false + 9: needs: +10: - servicemesh +11: - logging +12: - backend-v1 +13: - name: frontend-v2 +14: chart: charts/frontend +15: needs: +16: - servicemesh +17: - logging +18: - backend-v2 +19: - name: frontend-v3 +20: chart: charts/frontend +21: needs: +22: - servicemesh +23: - logging +24: - backend-v2 +25: - name: backend-v1 +26: chart: charts/backend +27: installed: false +28: needs: +29: - servicemesh +30: - logging +31: - database +32: - anotherbackend +33: - name: backend-v2 +34: chart: charts/backend +35: needs: +36: - servicemesh +37: - logging +38: - database +39: - anotherbackend +40: - name: anotherbackend +41: chart: charts/anotherbackend +42: needs: +43: - servicemesh +44: - logging +45: - database +46: - name: servicemesh +47: chart: charts/istio +48: needs: +49: - logging +50: - name: logging +51: chart: charts/fluent-bit +52: - name: front-proxy +53: chart: stable/envoy +54: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: database + 3: chart: charts/mysql + 4: needs: + 5: - logging + 6: - name: frontend-v1 + 7: chart: charts/frontend + 8: installed: false + 9: needs: +10: - servicemesh +11: - logging +12: - backend-v1 +13: - name: frontend-v2 +14: chart: charts/frontend +15: needs: +16: - servicemesh +17: - logging +18: - backend-v2 +19: - name: frontend-v3 +20: chart: charts/frontend +21: needs: +22: - servicemesh +23: - logging +24: - backend-v2 +25: - name: backend-v1 +26: chart: charts/backend +27: installed: false +28: needs: +29: - servicemesh +30: - logging +31: - database +32: - anotherbackend +33: - name: backend-v2 +34: chart: charts/backend +35: needs: +36: - servicemesh +37: - logging +38: - database +39: - anotherbackend +40: - name: anotherbackend +41: chart: charts/anotherbackend +42: needs: +43: - servicemesh +44: - logging +45: - database +46: - name: servicemesh +47: chart: charts/istio +48: needs: +49: - logging +50: - name: logging +51: chart: charts/fluent-bit +52: - name: front-proxy +53: chart: stable/envoy +54: + +merged environment: &{default map[] map[]} +10 release(s) found in helmfile.yaml + +worker 1/1 started +worker 1/1 finished +worker 1/1 started +worker 1/1 finished +Affected releases are: + anotherbackend (charts/anotherbackend) UPDATED + backend-v1 (charts/backend) DELETED + backend-v2 (charts/backend) UPDATED + database (charts/mysql) UPDATED + front-proxy (stable/envoy) UPDATED + frontend-v1 (charts/frontend) DELETED + frontend-v3 (charts/frontend) UPDATED + logging (charts/fluent-bit) UPDATED + servicemesh (charts/istio) UPDATED + +`, + }, + // + // noop: no changes + // + { + name: "noop", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 +- name: foo + chart: mychart1 + installed: false + needs: + - bar +`, + }, + detailedExitcode: true, + error: "", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: nil, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: ``, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + }, + // + // install + // + { + name: "install", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: baz + chart: mychart3 +- name: foo + chart: mychart1 + needs: + - bar +- name: bar + chart: mychart2 +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "baz", Chart: "mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{}, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + concurrency: 1, + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: baz + 3: chart: mychart3 + 4: - name: foo + 5: chart: mychart1 + 6: needs: + 7: - bar + 8: - name: bar + 9: chart: mychart2 +10: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: baz + 3: chart: mychart3 + 4: - name: foo + 5: chart: mychart1 + 6: needs: + 7: - bar + 8: - name: bar + 9: chart: mychart2 +10: + +merged environment: &{default map[] map[]} +3 release(s) found in helmfile.yaml + +worker 1/1 started +worker 1/1 finished +worker 1/1 started +worker 1/1 finished +Affected releases are: + bar (mychart2) UPDATED + baz (mychart3) UPDATED + foo (mychart1) UPDATED + +`, + }, + // + // upgrades + // + { + name: "upgrade when foo needs bar", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 +- name: foo + chart: mychart1 + needs: + - bar +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when bar needs foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 +- name: bar + chart: mychart2 + needs: + - foo +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when foo needs bar, with ns override", + loc: location(), + ns: "testNamespace", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 +- name: foo + chart: mychart1 + needs: + - bar +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when bar needs foo, with ns override", + loc: location(), + ns: "testNamespace", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 +- name: bar + chart: mychart2 + needs: + - foo +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when ns1/foo needs ns2/bar", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 + namespace: ns1 + needs: + - ns2/bar +- name: bar + chart: mychart2 + namespace: ns2 +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when ns2/bar needs ns1/foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 + namespace: ns2 + needs: + - ns1/foo +- name: foo + chart: mychart1 + namespace: ns1 +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when tns1/ns1/foo needs tns2/ns2/bar", + loc: location(), + + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 + namespace: ns1 + tillerNamespace: tns1 + needs: + - tns2/ns2/bar +- name: bar + chart: mychart2 + namespace: ns2 + tillerNamespace: tns2 +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + }, + { + name: "upgrade when tns2/ns2/bar needs tns1/ns1/foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 + namespace: ns2 + tillerNamespace: tns2 + needs: + - tns1/ns1/foo +- name: foo + chart: mychart1 + namespace: ns1 + tillerNamespace: tns1 +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: bar + 3: chart: mychart2 + 4: namespace: ns2 + 5: tillerNamespace: tns2 + 6: needs: + 7: - tns1/ns1/foo + 8: - name: foo + 9: chart: mychart1 +10: namespace: ns1 +11: tillerNamespace: tns1 +12: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: bar + 3: chart: mychart2 + 4: namespace: ns2 + 5: tillerNamespace: tns2 + 6: needs: + 7: - tns1/ns1/foo + 8: - name: foo + 9: chart: mychart1 +10: namespace: ns1 +11: tillerNamespace: tns1 +12: + +merged environment: &{default map[] map[]} +2 release(s) found in helmfile.yaml + +worker 1/1 started +worker 1/1 finished +worker 1/1 started +worker 1/1 finished +Affected releases are: + bar (mychart2) UPDATED + foo (mychart1) UPDATED + +`, + }, + // + // deletes: deleting all releases in the correct order + // + { + name: "delete foo and bar when foo needs bar", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 + installed: false +- name: foo + chart: mychart1 + installed: false + needs: + - bar +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + deleted: []exectest.Release{}, + }, + { + name: "delete foo and bar when bar needs foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 + installed: false + needs: + - foo +- name: foo + chart: mychart1 + installed: false +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + deleted: []exectest.Release{}, + }, + // + // upgrade and delete: upgrading one while deleting another + // + { + name: "delete foo when foo needs bar", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 +- name: foo + chart: mychart1 + installed: false + needs: + - bar +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + }, + { + name: "delete bar when foo needs bar", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: bar + chart: mychart2 + installed: false +- name: foo + chart: mychart1 + needs: + - bar +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + }, + { + name: "delete foo when bar needs foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 + installed: false +- name: bar + chart: mychart2 + needs: + - foo +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + }, + { + name: "delete bar when bar needs foo", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: mychart1 +- name: bar + chart: mychart2 + installed: false + needs: + - foo +`, + }, + detailedExitcode: true, + error: "Identified at least on change", + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "bar", Chart: "mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default +`, + exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `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 +`, + }, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + }, + // + // upgrades with selector + // + { + // see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747 + name: "upgrades with good selector", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +{{ $mark := "a" }} + +releases: +- name: kubernetes-external-secrets + chart: incubator/raw + namespace: kube-system + +- name: external-secrets + chart: incubator/raw + namespace: default + labels: + app: test + needs: + - kube-system/kubernetes-external-secrets + +- name: my-release + chart: incubator/raw + namespace: default + labels: + app: test + needs: + - default/external-secrets +`, + }, + selectors: []string{"app=test"}, + detailedExitcode: true, + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "external-secrets", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + upgraded: []exectest.Release{}, + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + error: "Identified at least on change", + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: + 2: + 3: releases: + 4: - name: kubernetes-external-secrets + 5: chart: incubator/raw + 6: namespace: kube-system + 7: + 8: - name: external-secrets + 9: chart: incubator/raw +10: namespace: default +11: labels: +12: app: test +13: needs: +14: - kube-system/kubernetes-external-secrets +15: +16: - name: my-release +17: chart: incubator/raw +18: namespace: default +19: labels: +20: app: test +21: needs: +22: - default/external-secrets +23: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: + 2: + 3: releases: + 4: - name: kubernetes-external-secrets + 5: chart: incubator/raw + 6: namespace: kube-system + 7: + 8: - name: external-secrets + 9: chart: incubator/raw +10: namespace: default +11: labels: +12: app: test +13: needs: +14: - kube-system/kubernetes-external-secrets +15: +16: - name: my-release +17: chart: incubator/raw +18: namespace: default +19: labels: +20: app: test +21: needs: +22: - default/external-secrets +23: + +merged environment: &{default map[] map[]} +2 release(s) matching app=test found in helmfile.yaml + +worker 1/1 started +worker 1/1 finished +worker 1/1 started +worker 1/1 finished +Affected releases are: + external-secrets (incubator/raw) UPDATED + my-release (incubator/raw) UPDATED + +`, + }, + { + // see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747 + name: "upgrades with bad selector", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +{{ $mark := "a" }} + +releases: +- name: kubernetes-external-secrets + chart: incubator/raw + namespace: kube-system + +- name: external-secrets + chart: incubator/raw + namespace: default + labels: + app: test + needs: + - kube-system/kubernetes-external-secrets + +- name: my-release + chart: incubator/raw + namespace: default + labels: + app: test + needs: + - default/external-secrets +`, + }, + selectors: []string{"app=test_non_existent"}, + detailedExitcode: true, + diffs: map[exectest.DiffKey]error{}, + upgraded: []exectest.Release{}, + error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: + 2: + 3: releases: + 4: - name: kubernetes-external-secrets + 5: chart: incubator/raw + 6: namespace: kube-system + 7: + 8: - name: external-secrets + 9: chart: incubator/raw +10: namespace: default +11: labels: +12: app: test +13: needs: +14: - kube-system/kubernetes-external-secrets +15: +16: - name: my-release +17: chart: incubator/raw +18: namespace: default +19: labels: +20: app: test +21: needs: +22: - default/external-secrets +23: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: + 2: + 3: releases: + 4: - name: kubernetes-external-secrets + 5: chart: incubator/raw + 6: namespace: kube-system + 7: + 8: - name: external-secrets + 9: chart: incubator/raw +10: namespace: default +11: labels: +12: app: test +13: needs: +14: - kube-system/kubernetes-external-secrets +15: +16: - name: my-release +17: chart: incubator/raw +18: namespace: default +19: labels: +20: app: test +21: needs: +22: - default/external-secrets +23: + +merged environment: &{default map[] map[]} +0 release(s) matching app=test_non_existent found in helmfile.yaml + +`, + }, + // + // error cases + // + { + name: "non-existent release in needs", + loc: location(), + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: baz + namespace: ns1 + chart: mychart3 +- name: foo + chart: mychart1 + needs: + - bar +`, + }, + detailedExitcode: true, + diffs: map[exectest.DiffKey]error{ + exectest.DiffKey{Name: "baz", Chart: "mychart3", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{}, + upgraded: []exectest.Release{}, + deleted: []exectest.Release{}, + concurrency: 1, + error: `in ./helmfile.yaml: "foo" depends on nonexistent release "bar"`, + log: `processing file "helmfile.yaml" in directory "." +first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: baz + 3: namespace: ns1 + 4: chart: mychart3 + 5: - name: foo + 6: chart: mychart1 + 7: needs: + 8: - bar + 9: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile.yaml.part.0": + 0: + 1: releases: + 2: - name: baz + 3: namespace: ns1 + 4: chart: mychart3 + 5: - name: foo + 6: chart: mychart1 + 7: needs: + 8: - bar + 9: + +merged environment: &{default map[] map[]} +2 release(s) found in helmfile.yaml + +err: "foo" depends on nonexistent release "bar" +`, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(tc.name, func(t *testing.T) { + wantUpgrades := tc.upgraded + wantDeletes := tc.deleted + + var helm = &exectest.Helm{ + FailOnUnexpectedList: true, + FailOnUnexpectedDiff: true, + Lists: tc.lists, + Diffs: tc.diffs, + DiffMutex: &sync.Mutex{}, + ChartsMutex: &sync.Mutex{}, + ReleasesMutex: &sync.Mutex{}, + } + + bs := &bytes.Buffer{} + + func() { + logReader, logWriter := io.Pipe() + + logFlushed := &sync.WaitGroup{} + // Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()` + logFlushed.Add(1) + go func() { + scanner := bufio.NewScanner(logReader) + for scanner.Scan() { + bs.Write(scanner.Bytes()) + bs.WriteString("\n") + } + logFlushed.Done() + }() + + defer func() { + // This is here to avoid data-trace on bytes buffer `bs` to capture logs + if err := logWriter.Close(); err != nil { + panic(err) + } + logFlushed.Wait() + }() + + logger := helmexec.NewLogger(logWriter, "debug") + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Errorf("unexpected error creating vals runtime: %v", err) + } + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + glob: filepath.Glob, + abs: filepath.Abs, + OverrideKubeContext: "default", + Env: "default", + Logger: logger, + helms: map[helmKey]helmexec.Interface{ + createHelmKey("helm", "default"): helm, + }, + valsRuntime: valsRuntime, + }, tc.files) + + if tc.ns != "" { + app.Namespace = tc.ns + } + + if tc.selectors != nil { + app.Selectors = tc.selectors + } + + diffErr := app.Diff(diffConfig{ + // if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic. + concurrency: tc.concurrency, + logger: logger, + detailedExitcode: tc.detailedExitcode, + }) + if tc.error == "" && diffErr != nil { + t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, diffErr) + } else if tc.error != "" && diffErr == nil { + t.Fatalf("expected error did not occur for data defined at %s", tc.loc) + } else if tc.error != "" && diffErr != nil && tc.error != diffErr.Error() { + t.Fatalf("invalid error: expected %q, got %q", tc.error, diffErr.Error()) + } + + if len(wantUpgrades) > len(helm.Releases) { + t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades)) + } + + for relIdx := range wantUpgrades { + if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name { + t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name) + } + for flagIdx := range wantUpgrades[relIdx].Flags { + if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] { + t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx]) + } + } + } + + if len(wantDeletes) > len(helm.Deleted) { + t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes)) + } + + for relIdx := range wantDeletes { + if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name { + t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name) + } + 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]) + } + } + } + }() + + if tc.log != "" { + actual := bs.String() + + diff, exists := testhelper.Diff(tc.log, actual, 3) + if exists { + t.Errorf("unexpected log for data defined %s:\nDIFF\n%s\nEOD", tc.loc, diff) + } + } + }) + } +} diff --git a/pkg/app/run.go b/pkg/app/run.go index 75b741e2..3dee83ab 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -1,9 +1,12 @@ package app import ( + "fmt" "github.com/roboll/helmfile/pkg/argparser" "github.com/roboll/helmfile/pkg/helmexec" "github.com/roboll/helmfile/pkg/state" + "sort" + "strings" ) type Run struct { @@ -61,21 +64,36 @@ func (r *Run) Status(c StatusesConfigProvider) []error { return r.state.ReleaseStatuses(r.helm, workers) } -func (r *Run) Diff(c DiffConfigProvider) []error { +func (r *Run) Diff(c DiffConfigProvider) (*string, bool, bool, []error) { st := r.state helm := r.helm ctx := r.ctx + allReleases := st.GetReleasesWithOverrides() + + toDiff, err := st.GetSelectedReleasesWithOverrides() + if err != nil { + return nil, false, false, []error{err} + } + + if len(toDiff) == 0 { + return nil, 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 = toDiff + if !c.SkipDeps() { if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { - return errs + return nil, false, false, errs } if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs + return nil, false, false, errs } } if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 { - return errs + return nil, false, false, errs } r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) @@ -85,9 +103,28 @@ func (r *Run) Diff(c DiffConfigProvider) []error { NoColor: c.NoColor(), Set: c.Set(), } - _, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), true, opts) - return errs + // Validate all releases for missing `needs` targets + st.Releases = allReleases + + if _, err := st.PlanReleases(false); err != nil { + return nil, false, false, []error{err} + } + + // Diff only targeted releases + + st.Releases = toDiff + + filtered := &Run{ + state: st, + helm: r.helm, + ctx: r.ctx, + Ask: r.Ask, + } + + infoMsg, updated, deleted, errs := filtered.diff(true, c.DetailedExitcode(), c, opts) + + return infoMsg, true, len(deleted) > 0 || len(updated) > 0, errs } func (r *Run) Test(c TestConfigProvider) []error { @@ -124,3 +161,83 @@ func (r *Run) Lint(c LintConfigProvider) []error { } return st.LintReleases(helm, values, args, workers, opts) } + +func (run *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfigProvider, diffOpts *state.DiffOpts) (*string, map[string]state.ReleaseSpec, map[string]state.ReleaseSpec, []error) { + st := run.state + helm := run.helm + + var changedReleases []state.ReleaseSpec + var deletingReleases []state.ReleaseSpec + var planningErrs []error + + // TODO Better way to detect diff on only filtered releases + { + changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), triggerCleanupEvent, diffOpts) + + var err error + deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases) + if err != nil { + planningErrs = append(planningErrs, err) + } + } + + fatalErrs := []error{} + + for _, e := range planningErrs { + switch err := e.(type) { + case *state.ReleaseError: + if err.Code != 2 { + fatalErrs = append(fatalErrs, e) + } + default: + fatalErrs = append(fatalErrs, e) + } + } + + if len(fatalErrs) > 0 { + return nil, nil, nil, fatalErrs + } + + releasesToBeDeleted := map[string]state.ReleaseSpec{} + for _, r := range deletingReleases { + id := state.ReleaseToID(&r) + releasesToBeDeleted[id] = r + } + + releasesToBeUpdated := map[string]state.ReleaseSpec{} + for _, r := range changedReleases { + id := state.ReleaseToID(&r) + + // If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade` + if _, ok := releasesToBeDeleted[id]; !ok { + releasesToBeUpdated[id] = r + } + } + + // sync only when there are changes + if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 { + var msg *string + if c.DetailedExitcode() { + // TODO better way to get the logger + m := "No affected releases" + msg = &m + } + return msg, nil, nil, nil + } + + names := []string{} + for _, r := range releasesToBeUpdated { + names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart)) + } + for _, r := range releasesToBeDeleted { + names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart)) + } + // Make the output deterministic for testing purpose + sort.Strings(names) + + infoMsg := fmt.Sprintf(`Affected releases are: +%s +`, strings.Join(names, "\n")) + + return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil +} diff --git a/pkg/state/state.go b/pkg/state/state.go index b6163ddd..3afd8851 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1327,6 +1327,15 @@ func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) { releases = append(releases, r.ReleaseSpec) } } + + var extra string + + if len(st.Selectors) > 0 { + extra = " matching " + strings.Join(st.Selectors, ",") + } + + st.logger.Debugf("%d release(s)%s found in %s\n", len(releases), extra, st.FilePath) + return releases, nil } @@ -1337,7 +1346,6 @@ func (st *HelmState) FilterReleases() error { return err } st.Releases = releases - st.logger.Debugf("%d release(s) matching %s found in %s\n", len(releases), strings.Join(st.Selectors, ","), st.FilePath) return nil }