Skip to content

Commit

Permalink
Introduce --abort-on-container-failure
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Apr 8, 2024
1 parent 2658c37 commit 29692b5
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 49 deletions.
24 changes: 20 additions & 4 deletions cmd/compose/up.go
Expand Up @@ -46,6 +46,7 @@ type upOptions struct {
noStart bool
noDeps bool
cascadeStop bool
cascadeFail bool
exitCodeFrom string
noColor bool
noPrefix bool
Expand Down Expand Up @@ -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 {
up := upOptions{}
create := createOptions{}
Expand Down Expand Up @@ -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(&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.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.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")
Expand All @@ -152,9 +165,12 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex

//nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error {
if up.exitCodeFrom != "" {
if up.exitCodeFrom != "" && !up.cascadeFail {
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.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
Expand All @@ -164,8 +180,8 @@ func validateFlags(up *upOptions, create *createOptions) error {
if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) {
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
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, --abort-on-container-failure, --attach or --attach-dependencies")
}
if create.forceRecreate && create.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
Expand Down Expand Up @@ -278,7 +294,7 @@ func runUp(
Attach: consumer,
AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom,
CascadeStop: upOptions.cascadeStop,
OnExit: upOptions.OnExit(),
Wait: upOptions.wait,
WaitTimeout: timeout,
Watch: upOptions.watch,
Expand Down
7 changes: 3 additions & 4 deletions cmd/compose/watch.go
Expand Up @@ -104,10 +104,9 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
QuietPull: buildOpts.quiet,
},
Start: api.StartOptions{
Project: project,
Attach: nil,
CascadeStop: false,
Services: services,
Project: project,
Attach: nil,
Services: services,
},
}
if err := backend.Up(ctx, project, upOpts); err != nil {
Expand Down
57 changes: 29 additions & 28 deletions docs/reference/compose_up.md
Expand Up @@ -5,34 +5,35 @@ Create and start containers

### Options

| Name | Type | Default | Description |
|:-----------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------|
| `--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. |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--attach-dependencies` | | | Automatically attach to log output of dependent services |
| `--build` | | | Build images before starting containers |
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
| `--dry-run` | | | Execute command in dry run mode |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed |
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
| `--no-build` | | | Don't build an image, even if it's policy |
| `--no-color` | | | Produce monochrome output |
| `--no-deps` | | | Don't start linked services |
| `--no-log-prefix` | | | Don't print prefix in logs |
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--no-start` | | | Don't start the services after creating them |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `--quiet-pull` | | | Pull without printing progress information |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file |
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
| `--timestamps` | | | Show timestamps |
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
| `--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. |
| Name | Type | Default | Description |
|:-------------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------|
| `--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. |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--attach-dependencies` | | | Automatically attach to log output of dependent services |
| `--build` | | | Build images before starting containers |
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
| `--dry-run` | | | Execute command in dry run mode |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed |
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
| `--no-build` | | | Don't build an image, even if it's policy |
| `--no-color` | | | Produce monochrome output |
| `--no-deps` | | | Don't start linked services |
| `--no-log-prefix` | | | Don't print prefix in logs |
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--no-start` | | | Don't start the services after creating them |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `--quiet-pull` | | | Pull without printing progress information |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file |
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
| `--timestamps` | | | Show timestamps |
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
| `--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-->
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/docker_compose_up.yaml
Expand Up @@ -35,6 +35,17 @@ options:
experimentalcli: false
kubernetes: 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
value_type: bool
default_value: "false"
Expand Down
12 changes: 10 additions & 2 deletions pkg/api/api.go
Expand Up @@ -209,8 +209,8 @@ type StartOptions struct {
Attach LogConsumer
// AttachTo set the services to attach to
AttachTo []string
// CascadeStop stops the application when a container stops
CascadeStop bool
// OnExit defines behavior when a container stops
OnExit Cascade
// ExitCodeFrom return exit code from specified service
ExitCodeFrom string
// Wait won't return until containers reached the running|healthy state
Expand All @@ -222,6 +222,14 @@ type StartOptions struct {
NavigationMenu bool
}

type Cascade int

const (
CascadeIgnore Cascade = iota
CascadeStop Cascade = iota
CascadeFail Cascade = iota
)

// RestartOptions group options of the Restart API
type RestartOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/logs.go
Expand Up @@ -80,7 +80,7 @@ func (s *composeService) Logs(
containers = containers.filter(isRunning())
printer := newLogPrinter(consumer)
eg.Go(func() error {
_, err := printer.Run(false, "", nil)
_, err := printer.Run(api.CascadeIgnore, "", nil)
return err
})

Expand Down
28 changes: 19 additions & 9 deletions pkg/compose/printer.go
Expand Up @@ -26,7 +26,7 @@ import (
// logPrinter watch application containers an collect their logs
type logPrinter interface {
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()
Stop()
}
Expand Down Expand Up @@ -79,7 +79,7 @@ func (p *printer) HandleEvent(event api.ContainerEvent) {
}

//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 (
aborting bool
exitCode int
Expand Down Expand Up @@ -115,22 +115,32 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
delete(containers, id)
}

if cascadeStop {
if cascade == api.CascadeStop {
if !aborting {
aborting = true
err := stopFn()
if err != nil {
return 0, err
}
}
if event.Type == api.ContainerEventExit {
if exitCodeFrom == "" {
exitCodeFrom = event.Service
}
if exitCodeFrom == event.Service {
exitCode = event.ExitCode
}
if event.Type == api.ContainerEventExit {
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
}
if exitCodeFrom == event.Service {
exitCode = event.ExitCode
}
}
if len(containers) == 0 {
// Last container terminated, done
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/up.go
Expand Up @@ -134,7 +134,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options

var exitCode int
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...")
return progress.Run(ctx, func(ctx context.Context) error {
return s.Stop(ctx, project.Name, api.StopOptions{
Expand Down
53 changes: 53 additions & 0 deletions pkg/e2e/cascade_test.go
@@ -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)
}
8 changes: 8 additions & 0 deletions pkg/e2e/fixtures/cascade/compose.yaml
@@ -0,0 +1,8 @@
services:
exit:
image: alpine
command: /bin/true

fail:
image: alpine
command: sh -c "sleep 0.1 && /bin/false"

0 comments on commit 29692b5

Please sign in to comment.