diff --git a/api/compose/api.go b/api/compose/api.go index aed5d79c7..7240eca25 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -67,8 +67,8 @@ type CreateOptions struct { type StartOptions struct { // Attach will attach to container and pipe stdout/stderr to LogConsumer Attach LogConsumer - // CascadeStop will run `Stop` on any container exit - CascadeStop bool + // Listener will get notified on container events + Listener Listener } // UpOptions group options of the Up API @@ -185,5 +185,14 @@ type Stack struct { // LogConsumer is a callback to process log messages from services type LogConsumer interface { Log(service, container, message string) - Exit(service, container string, exitCode int) + Status(service, container, message string) +} + +// Listener get notified on container Events +type Listener chan Event + +// Event let us know a Container exited +type Event struct { + Service string + Status int } diff --git a/cli/cmd/compose/start.go b/cli/cmd/compose/start.go index fd734fb3f..07be32ee6 100644 --- a/cli/cmd/compose/start.go +++ b/cli/cmd/compose/start.go @@ -18,12 +18,12 @@ package compose import ( "context" - "github.com/docker/compose-cli/api/compose" "os" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/cli/formatter" ) diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 325f81325..bdb50f0cc 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -49,6 +49,7 @@ type upOptions struct { forceRecreate bool noRecreate bool noStart bool + cascadeStop bool } func (o upOptions) recreateStrategy() string { @@ -73,6 +74,9 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { switch contextType { case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType: + if opts.cascadeStop && opts.Detach { + return fmt.Errorf("--abort-on-container-exit and --detach are incompatible") + } if opts.forceRecreate && opts.noRecreate { return fmt.Errorf("--force-recreate and --no-recreate are incompatible") } @@ -95,6 +99,7 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command { flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") flags.BoolVar(&opts.noStart, "no-start", false, "Don't start the services after creating them.") + flags.BoolVar(&opts.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") } return upCmd @@ -145,9 +150,25 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro return nil } + ctx, cancel := context.WithCancel(ctx) + listener := make(chan compose.Event) + go func() { + var aborting bool + for { + <-listener + if opts.cascadeStop && !aborting { + aborting = true + fmt.Println("Aborting on container exit...") + cancel() + } + } + }() + err = c.ComposeService().Start(ctx, project, compose.StartOptions{ - Attach: formatter.NewLogConsumer(ctx, os.Stdout), + Attach: formatter.NewLogConsumer(ctx, os.Stdout), + Listener: listener, }) + if errors.Is(ctx.Err(), context.Canceled) { fmt.Println("Gracefully stopping...") ctx = context.Background() diff --git a/cli/formatter/logs.go b/cli/formatter/logs.go index 22f6e981d..3bfbbbff6 100644 --- a/cli/formatter/logs.go +++ b/cli/formatter/logs.go @@ -51,9 +51,10 @@ func (l *logConsumer) Log(service, container, message string) { } } -func (l *logConsumer) Exit(service, container string, exitCode int) { - msg := fmt.Sprintf("%s exited with code %d\n", container, exitCode) - l.writer.Write([]byte(l.getColorFunc(service)(msg))) +func (l *logConsumer) Status(service, container, msg string) { + cf := l.getColorFunc(service) + buf := bytes.NewBufferString(fmt.Sprintf("%s %s \n", cf(container), cf(msg))) + l.writer.Write(buf.Bytes()) // nolint:errcheck } func (l *logConsumer) getColorFunc(service string) colorFunc { diff --git a/ecs/logs.go b/ecs/logs.go index f54a9d111..e36cd3df6 100644 --- a/ecs/logs.go +++ b/ecs/logs.go @@ -55,8 +55,8 @@ func (a *allowListLogConsumer) Log(service, container, message string) { } } -func (a *allowListLogConsumer) Exit(service, container string, exitCode int) { +func (a *allowListLogConsumer) Status(service, container, message string) { if a.allowList[service] { - a.delegate.Exit(service, container, exitCode) + a.delegate.Status(service, container, message) } } diff --git a/local/compose/attach.go b/local/compose/attach.go index 3292e5601..d0ecc963a 100644 --- a/local/compose/attach.go +++ b/local/compose/attach.go @@ -44,7 +44,10 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) for _, container := range containers { - s.attachContainer(ctx, container, consumer, project) + err := s.attachContainer(ctx, container, consumer, project) + if err != nil { + return nil, err + } } return containers, nil } diff --git a/local/compose/containers.go b/local/compose/containers.go index c90e4e0c7..470c96ff2 100644 --- a/local/compose/containers.go +++ b/local/compose/containers.go @@ -18,6 +18,7 @@ package compose import ( "context" + "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" diff --git a/local/compose/start.go b/local/compose/start.go index a81164bf8..47cdb470c 100644 --- a/local/compose/start.go +++ b/local/compose/start.go @@ -18,11 +18,12 @@ package compose import ( "context" - "github.com/docker/docker/api/types/container" + "fmt" "github.com/docker/compose-cli/api/compose" "github.com/compose-spec/compose-go/types" + "github.com/docker/docker/api/types/container" "golang.org/x/sync/errgroup" ) @@ -60,7 +61,14 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, opti statusC, errC := s.apiClient.ContainerWait(ctx, c.ID, container.WaitConditionNotRunning) select { case status := <-statusC: - options.Attach.Exit(c.Labels[serviceLabel], getContainerNameWithoutProject(c), int(status.StatusCode)) + service := c.Labels[serviceLabel] + options.Attach.Status(service, getContainerNameWithoutProject(c), fmt.Sprintf("exited with code %d", status.StatusCode)) + if options.Listener != nil { + options.Listener <- compose.Event{ + Service: service, + Status: int(status.StatusCode), + } + } return nil case err := <-errC: return err