diff --git a/pkg/app/cleanup_hooks_error_test.go b/pkg/app/cleanup_hooks_error_test.go new file mode 100644 index 00000000..7e0e4271 --- /dev/null +++ b/pkg/app/cleanup_hooks_error_test.go @@ -0,0 +1,124 @@ +package app + +import ( + "sync" + "testing" + + "github.com/helmfile/vals" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + + "github.com/helmfile/helmfile/pkg/exectest" + ffs "github.com/helmfile/helmfile/pkg/filesystem" + "github.com/helmfile/helmfile/pkg/helmexec" +) + +func TestCleanupHooksErrorPropagation(t *testing.T) { + type testcase struct { + name string + files map[string]string + releaseName string + expectedError bool + expectedInLogs string + } + + check := func(t *testing.T, tc testcase) { + t.Helper() + + var helm = &exectest.Helm{ + FailOnUnexpectedList: true, + FailOnUnexpectedDiff: true, + DiffMutex: &sync.Mutex{}, + ChartsMutex: &sync.Mutex{}, + ReleasesMutex: &sync.Mutex{}, + } + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Fatalf("unexpected error creating vals runtime: %v", err) + } + + bs := runWithLogCapture(t, "info", func(t *testing.T, logger *zap.SugaredLogger) { + t.Helper() + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + fs: ffs.DefaultFileSystem(), + OverrideKubeContext: "default", + DisableKubeVersionAutoDetection: true, + Env: "default", + Logger: logger, + helms: map[helmKey]helmexec.Interface{ + createHelmKey("helm", "default"): helm, + }, + valsRuntime: valsRuntime, + }, tc.files) + + syncErr := app.Sync(applyConfig{ + concurrency: 1, + logger: logger, + }) + + if tc.expectedError { + assert.Error(t, syncErr, "expected error for release %s", tc.releaseName) + } else { + assert.NoError(t, syncErr, "unexpected error for release %s", tc.releaseName) + } + }) + + logOutput := bs.String() + assert.Contains(t, logOutput, tc.expectedInLogs, "unexpected log output") + } + + t.Run("cleanup hook receives error when sync fails", func(t *testing.T) { + check(t, testcase{ + name: "sync-failure-cleanup-error", + releaseName: "error-release", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +hooks: + - name: global-cleanup + events: + - cleanup + showlogs: true + command: echo + args: + - "error is '{{ .Event.Error }}'" + +releases: + - name: error-release + chart: incubator/raw + namespace: default +`, + }, + expectedError: true, + expectedInLogs: "error is 'failed processing release error-release: error'", + }) + }) + + t.Run("cleanup hook receives nil when sync succeeds", func(t *testing.T) { + check(t, testcase{ + name: "sync-success-cleanup-nil", + releaseName: "success-release", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +hooks: + - name: global-cleanup + events: + - cleanup + showlogs: true + command: echo + args: + - "error is '{{ .Event.Error }}'" + +releases: + - name: success-release + chart: incubator/raw + namespace: default +`, + }, + expectedError: false, + expectedInLogs: "error is ''", + }) + }) +} diff --git a/pkg/state/cleanup_hooks_test.go b/pkg/state/cleanup_hooks_test.go new file mode 100644 index 00000000..955036ce --- /dev/null +++ b/pkg/state/cleanup_hooks_test.go @@ -0,0 +1,155 @@ +package state + +import ( + "errors" + "io" + "strings" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "github.com/helmfile/helmfile/pkg/environment" + "github.com/helmfile/helmfile/pkg/event" + ffs "github.com/helmfile/helmfile/pkg/filesystem" + "github.com/helmfile/helmfile/pkg/helmexec" +) + +type mockRunner struct { + executeCalls []struct { + cmd string + args []string + env map[string]string + } +} + +func (r *mockRunner) Execute(cmd string, args []string, env map[string]string, _ bool) ([]byte, error) { + r.executeCalls = append(r.executeCalls, struct { + cmd string + args []string + env map[string]string + }{cmd: cmd, args: args, env: env}) + return []byte(""), nil +} + +func (r *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, _ io.Reader) ([]byte, error) { + return []byte(""), nil +} + +func TestTriggerGlobalCleanupEventWithMockRunner(t *testing.T) { + runner := &mockRunner{} + + core, _ := observer.New(zap.InfoLevel) + logger := zap.New(core).Sugar() + + testError := errors.New("sync failed: release error") + + hooks := []event.Hook{ + { + Name: "cleanup-with-error", + Events: []string{"cleanup"}, + Command: "echo", + Args: []string{"error is '{{ .Event.Error }}'"}, + ShowLogs: true, + }, + } + + bus := &event.Bus{ + Hooks: hooks, + StateFilePath: "/path/to/helmfile.yaml", + BasePath: ".", + Namespace: "default", + Env: environment.Environment{Name: "default"}, + Logger: logger, + Fs: ffs.DefaultFileSystem(), + Runner: runner, + } + + data := map[string]any{ + "HelmfileCommand": "sync", + } + + executed, err := bus.Trigger("cleanup", testError, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !executed { + t.Fatal("expected cleanup hook to be executed") + } + + if len(runner.executeCalls) != 1 { + t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls)) + } + + call := runner.executeCalls[0] + if call.cmd != "echo" { + t.Errorf("expected command 'echo', got %q", call.cmd) + } + + if len(call.args) != 1 { + t.Fatalf("expected 1 arg, got %d", len(call.args)) + } + + expectedArg := "error is 'sync failed: release error'" + if !strings.Contains(call.args[0], "error is") { + t.Errorf("expected arg to contain 'error is', got %q", call.args[0]) + } + + if call.args[0] != expectedArg { + t.Errorf("expected arg %q, got %q", expectedArg, call.args[0]) + } +} + +func TestTriggerGlobalCleanupEventNilError(t *testing.T) { + runner := &mockRunner{} + + core, _ := observer.New(zap.InfoLevel) + logger := zap.New(core).Sugar() + + hooks := []event.Hook{ + { + Name: "cleanup-nil-error", + Events: []string{"cleanup"}, + Command: "echo", + Args: []string{"error is '{{ .Event.Error }}'"}, + ShowLogs: true, + }, + } + + bus := &event.Bus{ + Hooks: hooks, + StateFilePath: "/path/to/helmfile.yaml", + BasePath: ".", + Namespace: "default", + Env: environment.Environment{Name: "default"}, + Logger: logger, + Fs: ffs.DefaultFileSystem(), + Runner: runner, + } + + data := map[string]any{ + "HelmfileCommand": "sync", + } + + executed, err := bus.Trigger("cleanup", nil, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !executed { + t.Fatal("expected cleanup hook to be executed") + } + + if len(runner.executeCalls) != 1 { + t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls)) + } + + call := runner.executeCalls[0] + expectedArg := "error is ''" + if call.args[0] != expectedArg { + t.Errorf("expected arg %q, got %q", expectedArg, call.args[0]) + } +} + +var _ helmexec.Runner = &mockRunner{}