diff --git a/docs/index.md b/docs/index.md index 72fd97bb..887cd839 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1183,6 +1183,7 @@ Once `events` are triggered, associated `hooks` are executed, by running the `co Currently supported `events` are: - `prepare` +- `preapply` - `presync` - `preuninstall` - `postuninstall` @@ -1192,9 +1193,12 @@ Currently supported `events` are: Hooks associated to `prepare` events are triggered after each release in your helmfile is loaded from YAML, before execution. `prepare` hooks are triggered on the release as long as it is not excluded by the helmfile selector(e.g. `helmfile -l key=value`). -Hooks associated to `presync` events are triggered before each release is applied to the remote cluster. +Hooks associated to `presync` events are triggered before each release is installed or upgraded on the remote cluster. This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`. +`preapply` hooks are triggered before a release is uninstalled, installed, or upgraded as part of `helmfile apply`. +This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change, and you want the hook to be called only when any kind of change is being made. + `preuninstall` hooks are triggered immediately before a release is uninstalled as part of `helmfile apply`, `helmfile sync`, `helmfile delete`, and `helmfile destroy`. `postuninstall` hooks are triggered immediately after successful uninstall of a release while running `helmfile apply`, `helmfile sync`, `helmfile delete`, `helmfile destroy`. diff --git a/go.mod b/go.mod index d22ee5d1..24dc19cd 100644 --- a/go.mod +++ b/go.mod @@ -29,12 +29,14 @@ require ( github.com/variantdev/vals v0.18.0 go.uber.org/multierr v1.6.0 go.uber.org/zap v1.23.0 + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.8.1 k8s.io/apimachinery v0.24.4 + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 ) require ( @@ -194,7 +196,6 @@ require ( k8s.io/client-go v0.23.4 // indirect k8s.io/klog/v2 v2.60.1 // indirect k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect - k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect oras.land/oras-go v1.1.0 // indirect sigs.k8s.io/kustomize/api v0.10.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect diff --git a/go.sum b/go.sum index f30e358a..e32fb81f 100644 --- a/go.sum +++ b/go.sum @@ -1373,6 +1373,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/app/app.go b/pkg/app/app.go index 5c58717a..e4141468 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -790,7 +790,7 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta go func() { sig := <-sigs - errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)} + errs := []error{fmt.Errorf("received [%s] to shutdown ", sig)} _ = context{app: a, st: st, retainValues: defOpts.RetainValuesFiles}.clean(errs) // See http://tldp.org/LDP/abs/html/exitcodes.html switch sig { @@ -1342,7 +1342,7 @@ Do you really want to apply? a.Logger.Debug(*infoMsg) } - applyErrs := []error{} + var applyErrs []error affectedReleases := state.AffectedReleases{} @@ -1355,15 +1355,27 @@ Do you really want to apply? // We deleted releases by traversing the DAG in reverse order if len(releasesToBeDeleted) > 0 { _, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{Reverse: true, SelectedReleases: toDelete, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { - var rs []state.ReleaseSpec + var ( + rs []state.ReleaseSpec + preapplyErrors []error + ) for _, r := range subst.Releases { release := r if r2, ok := releasesToBeDeleted[state.ReleaseToID(&release)]; ok { + if _, err := st.TriggerPreapplyEvent(&r2, "apply"); err != nil { + preapplyErrors = append(applyErrs, err) + continue + } + rs = append(rs, r2) } } + if len(preapplyErrors) > 0 { + return preapplyErrors + } + subst.Releases = rs return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency()) @@ -1377,15 +1389,27 @@ Do you really want to apply? // We upgrade releases by traversing the DAG if len(releasesToBeUpdated) > 0 { _, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, Reverse: false, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { - var rs []state.ReleaseSpec + var ( + rs []state.ReleaseSpec + preapplyErrors []error + ) for _, r := range subst.Releases { release := r if r2, ok := releasesToBeUpdated[state.ReleaseToID(&release)]; ok { + if _, err := st.TriggerPreapplyEvent(&r2, "apply"); err != nil { + preapplyErrors = append(applyErrs, err) + continue + } + rs = append(rs, r2) } } + if len(preapplyErrors) > 0 { + return preapplyErrors + } + subst.Releases = rs syncOpts := state.SyncOpts{ diff --git a/pkg/app/app_apply_hooks_test.go b/pkg/app/app_apply_hooks_test.go new file mode 100644 index 00000000..846efdad --- /dev/null +++ b/pkg/app/app_apply_hooks_test.go @@ -0,0 +1,485 @@ +package app + +import ( + "bufio" + "bytes" + "io" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/variantdev/vals" + + "github.com/helmfile/helmfile/pkg/exectest" + "github.com/helmfile/helmfile/pkg/helmexec" + "github.com/helmfile/helmfile/pkg/testhelper" +) + +func TestApply_hooks(t *testing.T) { + type fields struct { + skipNeeds bool + includeNeeds bool + includeTransitiveNeeds bool + } + + type testcase struct { + fields fields + ns string + concurrency int + skipDiffOnInstall 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 + logLevel string + } + + check := func(t *testing.T, tc testcase) { + t.Helper() + + 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() { + t.Helper() + + 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, tc.logLevel) + + 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, + 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 + } + + syncErr := app.Apply(applyConfig{ + // if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic. + concurrency: tc.concurrency, + logger: logger, + skipDiffOnInstall: tc.skipDiffOnInstall, + skipNeeds: tc.fields.skipNeeds, + includeNeeds: tc.fields.includeNeeds, + includeTransitiveNeeds: tc.fields.includeTransitiveNeeds, + }) + + var gotErr string + if syncErr != nil { + gotErr = syncErr.Error() + } + + if d := cmp.Diff(tc.error, gotErr); d != "" { + t.Fatalf("unexpected error: want (-), got (+): %s", d) + } + + 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("releases[%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:\nDIFF\n%s\nEOD", diff) + } + } else { + assertEqualsToSnapshot(t, "log", bs.String()) + } + } + + t.Run("apply release with preapply hook", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["preapply"] + command: echo + showlogs: true + args: ["foo"] +`, + }, + selectors: []string{"name=foo"}, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("apply release with preapply hook", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["foo"] +`, + }, + selectors: []string{"name=foo"}, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("apply release with preapply hook", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["presync"] + command: echo + showlogs: true + args: ["foo"] +`, + }, + selectors: []string{"name=foo"}, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("hooks for no-diff release", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + # only prepare and cleanup are run + - events: ["prepare", "preapply", "presync", "cleanup"] + command: echo + showlogs: true + args: ["foo"] +`, + }, + selectors: []string{"app=test"}, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: nil, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("hooks are run on enabled release", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +values: +- bar: + enabled: true + +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["foo"] +- name: bar + condition: bar.enabled + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["bar"] +`, + }, + selectors: []string{"app=test"}, + upgraded: []exectest.Release{ + {Name: "foo"}, + {Name: "bar"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + {Name: "bar", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("hooks are not run on disabled release", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +values: +- bar: + enabled: false + +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["foo"] +- name: bar + condition: bar.enabled + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["bar"] +`, + }, + selectors: []string{"app=test"}, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("hooks are run on to-be-uninstalled release", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["foo"] +- name: bar + installed: false + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["bar"] +`, + }, + selectors: []string{"app=test"}, + lists: map[exectest.ListKey]string{ + {Filter: "^foo$", Flags: helmV2ListFlags}: `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 +`, + {Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default +`, + }, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) + + t.Run("hooks are not run on alreadyd uninstalled release", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: foo + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["foo"] +- name: bar + installed: false + chart: incubator/raw + namespace: default + labels: + app: test + hooks: + - events: ["prepare", "preapply", "presync"] + command: echo + showlogs: true + args: ["bar"] +`, + }, + selectors: []string{"app=test"}, + lists: map[exectest.ListKey]string{ + {Filter: "^foo$", Flags: helmV2ListFlags}: `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 +`, + {Filter: "^bar$", Flags: helmV2ListFlags}: ``, + }, + upgraded: []exectest.Release{ + {Name: "foo"}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "foo", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + error: "", + // as we check for log output, set concurrency to 1 to avoid non-deterministic test result + concurrency: 1, + logLevel: "info", + }) + }) +} diff --git a/pkg/app/app_apply_test.go b/pkg/app/app_apply_test.go index b0f8401f..e1e07827 100644 --- a/pkg/app/app_apply_test.go +++ b/pkg/app/app_apply_test.go @@ -137,7 +137,7 @@ func TestApply_2(t *testing.T) { } 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]) + t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx]) } } } diff --git a/pkg/app/testdata/testapply/install/log b/pkg/app/testdata/testapply/install/log index a8f02dc9..f340ce7b 100644 --- a/pkg/app/testdata/testapply/install/log +++ b/pkg/app/testdata/testapply/install/log @@ -47,10 +47,10 @@ GROUP RELEASES 2 default//foo processing releases in group 1/2: default//baz, default//bar -getting deployed release version failed:unexpected list key: listkey(filter=^baz$,flags=--kube-contextdefault--deleting--deployed--failed--pending) in -getting deployed release version failed:unexpected list key: listkey(filter=^bar$,flags=--kube-contextdefault--deleting--deployed--failed--pending) in +getting deployed release version failed:unexpected list key: listkey(filter=^baz$,flags=--kube-contextdefault--deleting--deployed--failed--pending) not found in +getting deployed release version failed:unexpected list key: listkey(filter=^bar$,flags=--kube-contextdefault--deleting--deployed--failed--pending) not found in processing releases in group 2/2: default//foo -getting deployed release version failed:unexpected list key: listkey(filter=^foo$,flags=--kube-contextdefault--deleting--deployed--failed--pending) in +getting deployed release version failed:unexpected list key: listkey(filter=^foo$,flags=--kube-contextdefault--deleting--deployed--failed--pending) not found in UPDATED RELEASES: NAME CHART VERSION diff --git a/pkg/app/testdata/testapply_2/apply_release_with_preapply_hook/log b/pkg/app/testdata/testapply_2/apply_release_with_preapply_hook/log new file mode 100644 index 00000000..2961d200 --- /dev/null +++ b/pkg/app/testdata/testapply_2/apply_release_with_preapply_hook/log @@ -0,0 +1,68 @@ +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: foo + 3: chart: incubator/raw + 4: namespace: default + 5: labels: + 6: app: test + 7: hooks: + 8: - events: ["preapply"] + 9: command: echo +10: showlogs: true +11: args: ["foo"] +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: foo + 3: chart: incubator/raw + 4: namespace: default + 5: labels: + 6: app: test + 7: hooks: + 8: - events: ["preapply"] + 9: command: echo +10: showlogs: true +11: args: ["foo"] +12: + +merged environment: &{default map[] map[]} +1 release(s) matching name=foo found in helmfile.yaml + +Affected releases are: + foo (incubator/raw) UPDATED +Releases with preapply hooks: + foo (incubator/raw) + +Running preapply hook for foo: +hook[echo]: stateFilePath=helmfile.yaml, basePath=. +hook[echo]: triggered by event "preapply" + +echo:XlBWj> foo +hook[echo]: foo + + + +hook[preapply] logs | foo +hook[preapply] logs | +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/foo + +processing releases in group 1/1: default/default/foo +getting deployed release version failed:unexpected list key: {^foo$ --kube-contextdefault--deleting--deployed--failed--pending} + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#01/log b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#01/log new file mode 100644 index 00000000..0c6b5308 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#01/log @@ -0,0 +1,14 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[preapply] logs | foo +hook[preapply] logs | + +hook[presync] logs | foo +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#02/log b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#02/log new file mode 100644 index 00000000..8f9089e2 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#02/log @@ -0,0 +1,8 @@ + +hook[presync] logs | foo +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook/log b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook/log new file mode 100644 index 00000000..e6233234 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook/log @@ -0,0 +1,8 @@ + +hook[preapply] logs | foo +hook[preapply] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_alreadyd_uninstalled_release/log b/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_alreadyd_uninstalled_release/log new file mode 100644 index 00000000..ac333ffe --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_alreadyd_uninstalled_release/log @@ -0,0 +1,14 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[preapply] logs | foo +hook[preapply] logs | + +hook[presync] logs | foo +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw 3.1.0 + diff --git a/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_disabled_release/log b/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_disabled_release/log new file mode 100644 index 00000000..0c6b5308 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/hooks_are_not_run_on_disabled_release/log @@ -0,0 +1,14 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[preapply] logs | foo +hook[preapply] logs | + +hook[presync] logs | foo +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/hooks_are_run_on_enabled_release/log b/pkg/app/testdata/testapply_hooks/hooks_are_run_on_enabled_release/log new file mode 100644 index 00000000..6da2c966 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/hooks_are_run_on_enabled_release/log @@ -0,0 +1,24 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[prepare] logs | bar +hook[prepare] logs | + +hook[preapply] logs | foo +hook[preapply] logs | + +hook[preapply] logs | bar +hook[preapply] logs | + +hook[presync] logs | foo +hook[presync] logs | + +hook[presync] logs | bar +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw +bar incubator/raw + diff --git a/pkg/app/testdata/testapply_hooks/hooks_are_run_on_to-be-uninstalled_release/log b/pkg/app/testdata/testapply_hooks/hooks_are_run_on_to-be-uninstalled_release/log new file mode 100644 index 00000000..c79da665 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/hooks_are_run_on_to-be-uninstalled_release/log @@ -0,0 +1,24 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[preapply] logs | bar +hook[preapply] logs | + +hook[presync] logs | bar +hook[presync] logs | + +hook[preapply] logs | foo +hook[preapply] logs | + +hook[presync] logs | foo +hook[presync] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw 3.1.0 + + +DELETED RELEASES: +NAME +bar diff --git a/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log b/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log new file mode 100644 index 00000000..1643d0d3 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log @@ -0,0 +1,6 @@ + +hook[prepare] logs | foo +hook[prepare] logs | + +hook[cleanup] logs | foo +hook[cleanup] logs | diff --git a/pkg/exectest/helm.go b/pkg/exectest/helm.go index d37965c8..6c42bab2 100644 --- a/pkg/exectest/helm.go +++ b/pkg/exectest/helm.go @@ -161,7 +161,7 @@ func (helm *Helm) List(context helmexec.HelmContext, filter string, flags ...str for k := range helm.Lists { keys = append(keys, k.String()) } - return "", fmt.Errorf("unexpected list key: %v in %v", key, strings.Join(keys, ", ")) + return "", fmt.Errorf("unexpected list key: %v not found in %v", key, strings.Join(keys, ", ")) } return res, nil } diff --git a/pkg/state/state.go b/pkg/state/state.go index 385ab903..97d921ae 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -2251,6 +2251,10 @@ func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, evtErr error, helmfile return st.triggerReleaseEvent("postsync", evtErr, r, helmfileCommand) } +func (st *HelmState) TriggerPreapplyEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { + return st.triggerReleaseEvent("preapply", nil, r, helmfileCommand) +} + func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpec, helmfileCmd string) (bool, error) { bus := &event.Bus{ Hooks: r.Hooks, @@ -2268,6 +2272,7 @@ func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpe "Release": r, "HelmfileCommand": helmfileCmd, } + return bus.Trigger(evt, evtErr, data) }