introduce cascade stop "--abort-on-container-exit" option

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-02-08 11:42:42 +01:00
parent 7a7114fb5f
commit f3d093cb54
8 changed files with 56 additions and 13 deletions

View File

@ -67,8 +67,8 @@ type CreateOptions struct {
type StartOptions struct { type StartOptions struct {
// Attach will attach to container and pipe stdout/stderr to LogConsumer // Attach will attach to container and pipe stdout/stderr to LogConsumer
Attach LogConsumer Attach LogConsumer
// CascadeStop will run `Stop` on any container exit // Listener will get notified on container events
CascadeStop bool Listener Listener
} }
// UpOptions group options of the Up API // UpOptions group options of the Up API
@ -185,5 +185,14 @@ type Stack struct {
// LogConsumer is a callback to process log messages from services // LogConsumer is a callback to process log messages from services
type LogConsumer interface { type LogConsumer interface {
Log(service, container, message string) 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
} }

View File

@ -18,12 +18,12 @@ package compose
import ( import (
"context" "context"
"github.com/docker/compose-cli/api/compose"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "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/api/progress"
"github.com/docker/compose-cli/cli/formatter" "github.com/docker/compose-cli/cli/formatter"
) )

View File

@ -49,6 +49,7 @@ type upOptions struct {
forceRecreate bool forceRecreate bool
noRecreate bool noRecreate bool
noStart bool noStart bool
cascadeStop bool
} }
func (o upOptions) recreateStrategy() string { 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 { RunE: func(cmd *cobra.Command, args []string) error {
switch contextType { switch contextType {
case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType: 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 { if opts.forceRecreate && opts.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible") 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.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.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.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 return upCmd
@ -145,9 +150,25 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
return nil 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{ 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) { if errors.Is(ctx.Err(), context.Canceled) {
fmt.Println("Gracefully stopping...") fmt.Println("Gracefully stopping...")
ctx = context.Background() ctx = context.Background()

View File

@ -51,9 +51,10 @@ func (l *logConsumer) Log(service, container, message string) {
} }
} }
func (l *logConsumer) Exit(service, container string, exitCode int) { func (l *logConsumer) Status(service, container, msg string) {
msg := fmt.Sprintf("%s exited with code %d\n", container, exitCode) cf := l.getColorFunc(service)
l.writer.Write([]byte(l.getColorFunc(service)(msg))) 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 { func (l *logConsumer) getColorFunc(service string) colorFunc {

View File

@ -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] { if a.allowList[service] {
a.delegate.Exit(service, container, exitCode) a.delegate.Status(service, container, message)
} }
} }

View File

@ -44,7 +44,10 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con
fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
for _, container := range containers { 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 return containers, nil
} }

View File

@ -18,6 +18,7 @@ package compose
import ( import (
"context" "context"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"

View File

@ -18,11 +18,12 @@ package compose
import ( import (
"context" "context"
"github.com/docker/docker/api/types/container" "fmt"
"github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/compose"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup" "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) statusC, errC := s.apiClient.ContainerWait(ctx, c.ID, container.WaitConditionNotRunning)
select { select {
case status := <-statusC: 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 return nil
case err := <-errC: case err := <-errC:
return err return err