From 7a7114fb5f351417be79dbbacf5beebb591506c5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 8 Feb 2021 11:04:46 +0100 Subject: [PATCH] Capture container exit code and dump on console Signed-off-by: Nicolas De Loof --- aci/compose.go | 2 +- api/client/compose.go | 2 +- api/compose/api.go | 11 ++++++++++- cli/cmd/compose/run.go | 2 +- cli/cmd/compose/start.go | 7 +++++-- cli/cmd/compose/up.go | 6 ++++-- cli/formatter/logs.go | 22 +++++++++++++++------ ecs/local/compose.go | 4 ++-- ecs/logs.go | 6 ++++++ ecs/up.go | 2 +- kube/compose.go | 2 +- local/compose/attach.go | 38 +++++++++++++----------------------- local/compose/compose.go | 10 ++++++++++ local/compose/containers.go | 22 ++++++++++++++++++++- local/compose/start.go | 39 +++++++++++++++++++++++++++++-------- 15 files changed, 124 insertions(+), 51 deletions(-) diff --git a/aci/compose.go b/aci/compose.go index 079acb983..b33c29049 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -60,7 +60,7 @@ func (cs *aciComposeService) Create(ctx context.Context, project *types.Project, return errdefs.ErrNotImplemented } -func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { +func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/client/compose.go b/api/client/compose.go index e201068f1..eacbc2ad8 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -44,7 +44,7 @@ func (c *composeService) Create(ctx context.Context, project *types.Project, opt return errdefs.ErrNotImplemented } -func (c *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { +func (c *composeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/compose/api.go b/api/compose/api.go index f3e32bf4b..aed5d79c7 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -34,7 +34,7 @@ type Service interface { // Create executes the equivalent to a `compose create` Create(ctx context.Context, project *types.Project, opts CreateOptions) error // Start executes the equivalent to a `compose start` - Start(ctx context.Context, project *types.Project, consumer LogConsumer) error + Start(ctx context.Context, project *types.Project, options StartOptions) error // Stop executes the equivalent to a `compose stop` Stop(ctx context.Context, project *types.Project) error // Up executes the equivalent to a `compose up` @@ -63,6 +63,14 @@ type CreateOptions struct { Recreate string } +// StartOptions group options of the Start API +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 +} + // UpOptions group options of the Up API type UpOptions struct { // Detach will create services and return immediately @@ -177,4 +185,5 @@ 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) } diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go index fc385b776..27cab00f0 100644 --- a/cli/cmd/compose/run.go +++ b/cli/cmd/compose/run.go @@ -102,7 +102,7 @@ func startDependencies(ctx context.Context, c *client.Client, project *types.Pro if err := c.ComposeService().Create(ctx, project, compose.CreateOptions{}); err != nil { return err } - if err := c.ComposeService().Start(ctx, project, nil); err != nil { + if err := c.ComposeService().Start(ctx, project, compose.StartOptions{}); err != nil { return err } return nil diff --git a/cli/cmd/compose/start.go b/cli/cmd/compose/start.go index f2290c3e9..fd734fb3f 100644 --- a/cli/cmd/compose/start.go +++ b/cli/cmd/compose/start.go @@ -18,6 +18,7 @@ package compose import ( "context" + "github.com/docker/compose-cli/api/compose" "os" "github.com/spf13/cobra" @@ -61,10 +62,12 @@ func runStart(ctx context.Context, opts startOptions, services []string) error { if opts.Detach { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Start(ctx, project, nil) + return "", c.ComposeService().Start(ctx, project, compose.StartOptions{}) }) return err } - return c.ComposeService().Start(ctx, project, formatter.NewLogConsumer(ctx, os.Stdout)) + return c.ComposeService().Start(ctx, project, compose.StartOptions{ + Attach: formatter.NewLogConsumer(ctx, os.Stdout), + }) } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 90da72c0c..325f81325 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -129,7 +129,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro return "", err } if opts.Detach { - err = c.ComposeService().Start(ctx, project, nil) + err = c.ComposeService().Start(ctx, project, compose.StartOptions{}) } return "", err }) @@ -145,7 +145,9 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro return nil } - err = c.ComposeService().Start(ctx, project, formatter.NewLogConsumer(ctx, os.Stdout)) + err = c.ComposeService().Start(ctx, project, compose.StartOptions{ + Attach: formatter.NewLogConsumer(ctx, os.Stdout), + }) 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 c3542bcd1..22f6e981d 100644 --- a/cli/formatter/logs.go +++ b/cli/formatter/logs.go @@ -42,12 +42,7 @@ func (l *logConsumer) Log(service, container, message string) { if l.ctx.Err() != nil { return } - cf, ok := l.colors[service] - if !ok { - cf = <-loop - l.colors[service] = cf - l.computeWidth() - } + cf := l.getColorFunc(service) prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container) for _, line := range strings.Split(message, "\n") { @@ -56,6 +51,21 @@ 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) getColorFunc(service string) colorFunc { + cf, ok := l.colors[service] + if !ok { + cf = <-loop + l.colors[service] = cf + l.computeWidth() + } + return cf +} + func (l *logConsumer) computeWidth() { width := 0 for n := range l.colors { diff --git a/ecs/local/compose.go b/ecs/local/compose.go index f110960c6..ce5710a17 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -53,8 +53,8 @@ func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project, return e.compose.Create(ctx, enhanced, opts) } -func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { - return e.compose.Start(ctx, project, consumer) +func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { + return e.compose.Start(ctx, project, options) } func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project) error { diff --git a/ecs/logs.go b/ecs/logs.go index 255e66e85..f54a9d111 100644 --- a/ecs/logs.go +++ b/ecs/logs.go @@ -54,3 +54,9 @@ func (a *allowListLogConsumer) Log(service, container, message string) { a.delegate.Log(service, container, message) } } + +func (a *allowListLogConsumer) Exit(service, container string, exitCode int) { + if a.allowList[service] { + a.delegate.Exit(service, container, exitCode) + } +} diff --git a/ecs/up.go b/ecs/up.go index 34acbd69f..f66bca71a 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -47,7 +47,7 @@ func (b *ecsAPIService) Create(ctx context.Context, project *types.Project, opts return errdefs.ErrNotImplemented } -func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { +func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { return errdefs.ErrNotImplemented } diff --git a/kube/compose.go b/kube/compose.go index 79995987a..f6bf9e90f 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -144,7 +144,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt } // Start executes the equivalent to a `compose start` -func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { +func (s *composeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { return errdefs.ErrNotImplemented } diff --git a/local/compose/attach.go b/local/compose/attach.go index 8149561fb..3292e5601 100644 --- a/local/compose/attach.go +++ b/local/compose/attach.go @@ -28,22 +28,14 @@ import ( "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/pkg/stdcopy" - "golang.org/x/sync/errgroup" ) -func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.LogConsumer) (*errgroup.Group, error) { - containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(project.Name), - ), - All: true, - }) +func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.LogConsumer) (Containers, error) { + containers, err := s.getContainers(ctx, project) if err != nil { return nil, err } - containers = Containers(containers).filter(isService(project.ServiceNames()...)) var names []string for _, c := range containers { @@ -51,19 +43,15 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con } fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) - eg, ctx := errgroup.WithContext(ctx) - for _, c := range containers { - container := c - eg.Go(func() error { - return s.attachContainer(ctx, container, consumer, project) - }) + for _, container := range containers { + s.attachContainer(ctx, container, consumer, project) } - return eg, nil + return containers, nil } func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer compose.LogConsumer, project *types.Project) error { serviceName := container.Labels[serviceLabel] - w := utils.GetWriter(serviceName, getCanonicalContainerName(container), consumer) + w := getWriter(serviceName, getContainerNameWithoutProject(container), consumer) service, err := project.GetService(serviceName) if err != nil { @@ -94,13 +82,15 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container m } if w != nil { - if tty { - _, err = io.Copy(w, stdout) - } else { - _, err = stdcopy.StdCopy(w, w, stdout) - } + go func() { + if tty { + io.Copy(w, stdout) // nolint:errcheck + } else { + stdcopy.StdCopy(w, w, stdout) // nolint:errcheck + } + }() } - return err + return nil } func (s *composeService) getContainerStreams(ctx context.Context, container moby.Container) (io.WriteCloser, io.ReadCloser, error) { diff --git a/local/compose/compose.go b/local/compose/compose.go index 894db8fe9..d3af6e536 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -55,6 +55,16 @@ func getCanonicalContainerName(c moby.Container) string { return c.Names[0][1:] } +func getContainerNameWithoutProject(c moby.Container) string { + name := getCanonicalContainerName(c) + project := c.Labels[projectLabel] + prefix := fmt.Sprintf("%s_%s_", project, c.Labels[serviceLabel]) + if strings.HasPrefix(name, prefix) { + return name[len(project)+1:] + } + return name +} + func (s *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) { switch options.Format { case "json": diff --git a/local/compose/containers.go b/local/compose/containers.go index ee13f9d23..c90e4e0c7 100644 --- a/local/compose/containers.go +++ b/local/compose/containers.go @@ -16,11 +16,31 @@ package compose -import moby "github.com/docker/docker/api/types" +import ( + "context" + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) // Containers is a set of moby Container type Containers []moby.Container +func (s *composeService) getContainers(ctx context.Context, project *types.Project) (Containers, error) { + var containers Containers + containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(project.Name), + ), + All: true, + }) + if err != nil { + return nil, err + } + containers = containers.filter(isService(project.ServiceNames()...)) + return containers, nil +} + // containerPredicate define a predicate we want container to satisfy for filtering operations type containerPredicate func(c moby.Container) bool diff --git a/local/compose/start.go b/local/compose/start.go index ab2e7ed52..a81164bf8 100644 --- a/local/compose/start.go +++ b/local/compose/start.go @@ -18,6 +18,7 @@ package compose import ( "context" + "github.com/docker/docker/api/types/container" "github.com/docker/compose-cli/api/compose" @@ -25,14 +26,20 @@ import ( "golang.org/x/sync/errgroup" ) -func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { - var group *errgroup.Group - if consumer != nil { - eg, err := s.attach(ctx, project, consumer) +func (s *composeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error { + var containers Containers + if options.Attach != nil { + c, err := s.attach(ctx, project, options.Attach) if err != nil { return err } - group = eg + containers = c + } else { + c, err := s.getContainers(ctx, project) + if err != nil { + return err + } + containers = c } err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { @@ -41,8 +48,24 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, cons if err != nil { return err } - if group != nil { - return group.Wait() + + if options.Attach == nil { + return nil } - return nil + + eg, ctx := errgroup.WithContext(ctx) + for _, c := range containers { + c := c + eg.Go(func() error { + 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)) + return nil + case err := <-errC: + return err + } + }) + } + return eg.Wait() }