diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 91261d1ee..5e61026b9 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -214,6 +214,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions ExitCodeFrom: upOptions.exitCodeFrom, CascadeStop: upOptions.cascadeStop, Wait: upOptions.wait, + Services: services, }, }) } diff --git a/pkg/compose/containers.go b/pkg/compose/containers.go index 31ea5c8a0..4a22789a4 100644 --- a/pkg/compose/containers.go +++ b/pkg/compose/containers.go @@ -141,3 +141,14 @@ func (containers Containers) sorted() Containers { }) return containers } + +func (containers Containers) remove(id string) Containers { + for i, c := range containers { + if c.ID == id { + l := len(containers) - 1 + containers[i] = containers[l] + return containers[:l] + } + } + return containers +} diff --git a/pkg/compose/logs.go b/pkg/compose/logs.go index ce2f0cf96..9968e1283 100644 --- a/pkg/compose/logs.go +++ b/pkg/compose/logs.go @@ -65,19 +65,16 @@ func (s *composeService) Logs( if options.Follow { printer := newLogPrinter(consumer) - eg.Go(func() error { - for _, c := range containers { - printer.HandleEvent(api.ContainerEvent{ - Type: api.ContainerEventAttach, - Container: getContainerNameWithoutProject(c), - Service: c.Labels[api.ServiceLabel], - }) - } - return nil - }) + for _, c := range containers { + printer.HandleEvent(api.ContainerEvent{ + Type: api.ContainerEventAttach, + Container: getContainerNameWithoutProject(c), + Service: c.Labels[api.ServiceLabel], + }) + } eg.Go(func() error { - return s.watchContainers(ctx, projectName, options.Services, printer.HandleEvent, containers, func(c types.Container) error { + return s.watchContainers(ctx, projectName, options.Services, nil, printer.HandleEvent, containers, func(c types.Container) error { printer.HandleEvent(api.ContainerEvent{ Type: api.ContainerEventAttach, Container: getContainerNameWithoutProject(c), diff --git a/pkg/compose/printer.go b/pkg/compose/printer.go index 158c98e54..e83ec2a36 100644 --- a/pkg/compose/printer.go +++ b/pkg/compose/printer.go @@ -28,19 +28,23 @@ type logPrinter interface { HandleEvent(event api.ContainerEvent) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) Cancel() + Stop() } type printer struct { queue chan api.ContainerEvent consumer api.LogConsumer + stopCh chan struct{} } // newLogPrinter builds a LogPrinter passing containers logs to LogConsumer func newLogPrinter(consumer api.LogConsumer) logPrinter { queue := make(chan api.ContainerEvent) + stopCh := make(chan struct{}, 1) // printer MAY stop on his own, so Stop MUST not be blocking printer := printer{ consumer: consumer, queue: queue, + stopCh: stopCh, } return &printer } @@ -51,6 +55,10 @@ func (p *printer) Cancel() { } } +func (p *printer) Stop() { + p.stopCh <- struct{}{} +} + func (p *printer) HandleEvent(event api.ContainerEvent) { p.queue <- event } @@ -64,6 +72,8 @@ func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string containers := map[string]struct{}{} for { select { + case <-p.stopCh: + return exitCode, nil case <-ctx.Done(): return exitCode, ctx.Err() case event := <-p.queue: diff --git a/pkg/compose/start.go b/pkg/compose/start.go index 0dcef8ded..bd0093516 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/compose-spec/compose-go/types" + "github.com/docker/compose/v2/pkg/utils" moby "github.com/docker/docker/api/types" "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -50,13 +51,6 @@ func (s *composeService) start(ctx context.Context, projectName string, options } } - if len(options.Services) > 0 { - err := project.ForServices(options.Services) - if err != nil { - return err - } - } - eg, ctx := errgroup.WithContext(ctx) if listener != nil { attached, err := s.attach(ctx, project, listener, options.AttachTo) @@ -65,7 +59,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options } eg.Go(func() error { - return s.watchContainers(context.Background(), project.Name, options.AttachTo, listener, attached, func(container moby.Container) error { + return s.watchContainers(context.Background(), project.Name, options.AttachTo, options.Services, listener, attached, func(container moby.Container) error { return s.attachContainer(ctx, container, listener) }) }) @@ -116,9 +110,20 @@ func getDependencyCondition(service types.ServiceConfig, project *types.Project) type containerWatchFn func(container moby.Container) error // watchContainers uses engine events to capture container start/die and notify ContainerEventListener -func (s *composeService) watchContainers(ctx context.Context, projectName string, services []string, listener api.ContainerEventListener, containers Containers, onStart containerWatchFn) error { - watched := map[string]int{} +func (s *composeService) watchContainers(ctx context.Context, projectName string, services, required []string, + listener api.ContainerEventListener, containers Containers, onStart containerWatchFn) error { + if len(required) == 0 { + required = services + } + + var ( + expected Containers + watched = map[string]int{} + ) for _, c := range containers { + if utils.Contains(required, c.Labels[api.ServiceLabel]) { + expected = append(expected, c) + } watched[c.ID] = 0 } @@ -143,22 +148,18 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string } name := getContainerNameWithoutProject(container) - if event.Status == "stop" { + service := container.Labels[api.ServiceLabel] + switch event.Status { + case "stop": listener(api.ContainerEvent{ Type: api.ContainerEventStopped, Container: name, - Service: container.Labels[api.ServiceLabel], + Service: service, }) delete(watched, container.ID) - if len(watched) == 0 { - // all project containers stopped, we're done - stop() - } - return nil - } - - if event.Status == "die" { + expected = expected.remove(container.ID) + case "die": restarted := watched[container.ID] watched[container.ID] = restarted + 1 // Container terminated. @@ -167,7 +168,7 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string listener(api.ContainerEvent{ Type: api.ContainerEventExit, Container: name, - Service: container.Labels[api.ServiceLabel], + Service: service, ExitCode: inspected.State.ExitCode, Restarting: willRestart, }) @@ -175,21 +176,15 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string if !willRestart { // we're done with this one delete(watched, container.ID) + expected = expected.remove(container.ID) } - - if len(watched) == 0 { - // all project containers stopped, we're done - stop() - } - return nil - } - - if event.Status == "start" { + case "start": count, ok := watched[container.ID] mustAttach := ok && count > 0 // Container restarted, need to re-attach if !ok { // A new container has just been added to service by scale watched[container.ID] = 0 + expected = append(expected, container) mustAttach = true } if mustAttach { @@ -200,7 +195,9 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string } } } - + if len(expected) == 0 { + stop() + } return nil }, }) diff --git a/pkg/compose/up.go b/pkg/compose/up.go index 72d10fc3c..b8631e0ef 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -23,11 +23,10 @@ import ( "os/signal" "syscall" - "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" - "github.com/compose-spec/compose-go/types" "github.com/docker/cli/cli" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/progress" "golang.org/x/sync/errgroup" ) @@ -92,6 +91,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options return err } + printer.Stop() err = eg.Wait() if exitCode != 0 { errMsg := "" diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 3eade16cc..0dd92a241 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -259,3 +259,13 @@ networks: name: compose-e2e-convert-interpolate_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } + +func TestStopWithDependeciesAttached(t *testing.T) { + const projectName = "compose-e2e-stop-with-deps" + c := NewParallelCLI(t, WithEnv("COMMAND=echo hello")) + + t.Run("up", func(t *testing.T) { + res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "-p", projectName, "up", "--attach-dependencies", "foo") + res.Assert(t, icmd.Expected{Out: "exited with code 0"}) + }) +} diff --git a/pkg/e2e/fixtures/dependencies/compose.yaml b/pkg/e2e/fixtures/dependencies/compose.yaml index 82d3e4e84..099c5f18a 100644 --- a/pkg/e2e/fixtures/dependencies/compose.yaml +++ b/pkg/e2e/fixtures/dependencies/compose.yaml @@ -1,8 +1,10 @@ services: foo: image: nginx:alpine + command: "${COMMAND}" depends_on: - bar bar: image: nginx:alpine + scale: 2