Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce --abort-on-container-failure #11680

Merged
merged 1 commit into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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) 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 @@
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incompatible with -d or cascadeStop or both?
If not compatible with -d, I think you forgot to add the associated check in validateFlags function

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 @@

//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")
}

Check warning on line 173 in cmd/compose/up.go

View check run for this annotation

Codecov / codecov/patch

cmd/compose/up.go#L172-L173

Added lines #L172 - L173 were not covered by tests
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 @@
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")

Check warning on line 184 in cmd/compose/up.go

View check run for this annotation

Codecov / codecov/patch

cmd/compose/up.go#L184

Added line #L184 was not covered by tests
}
if create.forceRecreate && create.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
Expand Down Expand Up @@ -278,7 +294,7 @@
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
// 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 @@
}

//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 @@
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
}

Check warning on line 135 in pkg/compose/printer.go

View check run for this annotation

Codecov / codecov/patch

pkg/compose/printer.go#L134-L135

Added lines #L134 - L135 were not covered by tests
}
}
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
exit:
image: alpine
command: /bin/true

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