diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index 5306bfc200a..745640eabd9 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -535,13 +535,19 @@ func (hooks *Hooks) MarshalJSON() ([]byte, error) { // Run executes all hooks for the given hook name. func (hooks Hooks) Run(name HookName, state *specs.State) error { list := hooks[name] + var errs []error for i, h := range list { if err := h.Run(state); err != nil { - return fmt.Errorf("error running %s hook #%d: %w", name, i, err) + err = fmt.Errorf("error running %s hook #%d: %w", name, i, err) + if name != Poststop { + return err + } + // Poststop hooks must all be run, even if one fails. + errs = append(errs, err) } } - return nil + return errors.Join(errs...) } // SetDefaultEnv sets the environment for those CommandHook entries diff --git a/libcontainer/configs/config_test.go b/libcontainer/configs/config_test.go index 70dcb799531..e6ef7b663da 100644 --- a/libcontainer/configs/config_test.go +++ b/libcontainer/configs/config_test.go @@ -2,6 +2,7 @@ package configs_test import ( "encoding/json" + "errors" "fmt" "os" "reflect" @@ -155,6 +156,63 @@ func TestFuncHookRun(t *testing.T) { } } +func TestPoststopHooksRunAfterErrors(t *testing.T) { + state := &specs.State{Status: specs.StateStopped} + firstErr := errors.New("first hook failed") + lastErr := errors.New("last hook failed") + var executed []int + + hooks := configs.Hooks{ + configs.Poststop: configs.HookList{ + configs.NewFunctionHook(func(*specs.State) error { + executed = append(executed, 1) + return firstErr + }), + configs.NewFunctionHook(func(*specs.State) error { + executed = append(executed, 2) + return nil + }), + configs.NewFunctionHook(func(*specs.State) error { + executed = append(executed, 3) + return lastErr + }), + }, + } + + err := hooks.Run(configs.Poststop, state) + if !reflect.DeepEqual(executed, []int{1, 2, 3}) { + t.Fatalf("expected every poststop hook to run, got %v", executed) + } + if !errors.Is(err, firstErr) || !errors.Is(err, lastErr) { + t.Fatalf("expected both hook errors to be returned, got %v", err) + } +} + +func TestOtherHooksStopAfterError(t *testing.T) { + hookErr := errors.New("prestart hook failed") + executed := 0 + hooks := configs.Hooks{ + configs.Prestart: configs.HookList{ + configs.NewFunctionHook(func(*specs.State) error { + executed++ + return hookErr + }), + configs.NewFunctionHook(func(*specs.State) error { + executed++ + return nil + }), + }, + } + + err := hooks.Run(configs.Prestart, &specs.State{}) + if !errors.Is(err, hookErr) { + t.Fatalf("expected hook error %v, got %v", hookErr, err) + } + if executed != 1 { + t.Fatalf("expected later prestart hooks not to run, got %d hooks", executed) + } +} + func TestCommandHookRun(t *testing.T) { state := &specs.State{ Version: "1",