Browse Source

Introduce --abort-on-container-failure

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 1 year ago
parent
commit
29692b5921

+ 20 - 4
cmd/compose/up.go

@@ -46,6 +46,7 @@ type upOptions struct {
 	noStart               bool
 	noDeps                bool
 	cascadeStop           bool
+	cascadeFail           bool
 	exitCodeFrom          string
 	noColor               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 {
 	up := upOptions{}
 	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(&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")
@@ -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")
@@ -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")
@@ -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,

+ 3 - 4
cmd/compose/watch.go

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

+ 29 - 28
docs/reference/compose_up.md

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

+ 11 - 0
docs/reference/docker_compose_up.yaml

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

+ 10 - 2
pkg/api/api.go

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

+ 1 - 1
pkg/compose/logs.go

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

+ 19 - 9
pkg/compose/printer.go

@@ -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()
 }
@@ -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
@@ -115,7 +115,7 @@ 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()
@@ -123,14 +123,24 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
 							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

+ 1 - 1
pkg/compose/up.go

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

+ 53 - 0
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 - 0
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"