瀏覽代碼

Capture container exit code and dump on console

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 4 年之前
父節點
當前提交
7a7114fb5f

+ 1 - 1
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
 }
 

+ 1 - 1
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
 }
 

+ 10 - 1
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)
 }

+ 1 - 1
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

+ 5 - 2
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),
+	})
 }

+ 4 - 2
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()

+ 16 - 6
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 {

+ 2 - 2
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 {

+ 6 - 0
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)
+	}
+}

+ 1 - 1
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
 }
 

+ 1 - 1
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
 }
 

+ 14 - 24
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) {

+ 10 - 0
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":

+ 21 - 1
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
 

+ 31 - 8
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
+	}
+
+	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 nil
+	return eg.Wait()
 }