From bb22a260e892fc92406a0692b4520b3b8baec765 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 17 Sep 2026 12:57:22 +0200 Subject: [PATCH] fix(provider): guard down's stopContainer against the relay too glours' review flagged the same bug class this PR already fixes for start/restart: stopContainer (down.go) runs PreStop hooks unconditionally, and runHook execs into the container -- which fails against a shell-less relay. Guarded it the same way, with a regression test. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Nicolas De Loof --- pkg/compose/down.go | 2 +- pkg/compose/down_test.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pkg/compose/down.go b/pkg/compose/down.go index 6e408ef87..b4bbc6e3c 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -311,7 +311,7 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi eventName := getContainerProgressName(ctr) s.events.On(newEvent(eventName, api.Working, api.StatusStopping)) - if service != nil { + if service != nil && !isRelayContainer(ctr) { for _, hook := range service.PreStop { err := s.runHook(ctx, ctr, *service, hook, listener) if err != nil { diff --git a/pkg/compose/down_test.go b/pkg/compose/down_test.go index cd0aba534..d8fa9338f 100644 --- a/pkg/compose/down_test.go +++ b/pkg/compose/down_test.go @@ -676,6 +676,27 @@ func TestDownHookContainerRemovalFailureIsNonFatal(t *testing.T) { assert.NilError(t, err) } +// A relay stands in for the service on the network but is a shell-less +// scratch binary: pre_stop has no process inside it to act on. No +// ExecCreate expectation is set: per newStartTestService, gomock fails the +// test if the hook still runs. +func TestStopContainerSkipsPreStopForRelay(t *testing.T) { + svc, apiClient, _ := newStartTestService(t) + + service := types.ServiceConfig{ + Name: "db", + PreStop: []types.ServiceHook{{Command: types.ShellCommand{"quiesce"}}}, + } + relay := serviceContainer("db", 1, container.StateRunning) + relay.Labels[compose.RelayLabel] = "abc123" + + apiClient.EXPECT().ContainerStop(gomock.Any(), relay.ID, gomock.Any()). + Return(client.ContainerStopResult{}, nil) + + err := svc.stopContainer(t.Context(), &service, relay, nil, nil) + assert.NilError(t, err) +} + // runningOneOff builds a RUNNING `compose run` container of the given service. func runningOneOff(service, id string) container.Summary { c := testContainer(service, id, true)