Introduce --abort-on-container-failure

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2024-04-03 09:51:11 +02:00 committed by Nicolas De loof
parent 2658c372a7
commit 29692b5921
10 changed files with 155 additions and 49 deletions

View File

@ -46,6 +46,7 @@ type upOptions struct {
noStart bool noStart bool
noDeps bool noDeps bool
cascadeStop bool cascadeStop bool
cascadeFail bool
exitCodeFrom string exitCodeFrom string
noColor bool noColor bool
noPrefix bool noPrefix bool
@ -89,6 +90,17 @@ func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli, experimenta
} }
} }
func (opts upOptions) OnExit() api.Cascade {
switch {
case opts.cascadeStop:
return api.CascadeStop
case opts.cascadeFail:
return api.CascadeFail
default:
return api.CascadeIgnore
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, experiments *experimental.State) *cobra.Command { func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, experiments *experimental.State) *cobra.Command {
up := upOptions{} up := upOptions{}
create := createOptions{} create := createOptions{}
@ -131,6 +143,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them") flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them")
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
flags.BoolVar(&up.cascadeFail, "abort-on-container-failure", false, "Stops all containers if any container exited with failure. Incompatible with -d")
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit") flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running") flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running")
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps") flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps")
@ -152,9 +165,12 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
//nolint:gocyclo //nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error { func validateFlags(up *upOptions, create *createOptions) error {
if up.exitCodeFrom != "" { if up.exitCodeFrom != "" && !up.cascadeFail {
up.cascadeStop = true up.cascadeStop = true
} }
if up.cascadeStop && up.cascadeFail {
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
}
if up.wait { if up.wait {
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 { if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
@ -164,8 +180,8 @@ func validateFlags(up *upOptions, create *createOptions) error {
if create.Build && create.noBuild { if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible") return fmt.Errorf("--build and --no-build are incompatible")
} }
if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) { if up.Detach && (up.attachDependencies || up.cascadeStop || up.cascadeFail || len(up.attach) > 0) {
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach or --attach-dependencies")
} }
if create.forceRecreate && create.noRecreate { if create.forceRecreate && create.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible") return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
@ -278,7 +294,7 @@ func runUp(
Attach: consumer, Attach: consumer,
AttachTo: attach, AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom, ExitCodeFrom: upOptions.exitCodeFrom,
CascadeStop: upOptions.cascadeStop, OnExit: upOptions.OnExit(),
Wait: upOptions.wait, Wait: upOptions.wait,
WaitTimeout: timeout, WaitTimeout: timeout,
Watch: upOptions.watch, Watch: upOptions.watch,

View File

@ -106,7 +106,6 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
Start: api.StartOptions{ Start: api.StartOptions{
Project: project, Project: project,
Attach: nil, Attach: nil,
CascadeStop: false,
Services: services, Services: services,
}, },
} }

View File

@ -6,8 +6,9 @@ Create and start containers
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:-----------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------| |:-------------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------|
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d | | `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
| `--abort-on-container-failure` | | | Stops all containers if any container exited with failure. Incompatible with -d |
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. | | `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. | | `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--attach-dependencies` | | | Automatically attach to log output of dependent services | | `--attach-dependencies` | | | Automatically attach to log output of dependent services |

View File

@ -35,6 +35,17 @@ options:
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
- option: abort-on-container-failure
value_type: bool
default_value: "false"
description: |
Stops all containers if any container exited with failure. Incompatible with -d
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: always-recreate-deps - option: always-recreate-deps
value_type: bool value_type: bool
default_value: "false" default_value: "false"

View File

@ -209,8 +209,8 @@ type StartOptions struct {
Attach LogConsumer Attach LogConsumer
// AttachTo set the services to attach to // AttachTo set the services to attach to
AttachTo []string AttachTo []string
// CascadeStop stops the application when a container stops // OnExit defines behavior when a container stops
CascadeStop bool OnExit Cascade
// ExitCodeFrom return exit code from specified service // ExitCodeFrom return exit code from specified service
ExitCodeFrom string ExitCodeFrom string
// Wait won't return until containers reached the running|healthy state // Wait won't return until containers reached the running|healthy state
@ -222,6 +222,14 @@ type StartOptions struct {
NavigationMenu bool NavigationMenu bool
} }
type Cascade int
const (
CascadeIgnore Cascade = iota
CascadeStop Cascade = iota
CascadeFail Cascade = iota
)
// RestartOptions group options of the Restart API // RestartOptions group options of the Restart API
type RestartOptions struct { type RestartOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name // Project is the compose project used to define this app. Might be nil if user ran command just with project name

View File

@ -80,7 +80,7 @@ func (s *composeService) Logs(
containers = containers.filter(isRunning()) containers = containers.filter(isRunning())
printer := newLogPrinter(consumer) printer := newLogPrinter(consumer)
eg.Go(func() error { eg.Go(func() error {
_, err := printer.Run(false, "", nil) _, err := printer.Run(api.CascadeIgnore, "", nil)
return err return err
}) })

View File

@ -26,7 +26,7 @@ import (
// logPrinter watch application containers an collect their logs // logPrinter watch application containers an collect their logs
type logPrinter interface { type logPrinter interface {
HandleEvent(event api.ContainerEvent) HandleEvent(event api.ContainerEvent)
Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error)
Cancel() Cancel()
Stop() Stop()
} }
@ -79,7 +79,7 @@ func (p *printer) HandleEvent(event api.ContainerEvent) {
} }
//nolint:gocyclo //nolint:gocyclo
func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error) {
var ( var (
aborting bool aborting bool
exitCode int exitCode int
@ -115,7 +115,7 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
delete(containers, id) delete(containers, id)
} }
if cascadeStop { if cascade == api.CascadeStop {
if !aborting { if !aborting {
aborting = true aborting = true
err := stopFn() err := stopFn()
@ -123,15 +123,25 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
return 0, err return 0, err
} }
} }
}
if event.Type == api.ContainerEventExit { if event.Type == api.ContainerEventExit {
if exitCodeFrom == "" { if cascade == api.CascadeFail && event.ExitCode != 0 {
exitCodeFrom = event.Service
if !aborting {
aborting = true
err := stopFn()
if err != nil {
return 0, err
}
}
}
if cascade == api.CascadeStop && exitCodeFrom == "" {
exitCodeFrom = event.Service exitCodeFrom = event.Service
} }
if exitCodeFrom == event.Service { if exitCodeFrom == event.Service {
exitCode = event.ExitCode exitCode = event.ExitCode
} }
} }
}
if len(containers) == 0 { if len(containers) == 0 {
// Last container terminated, done // Last container terminated, done
return exitCode, nil return exitCode, nil

View File

@ -134,7 +134,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
var exitCode int var exitCode int
eg.Go(func() error { eg.Go(func() error {
code, err := printer.Run(options.Start.CascadeStop, options.Start.ExitCodeFrom, func() error { code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
fmt.Fprintln(s.stdinfo(), "Aborting on container exit...") fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
return progress.Run(ctx, func(ctx context.Context) error { return progress.Run(ctx, func(ctx context.Context) error {
return s.Stop(ctx, project.Name, api.StopOptions{ return s.Stop(ctx, project.Name, api.StopOptions{

53
pkg/e2e/cascade_test.go Normal file
View File

@ -0,0 +1,53 @@
//go:build !windows
// +build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestCascadeStop(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-cascade-stop"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
"up", "--abort-on-container-exit")
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
}
func TestCascadeFail(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-cascade-fail"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
"up", "--abort-on-container-failure")
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "fail-1 exited with code 1"), res.Combined())
assert.Equal(t, res.ExitCode, 1)
}

View File

@ -0,0 +1,8 @@
services:
exit:
image: alpine
command: /bin/true
fail:
image: alpine
command: sh -c "sleep 0.1 && /bin/false"