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

@ -104,10 +104,9 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
QuietPull: buildOpts.quiet, QuietPull: buildOpts.quiet,
}, },
Start: api.StartOptions{ Start: api.StartOptions{
Project: project, Project: project,
Attach: nil, Attach: nil,
CascadeStop: false, Services: services,
Services: services,
}, },
} }
if err := backend.Up(ctx, project, upOpts); err != nil { if err := backend.Up(ctx, project, upOpts); err != nil {

View File

@ -5,34 +5,35 @@ 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 |
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. | | `--abort-on-container-failure` | | | Stops all containers if any container exited with failure. Incompatible with -d |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. | | `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
| `--attach-dependencies` | | | Automatically attach to log output of dependent services | | `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--build` | | | Build images before starting containers | | `--attach-dependencies` | | | Automatically attach to log output of dependent services |
| `-d`, `--detach` | | | Detached mode: Run containers in the background | | `--build` | | | Build images before starting containers |
| `--dry-run` | | | Execute command in dry run mode | | `-d`, `--detach` | | | Detached mode: Run containers in the background |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit | | `--dry-run` | | | Execute command in dry run mode |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed | | `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services | | `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed |
| `--no-build` | | | Don't build an image, even if it's policy | | `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
| `--no-color` | | | Produce monochrome output | | `--no-build` | | | Don't build an image, even if it's policy |
| `--no-deps` | | | Don't start linked services | | `--no-color` | | | Produce monochrome output |
| `--no-log-prefix` | | | Don't print prefix in logs | | `--no-deps` | | | Don't start linked services |
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. | | `--no-log-prefix` | | | Don't print prefix in logs |
| `--no-start` | | | Don't start the services after creating them | | `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") | | `--no-start` | | | Don't start the services after creating them |
| `--quiet-pull` | | | Pull without printing progress information | | `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | | `--quiet-pull` | | | Pull without printing progress information |
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers | | `--remove-orphans` | | | Remove containers for services not defined in the Compose file |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | | `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running | | `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `--timestamps` | | | Show timestamps | | `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. | | `--timestamps` | | | Show timestamps |
| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy | | `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
| `-w`, `--watch` | | | Watch source code and rebuild/refresh containers when files are updated. | | `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy |
| `-w`, `--watch` | | | Watch source code and rebuild/refresh containers when files are updated. |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

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,14 +123,24 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
return 0, err return 0, err
} }
} }
if event.Type == api.ContainerEventExit { }
if exitCodeFrom == "" { if event.Type == api.ContainerEventExit {
exitCodeFrom = event.Service if cascade == api.CascadeFail && event.ExitCode != 0 {
} exitCodeFrom = event.Service
if exitCodeFrom == event.Service { if !aborting {
exitCode = event.ExitCode aborting = true
err := stopFn()
if err != nil {
return 0, err
}
} }
} }
if cascade == api.CascadeStop && exitCodeFrom == "" {
exitCodeFrom = event.Service
}
if exitCodeFrom == event.Service {
exitCode = event.ExitCode
}
} }
if len(containers) == 0 { if len(containers) == 0 {
// Last container terminated, done // Last container terminated, done

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"