fix: cleanup hooks not receiving error signal

Closes #1041

Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2026-03-10 07:58:39 +08:00
parent da8a1ff2e6
commit c800910903
2 changed files with 279 additions and 0 deletions

View File

@ -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 '<nil>'",
})
})
}

View File

@ -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 '<nil>'"
if call.args[0] != expectedArg {
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
}
}
var _ helmexec.Runner = &mockRunner{}