From 1a3c11dffde8380a36c627b0b6720dd8d791a32e Mon Sep 17 00:00:00 2001 From: Anton Bretting Date: Sun, 1 May 2022 11:11:53 +0200 Subject: [PATCH] Add unittests for preapply Signed-off-by: Anton Bretting --- pkg/app/app.go | 3 +- pkg/app/app_apply_hooks_test.go | 236 ++++++++++++++++++ pkg/app/app_apply_test.go | 32 ++- .../apply_release_with_preapply_hook#01/log | 10 + .../apply_release_with_preapply_hook/log | 10 + pkg/event/bus.go | 2 +- 6 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 pkg/app/app_apply_hooks_test.go create mode 100644 pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#01/log create mode 100644 pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook/log diff --git a/pkg/app/app.go b/pkg/app/app.go index dddf0d21..4af49d56 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1376,7 +1376,8 @@ Do you really want to apply? if !interactive || interactive && r.askForConfirmation(confMsg) { r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) - for _, release := range st.Releases { + for _, release := range releasesWithPreApply { + a.Logger.Infof("\nRunning preapply hook for %s:", release.Name) if _, err := st.TriggerPreapplyEvent(&release, "apply"); err != nil { syncErrs = append(syncErrs, err) } diff --git a/pkg/app/app_apply_hooks_test.go b/pkg/app/app_apply_hooks_test.go new file mode 100644 index 00000000..eeb74cbb --- /dev/null +++ b/pkg/app/app_apply_hooks_test.go @@ -0,0 +1,236 @@ +package app + +import ( + "bufio" + "bytes" + "io" + "path/filepath" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/roboll/helmfile/pkg/exectest" + "github.com/roboll/helmfile/pkg/helmexec" + "github.com/roboll/helmfile/pkg/testhelper" + "github.com/variantdev/vals" +) + +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, + 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 + } + + 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: ["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", + }) + }) + +} diff --git a/pkg/app/app_apply_test.go b/pkg/app/app_apply_test.go index b0f8401f..a3512801 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]) } } } @@ -1340,4 +1340,34 @@ foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default concurrency: 1, }) }) + + 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, + }) + }) } 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..5c8158c0 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook#01/log @@ -0,0 +1,10 @@ + +Running preapply hook for foo: + +hook[preapply] logs | foo +hook[preapply] 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..5c8158c0 --- /dev/null +++ b/pkg/app/testdata/testapply_hooks/apply_release_with_preapply_hook/log @@ -0,0 +1,10 @@ + +Running preapply hook for foo: + +hook[preapply] logs | foo +hook[preapply] logs | + +UPDATED RELEASES: +NAME CHART VERSION +foo incubator/raw + diff --git a/pkg/event/bus.go b/pkg/event/bus.go index 92b69bff..b3448720 100644 --- a/pkg/event/bus.go +++ b/pkg/event/bus.go @@ -88,7 +88,7 @@ func (bus *Bus) Trigger(evt string, evtErr error, context map[string]interface{} } } - bus.Logger.Debugf("hook[%s]: stateFilePath=%s, basePath=%s\n", name, bus.StateFilePath, bus.BasePath) + bus.Logger.Debugf("hook[%s]: stateFilePath=%s, basePath=%s", name, bus.StateFilePath, bus.BasePath) data := map[string]interface{}{ "Environment": bus.Env,