Skip to content

Commit e09b71d

Browse files
committed
container: add --health-cmd-exec for CMD healthcheck form
docker run/create always wrapped --health-cmd in CMD-SHELL, which breaks scratch and other images without a shell. Add --health-cmd-exec to use the exec (CMD) form, matching Dockerfile HEALTHCHECK CMD behavior. Fixes #3719 Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
1 parent 9f16882 commit e09b71d

6 files changed

Lines changed: 52 additions & 1 deletion

File tree

cli/command/container/opts.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/docker/cli/internal/volumespec"
2323
"github.com/docker/cli/opts"
2424
"github.com/docker/go-connections/nat"
25+
"github.com/google/shlex"
2526
"github.com/moby/moby/api/types/container"
2627
"github.com/moby/moby/api/types/mount"
2728
"github.com/moby/moby/api/types/network"
@@ -131,6 +132,7 @@ type containerOptions struct {
131132
shmSize opts.MemBytes
132133
noHealthcheck bool
133134
healthCmd string
135+
healthCmdExec bool
134136
healthInterval time.Duration
135137
healthTimeout time.Duration
136138
healthStartPeriod time.Duration
@@ -261,6 +263,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
261263

262264
// Health-checking
263265
flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
266+
flags.BoolVar(&copts.healthCmdExec, "health-cmd-exec", false, "Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell)")
264267
flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)")
265268
flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy")
266269
flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)")
@@ -560,6 +563,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
560563
// Healthcheck
561564
var healthConfig *container.HealthConfig
562565
haveHealthSettings := copts.healthCmd != "" ||
566+
copts.healthCmdExec ||
563567
copts.healthInterval != 0 ||
564568
copts.healthTimeout != 0 ||
565569
copts.healthStartPeriod != 0 ||
@@ -571,9 +575,16 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
571575
}
572576
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
573577
} else if haveHealthSettings {
578+
if copts.healthCmdExec && copts.healthCmd == "" {
579+
return nil, errors.New("--health-cmd-exec requires --health-cmd")
580+
}
574581
var probe []string
575582
if copts.healthCmd != "" {
576-
probe = []string{"CMD-SHELL", copts.healthCmd}
583+
var err error
584+
probe, err = healthcheckProbe(copts.healthCmd, copts.healthCmdExec)
585+
if err != nil {
586+
return nil, err
587+
}
577588
}
578589
if copts.healthInterval < 0 {
579590
return nil, errors.New("--health-interval cannot be negative")
@@ -1130,6 +1141,23 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
11301141
return val, nil
11311142
}
11321143

1144+
// healthcheckProbe builds the HealthConfig.Test slice for --health-cmd.
1145+
// When execForm is false, the command runs via CMD-SHELL; when true, via CMD
1146+
// (exec form), which is required for images without a shell (for example scratch).
1147+
func healthcheckProbe(cmd string, execForm bool) ([]string, error) {
1148+
if !execForm {
1149+
return []string{"CMD-SHELL", cmd}, nil
1150+
}
1151+
parts, err := shlex.Split(cmd)
1152+
if err != nil {
1153+
return nil, fmt.Errorf("--health-cmd: %w", err)
1154+
}
1155+
if len(parts) == 0 {
1156+
return nil, errors.New("--health-cmd: command must not be empty")
1157+
}
1158+
return append([]string{"CMD"}, parts...), nil
1159+
}
1160+
11331161
// validateAttach validates that the specified string is a valid attach option.
11341162
func validateAttach(val string) (string, error) {
11351163
s := strings.ToLower(val)

cli/command/container/opts_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,25 @@ func TestParseHealth(t *testing.T) {
915915
t.Fatalf("--health-cmd: timeout = %s", health.Timeout)
916916
}
917917

918+
health = checkOk("--health-cmd=/healthcheck", "--health-cmd-exec", "img", "cmd")
919+
if len(health.Test) != 2 || health.Test[0] != "CMD" || health.Test[1] != "/healthcheck" {
920+
t.Fatalf("--health-cmd-exec: got %#v", health.Test)
921+
}
922+
923+
health = checkOk("--health-cmd=/usr/bin/wget -q -O /dev/null http://localhost/", "--health-cmd-exec", "img", "cmd")
924+
want := []string{"CMD", "/usr/bin/wget", "-q", "-O", "/dev/null", "http://localhost/"}
925+
if len(health.Test) != len(want) {
926+
t.Fatalf("--health-cmd-exec: got %#v, want %#v", health.Test, want)
927+
}
928+
for i := range want {
929+
if health.Test[i] != want[i] {
930+
t.Fatalf("--health-cmd-exec: got %#v, want %#v", health.Test, want)
931+
}
932+
}
933+
934+
checkError("--health-cmd-exec requires --health-cmd",
935+
"--health-cmd-exec", "img", "cmd")
936+
918937
checkError("--no-healthcheck conflicts with --health-* options",
919938
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
920939

docs/reference/commandline/container_create.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Create a new container
4848
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
4949
| `--group-add` | `list` | | Add additional groups to join |
5050
| `--health-cmd` | `string` | | Command to run to check health |
51+
| `--health-cmd-exec` | `bool` | | Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell) |
5152
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
5253
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
5354
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |

docs/reference/commandline/container_run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Create and run a new container from an image
5050
| [`--gpus`](#gpus) | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
5151
| `--group-add` | `list` | | Add additional groups to join |
5252
| `--health-cmd` | `string` | | Command to run to check health |
53+
| `--health-cmd-exec` | `bool` | | Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell) |
5354
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
5455
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
5556
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |

docs/reference/commandline/create.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Create a new container
4848
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
4949
| `--group-add` | `list` | | Add additional groups to join |
5050
| `--health-cmd` | `string` | | Command to run to check health |
51+
| `--health-cmd-exec` | `bool` | | Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell) |
5152
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
5253
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
5354
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |

docs/reference/commandline/run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Create and run a new container from an image
5050
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
5151
| `--group-add` | `list` | | Add additional groups to join |
5252
| `--health-cmd` | `string` | | Command to run to check health |
53+
| `--health-cmd-exec` | `bool` | | Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell) |
5354
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
5455
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
5556
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |

0 commit comments

Comments
 (0)