Skip to content

Commit 29692b5

Browse files
committed
Introduce --abort-on-container-failure
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 2658c37 commit 29692b5

File tree

10 files changed

+155
-49
lines changed

10 files changed

+155
-49
lines changed

cmd/compose/up.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type upOptions struct {
4646
noStart bool
4747
noDeps bool
4848
cascadeStop bool
49+
cascadeFail bool
4950
exitCodeFrom string
5051
noColor bool
5152
noPrefix bool
@@ -89,6 +90,17 @@ func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli, experimenta
8990
}
9091
}
9192

93+
func (opts upOptions) OnExit() api.Cascade {
94+
switch {
95+
case opts.cascadeStop:
96+
return api.CascadeStop
97+
case opts.cascadeFail:
98+
return api.CascadeFail
99+
default:
100+
return api.CascadeIgnore
101+
}
102+
}
103+
92104
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, experiments *experimental.State) *cobra.Command {
93105
up := upOptions{}
94106
create := createOptions{}
@@ -131,6 +143,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
131143
flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
132144
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them")
133145
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
146+
flags.BoolVar(&up.cascadeFail, "abort-on-container-failure", false, "Stops all containers if any container exited with failure. Incompatible with -d")
134147
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
135148
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running")
136149
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps")
@@ -152,9 +165,12 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
152165

153166
//nolint:gocyclo
154167
func validateFlags(up *upOptions, create *createOptions) error {
155-
if up.exitCodeFrom != "" {
168+
if up.exitCodeFrom != "" && !up.cascadeFail {
156169
up.cascadeStop = true
157170
}
171+
if up.cascadeStop && up.cascadeFail {
172+
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
173+
}
158174
if up.wait {
159175
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
160176
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 {
164180
if create.Build && create.noBuild {
165181
return fmt.Errorf("--build and --no-build are incompatible")
166182
}
167-
if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) {
168-
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
183+
if up.Detach && (up.attachDependencies || up.cascadeStop || up.cascadeFail || len(up.attach) > 0) {
184+
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach or --attach-dependencies")
169185
}
170186
if create.forceRecreate && create.noRecreate {
171187
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
@@ -278,7 +294,7 @@ func runUp(
278294
Attach: consumer,
279295
AttachTo: attach,
280296
ExitCodeFrom: upOptions.exitCodeFrom,
281-
CascadeStop: upOptions.cascadeStop,
297+
OnExit: upOptions.OnExit(),
282298
Wait: upOptions.wait,
283299
WaitTimeout: timeout,
284300
Watch: upOptions.watch,

cmd/compose/watch.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,9 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
104104
QuietPull: buildOpts.quiet,
105105
},
106106
Start: api.StartOptions{
107-
Project: project,
108-
Attach: nil,
109-
CascadeStop: false,
110-
Services: services,
107+
Project: project,
108+
Attach: nil,
109+
Services: services,
111110
},
112111
}
113112
if err := backend.Up(ctx, project, upOpts); err != nil {

docs/reference/compose_up.md

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,35 @@ Create and start containers
55

66
### Options
77

8-
| Name | Type | Default | Description |
9-
|:-----------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------|
10-
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
11-
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
12-
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
13-
| `--attach-dependencies` | | | Automatically attach to log output of dependent services |
14-
| `--build` | | | Build images before starting containers |
15-
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
16-
| `--dry-run` | | | Execute command in dry run mode |
17-
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
18-
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed |
19-
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
20-
| `--no-build` | | | Don't build an image, even if it's policy |
21-
| `--no-color` | | | Produce monochrome output |
22-
| `--no-deps` | | | Don't start linked services |
23-
| `--no-log-prefix` | | | Don't print prefix in logs |
24-
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
25-
| `--no-start` | | | Don't start the services after creating them |
26-
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
27-
| `--quiet-pull` | | | Pull without printing progress information |
28-
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file |
29-
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers |
30-
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
31-
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
32-
| `--timestamps` | | | Show timestamps |
33-
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
34-
| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy |
35-
| `-w`, `--watch` | | | Watch source code and rebuild/refresh containers when files are updated. |
8+
| Name | Type | Default | Description |
9+
|:-------------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------|
10+
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
11+
| `--abort-on-container-failure` | | | Stops all containers if any container exited with failure. Incompatible with -d |
12+
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
13+
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
14+
| `--attach-dependencies` | | | Automatically attach to log output of dependent services |
15+
| `--build` | | | Build images before starting containers |
16+
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
17+
| `--dry-run` | | | Execute command in dry run mode |
18+
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
19+
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed |
20+
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services |
21+
| `--no-build` | | | Don't build an image, even if it's policy |
22+
| `--no-color` | | | Produce monochrome output |
23+
| `--no-deps` | | | Don't start linked services |
24+
| `--no-log-prefix` | | | Don't print prefix in logs |
25+
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
26+
| `--no-start` | | | Don't start the services after creating them |
27+
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
28+
| `--quiet-pull` | | | Pull without printing progress information |
29+
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file |
30+
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers |
31+
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
32+
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
33+
| `--timestamps` | | | Show timestamps |
34+
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
35+
| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy |
36+
| `-w`, `--watch` | | | Watch source code and rebuild/refresh containers when files are updated. |
3637

3738

3839
<!---MARKER_GEN_END-->

docs/reference/docker_compose_up.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ options:
3535
experimentalcli: false
3636
kubernetes: false
3737
swarm: false
38+
- option: abort-on-container-failure
39+
value_type: bool
40+
default_value: "false"
41+
description: |
42+
Stops all containers if any container exited with failure. Incompatible with -d
43+
deprecated: false
44+
hidden: false
45+
experimental: false
46+
experimentalcli: false
47+
kubernetes: false
48+
swarm: false
3849
- option: always-recreate-deps
3950
value_type: bool
4051
default_value: "false"

pkg/api/api.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ type StartOptions struct {
209209
Attach LogConsumer
210210
// AttachTo set the services to attach to
211211
AttachTo []string
212-
// CascadeStop stops the application when a container stops
213-
CascadeStop bool
212+
// OnExit defines behavior when a container stops
213+
OnExit Cascade
214214
// ExitCodeFrom return exit code from specified service
215215
ExitCodeFrom string
216216
// Wait won't return until containers reached the running|healthy state
@@ -222,6 +222,14 @@ type StartOptions struct {
222222
NavigationMenu bool
223223
}
224224

225+
type Cascade int
226+
227+
const (
228+
CascadeIgnore Cascade = iota
229+
CascadeStop Cascade = iota
230+
CascadeFail Cascade = iota
231+
)
232+
225233
// RestartOptions group options of the Restart API
226234
type RestartOptions struct {
227235
// Project is the compose project used to define this app. Might be nil if user ran command just with project name

pkg/compose/logs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (s *composeService) Logs(
8080
containers = containers.filter(isRunning())
8181
printer := newLogPrinter(consumer)
8282
eg.Go(func() error {
83-
_, err := printer.Run(false, "", nil)
83+
_, err := printer.Run(api.CascadeIgnore, "", nil)
8484
return err
8585
})
8686

pkg/compose/printer.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
// logPrinter watch application containers an collect their logs
2727
type logPrinter interface {
2828
HandleEvent(event api.ContainerEvent)
29-
Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error)
29+
Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error)
3030
Cancel()
3131
Stop()
3232
}
@@ -79,7 +79,7 @@ func (p *printer) HandleEvent(event api.ContainerEvent) {
7979
}
8080

8181
//nolint:gocyclo
82-
func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) {
82+
func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error) {
8383
var (
8484
aborting bool
8585
exitCode int
@@ -115,22 +115,32 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
115115
delete(containers, id)
116116
}
117117

118-
if cascadeStop {
118+
if cascade == api.CascadeStop {
119119
if !aborting {
120120
aborting = true
121121
err := stopFn()
122122
if err != nil {
123123
return 0, err
124124
}
125125
}
126-
if event.Type == api.ContainerEventExit {
127-
if exitCodeFrom == "" {
128-
exitCodeFrom = event.Service
129-
}
130-
if exitCodeFrom == event.Service {
131-
exitCode = event.ExitCode
126+
}
127+
if event.Type == api.ContainerEventExit {
128+
if cascade == api.CascadeFail && event.ExitCode != 0 {
129+
exitCodeFrom = event.Service
130+
if !aborting {
131+
aborting = true
132+
err := stopFn()
133+
if err != nil {
134+
return 0, err
135+
}
132136
}
133137
}
138+
if cascade == api.CascadeStop && exitCodeFrom == "" {
139+
exitCodeFrom = event.Service
140+
}
141+
if exitCodeFrom == event.Service {
142+
exitCode = event.ExitCode
143+
}
134144
}
135145
if len(containers) == 0 {
136146
// Last container terminated, done

pkg/compose/up.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
134134

135135
var exitCode int
136136
eg.Go(func() error {
137-
code, err := printer.Run(options.Start.CascadeStop, options.Start.ExitCodeFrom, func() error {
137+
code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
138138
fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
139139
return progress.Run(ctx, func(ctx context.Context) error {
140140
return s.Stop(ctx, project.Name, api.StopOptions{

pkg/e2e/cascade_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
/*
5+
Copyright 2022 Docker Compose CLI authors
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package e2e
21+
22+
import (
23+
"strings"
24+
"testing"
25+
26+
"gotest.tools/v3/assert"
27+
)
28+
29+
func TestCascadeStop(t *testing.T) {
30+
c := NewCLI(t)
31+
const projectName = "compose-e2e-cascade-stop"
32+
t.Cleanup(func() {
33+
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
34+
})
35+
36+
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
37+
"up", "--abort-on-container-exit")
38+
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
39+
}
40+
41+
func TestCascadeFail(t *testing.T) {
42+
c := NewCLI(t)
43+
const projectName = "compose-e2e-cascade-fail"
44+
t.Cleanup(func() {
45+
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
46+
})
47+
48+
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/cascade/compose.yaml", "--project-name", projectName,
49+
"up", "--abort-on-container-failure")
50+
assert.Assert(t, strings.Contains(res.Combined(), "exit-1 exited with code 0"), res.Combined())
51+
assert.Assert(t, strings.Contains(res.Combined(), "fail-1 exited with code 1"), res.Combined())
52+
assert.Equal(t, res.ExitCode, 1)
53+
}

pkg/e2e/fixtures/cascade/compose.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
exit:
3+
image: alpine
4+
command: /bin/true
5+
6+
fail:
7+
image: alpine
8+
command: sh -c "sleep 0.1 && /bin/false"

0 commit comments

Comments
 (0)