diff --git a/pkg/compose/up.go b/pkg/compose/up.go index b5764324dc..53695f9c3c 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -492,6 +492,7 @@ func (u *upSession) stopApplication() { err := u.stop(context.WithoutCancel(u.globalCtx), u.project.Name, api.StopOptions{ Services: u.options.Create.Services, Project: u.project, + Timeout: u.options.Create.Timeout, }, u.printer.HandleEvent) u.appendErr(err) return nil @@ -575,6 +576,7 @@ func (u *upSession) stopLateStarter(service string) { err := u.stop(context.WithoutCancel(u.globalCtx), u.project.Name, api.StopOptions{ Services: []string{service}, Project: u.project, + Timeout: u.options.Create.Timeout, }, u.printer.HandleEvent) u.appendErr(err) return nil diff --git a/pkg/compose/up_test.go b/pkg/compose/up_test.go index 243a671845..69a6ce7230 100644 --- a/pkg/compose/up_test.go +++ b/pkg/compose/up_test.go @@ -34,6 +34,7 @@ import ( "gotest.tools/v3/assert" "github.com/docker/compose/v5/pkg/api" + "github.com/docker/compose/v5/pkg/utils" ) func TestShouldFollowStartEvent(t *testing.T) { @@ -189,6 +190,42 @@ func TestAppendErrKeepsCancellationBeforeShutdown(t *testing.T) { assert.Equal(t, len(u.errs), 1) } +// TestUpSessionStopsWithUpTimeout locks that the stops an attached up issues +// itself carry -t (#14394). +func TestUpSessionStopsWithUpTimeout(t *testing.T) { + timeout := 2 * time.Second + stops := map[string]func(*upSession){ + "stopApplication": (*upSession).stopApplication, + "stopLateStarter": func(u *upSession) { u.stopLateStarter("svc0") }, + } + for name, stop := range stops { + t.Run(name, func(t *testing.T) { + svc, apiClient := newTestService(t) + project, containers := nIndependentServiceContainers(1) + apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()). + Return(client.ContainerListResult{Items: containers}, nil) + var got client.ContainerStopOptions + apiClient.EXPECT().ContainerStop(gomock.Any(), "c0", gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, opts client.ContainerStopOptions) (client.ContainerStopResult, error) { + got = opts + return client.ContainerStopResult{}, nil + }) + + u := &upSession{ + composeService: svc, + project: project, + globalCtx: t.Context(), + printer: newLogPrinter(&testLogConsumer{}), + options: api.UpOptions{Create: api.CreateOptions{Timeout: &timeout}}, + } + stop(u) + assert.NilError(t, u.eg.Wait()) + assert.Equal(t, len(u.errs), 0, "%v", u.errs) + assert.DeepEqual(t, got, client.ContainerStopOptions{Timeout: utils.DurationSecondToInt(&timeout)}) + }) + } +} + // TestStreamContainerLogs_ConcurrencyIsBounded guards a regression flagged // in review: streamContainerLogs (attaches logs for containers that start // after up's initial attach, e.g. via depends_on/restart/watch rebuilds) diff --git a/pkg/e2e/cascade_test.go b/pkg/e2e/cascade_test.go index f9f72ca1ef..05cde70870 100644 --- a/pkg/e2e/cascade_test.go +++ b/pkg/e2e/cascade_test.go @@ -46,6 +46,16 @@ func TestCascadeFail(t *testing.T) { ServiceState("running", "exited")) } +// TestCascadeStopHonorsTimeout is a regression test for #14394: the stop +// triggered by the first exit ignored -t and waited for stop_grace_period. +func TestCascadeStopHonorsTimeout(t *testing.T) { + NewScenario(t, "up --abort-on-container-exit must stop the project within the -t timeout"). + Step("up aborts on the first exit and kills a service ignoring SIGTERM once -t elapses", + ComposeCmd("up", "-t", "1", "--abort-on-container-exit", "--exit-code-from", "tests").MayFail().Within(60*time.Second), + ExitCode(3), + ServiceState("app", "exited")) +} + func TestCascadeIgnoresOneOffContainer(t *testing.T) { const projectName = "compose-e2e-cascade-oneoff" c := NewCLI(t, WithEnv("COMPOSE_PROJECT_NAME="+projectName)) diff --git a/pkg/e2e/testdata/TestCascadeStopHonorsTimeout/compose.yaml b/pkg/e2e/testdata/TestCascadeStopHonorsTimeout/compose.yaml new file mode 100644 index 0000000000..6adc0b1e6b --- /dev/null +++ b/pkg/e2e/testdata/TestCascadeStopHonorsTimeout/compose.yaml @@ -0,0 +1,10 @@ +services: + app: + image: alpine + # PID 1 without a SIGTERM handler: only the kill at the end of the timeout stops it + command: sleep infinity + stop_grace_period: 2m + + tests: + image: alpine + command: sh -c "sleep 1; exit 3"