Skip to content

Commit ec531ea

Browse files
committed
container: add --health-cmd-mode 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-mode=exec to use the exec (CMD) form, matching Dockerfile HEALTHCHECK CMD behavior. The default "shell" keeps existing --health-cmd behavior unchanged. Fixes #3719 Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
1 parent 9f16882 commit ec531ea

6 files changed

Lines changed: 63 additions & 1 deletion

File tree

cli/command/container/opts.go

Lines changed: 32 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+
healthCmdMode string
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.StringVar(&copts.healthCmdMode, "health-cmd-mode", "shell", `Healthcheck command mode: "shell" runs via CMD-SHELL (default), "exec" uses the exec form (CMD) and is required 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)")
@@ -559,6 +562,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
559562

560563
// Healthcheck
561564
var healthConfig *container.HealthConfig
565+
if flags.Changed("health-cmd-mode") && copts.healthCmd == "" {
566+
return nil, errors.New("--health-cmd-mode requires --health-cmd")
567+
}
562568
haveHealthSettings := copts.healthCmd != "" ||
563569
copts.healthInterval != 0 ||
564570
copts.healthTimeout != 0 ||
@@ -573,7 +579,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
573579
} else if haveHealthSettings {
574580
var probe []string
575581
if copts.healthCmd != "" {
576-
probe = []string{"CMD-SHELL", copts.healthCmd}
582+
var err error
583+
probe, err = healthcheckProbe(copts.healthCmd, copts.healthCmdMode)
584+
if err != nil {
585+
return nil, err
586+
}
577587
}
578588
if copts.healthInterval < 0 {
579589
return nil, errors.New("--health-interval cannot be negative")
@@ -1130,6 +1140,27 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
11301140
return val, nil
11311141
}
11321142

1143+
// healthcheckProbe builds the HealthConfig.Test slice for --health-cmd.
1144+
// mode must be "shell" (CMD-SHELL, the default) or "exec" (CMD exec form,
1145+
// required for images without a shell such as scratch or distroless).
1146+
func healthcheckProbe(cmd, mode string) ([]string, error) {
1147+
switch mode {
1148+
case "", "shell":
1149+
return []string{"CMD-SHELL", cmd}, nil
1150+
case "exec":
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+
default:
1160+
return nil, fmt.Errorf("--health-cmd-mode: invalid value %q, must be one of \"shell\" or \"exec\"", mode)
1161+
}
1162+
}
1163+
11331164
// validateAttach validates that the specified string is a valid attach option.
11341165
func validateAttach(val string) (string, error) {
11351166
s := strings.ToLower(val)

cli/command/container/opts_test.go

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

918+
health = checkOk("--health-cmd=/healthcheck", "--health-cmd-mode=exec", "img", "cmd")
919+
if len(health.Test) != 2 || health.Test[0] != "CMD" || health.Test[1] != "/healthcheck" {
920+
t.Fatalf("--health-cmd-mode=exec: got %#v", health.Test)
921+
}
922+
923+
health = checkOk("--health-cmd=/usr/bin/wget -q -O /dev/null http://localhost/", "--health-cmd-mode=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-mode=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-mode=exec: got %#v, want %#v", health.Test, want)
931+
}
932+
}
933+
934+
health = checkOk("--health-cmd=/check.sh", "--health-cmd-mode=shell", "img", "cmd")
935+
if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh" {
936+
t.Fatalf("--health-cmd-mode=shell: got %#v", health.Test)
937+
}
938+
939+
checkError("--health-cmd-mode: invalid value \"bad\", must be one of \"shell\" or \"exec\"",
940+
"--health-cmd=/check.sh", "--health-cmd-mode=bad", "img", "cmd")
941+
942+
checkError("--health-cmd-mode requires --health-cmd",
943+
"--health-cmd-mode=exec", "img", "cmd")
944+
918945
checkError("--no-healthcheck conflicts with --health-* options",
919946
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
920947

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-mode` | `string` | `shell` | Healthcheck command mode: `shell` runs via CMD-SHELL (default), `exec` uses the exec form (CMD) and is required 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-mode` | `string` | `shell` | Healthcheck command mode: `shell` runs via CMD-SHELL (default), `exec` uses the exec form (CMD) and is required 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-mode` | `string` | `shell` | Healthcheck command mode: `shell` runs via CMD-SHELL (default), `exec` uses the exec form (CMD) and is required 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-mode` | `string` | `shell` | Healthcheck command mode: `shell` runs via CMD-SHELL (default), `exec` uses the exec form (CMD) and is required 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)