Skip to content
Open
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
33 changes: 32 additions & 1 deletion cli/command/container/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/docker/cli/internal/volumespec"
"github.com/docker/cli/opts"
"github.com/docker/go-connections/nat"
"github.com/google/shlex"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
Expand Down Expand Up @@ -131,6 +132,7 @@ type containerOptions struct {
shmSize opts.MemBytes
noHealthcheck bool
healthCmd string
healthCmdMode string
healthInterval time.Duration
healthTimeout time.Duration
healthStartPeriod time.Duration
Expand Down Expand Up @@ -261,6 +263,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {

// Health-checking
flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
flags.StringVar(&copts.healthCmdMode, "health-cmd-mode", "shell", `Healthcheck command mode: "shell" runs via CMD-SHELL, "exec" uses exec form (CMD) for shell-less images`)
flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)")
flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy")
flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)")
Expand Down Expand Up @@ -559,6 +562,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con

// Healthcheck
var healthConfig *container.HealthConfig
if flags.Changed("health-cmd-mode") && copts.healthCmd == "" {
return nil, errors.New("--health-cmd-mode requires --health-cmd")
}
haveHealthSettings := copts.healthCmd != "" ||
copts.healthInterval != 0 ||
copts.healthTimeout != 0 ||
Expand All @@ -573,7 +579,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
} else if haveHealthSettings {
var probe []string
if copts.healthCmd != "" {
probe = []string{"CMD-SHELL", copts.healthCmd}
var err error
probe, err = healthcheckProbe(copts.healthCmd, copts.healthCmdMode)
if err != nil {
return nil, err
}
}
if copts.healthInterval < 0 {
return nil, errors.New("--health-interval cannot be negative")
Expand Down Expand Up @@ -1130,6 +1140,27 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
return val, nil
}

// healthcheckProbe builds the HealthConfig.Test slice for --health-cmd.
// mode must be "shell" (CMD-SHELL, the default) or "exec" (CMD exec form,
// required for images without a shell such as scratch or distroless).
func healthcheckProbe(cmd, mode string) ([]string, error) {
switch mode {
case "", "shell":
return []string{"CMD-SHELL", cmd}, nil
case "exec":
parts, err := shlex.Split(cmd)
if err != nil {
return nil, fmt.Errorf("--health-cmd: %w", err)
}
if len(parts) == 0 {
return nil, errors.New("--health-cmd: command must not be empty")
}
return append([]string{"CMD"}, parts...), nil
default:
return nil, fmt.Errorf("--health-cmd-mode: invalid value %q, must be one of \"shell\" or \"exec\"", mode)
}
}

// validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) {
s := strings.ToLower(val)
Expand Down
67 changes: 55 additions & 12 deletions cli/command/container/opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -893,15 +893,6 @@ func TestParseHealth(t *testing.T) {
}
return config.Healthcheck
}
checkError := func(expected string, args ...string) {
config, _, _, err := parseRun(args)
if err == nil {
t.Fatalf("Expected error, but got %#v", config)
}
if err.Error() != expected {
t.Fatalf("Expected %#v, got %#v", expected, err)
}
}
health := checkOk("--no-healthcheck", "img", "cmd")
if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" {
t.Fatalf("--no-healthcheck failed: %#v", health)
Expand All @@ -915,15 +906,67 @@ func TestParseHealth(t *testing.T) {
t.Fatalf("--health-cmd: timeout = %s", health.Timeout)
}

checkError("--no-healthcheck conflicts with --health-* options",
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")

health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "--health-start-interval=1s", "img", "cmd")
if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second || health.StartInterval != 1*time.Second {
t.Fatalf("--health-*: got %#v", health)
}
}

func TestParseHealthCmdMode(t *testing.T) {
checkOk := func(args ...string) *container.HealthConfig {
config, _, _, err := parseRun(args)
if err != nil {
t.Fatalf("%#v: %v", args, err)
}
return config.Healthcheck
}
checkError := func(expected string, args ...string) {
config, _, _, err := parseRun(args)
if err == nil {
t.Fatalf("Expected error, but got %#v", config)
}
if err.Error() != expected {
t.Fatalf("Expected %#v, got %#v", expected, err)
}
}

health := checkOk("--health-cmd=/healthcheck", "--health-cmd-mode=exec", "img", "cmd")
if len(health.Test) != 2 || health.Test[0] != "CMD" || health.Test[1] != "/healthcheck" {
t.Fatalf("--health-cmd-mode=exec single arg: got %#v", health.Test)
}

health = checkOk("--health-cmd=/usr/bin/wget -q -O /dev/null http://localhost/", "--health-cmd-mode=exec", "img", "cmd")
want := []string{"CMD", "/usr/bin/wget", "-q", "-O", "/dev/null", "http://localhost/"}
if len(health.Test) != len(want) {
t.Fatalf("--health-cmd-mode=exec multi arg: got %#v, want %#v", health.Test, want)
}
for i := range want {
if health.Test[i] != want[i] {
t.Fatalf("--health-cmd-mode=exec multi arg: got %#v, want %#v", health.Test, want)
}
}

health = checkOk("--health-cmd=/check.sh", "--health-cmd-mode=shell", "img", "cmd")
if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh" {
t.Fatalf("--health-cmd-mode=shell explicit: got %#v", health.Test)
}

checkError("--health-cmd-mode: invalid value \"bad\", must be one of \"shell\" or \"exec\"",
"--health-cmd=/check.sh", "--health-cmd-mode=bad", "img", "cmd")

checkError("--health-cmd-mode requires --health-cmd",
"--health-cmd-mode=exec", "img", "cmd")

checkError("--health-cmd: EOF found when expecting closing quote",
"--health-cmd=unclosed 'quote", "--health-cmd-mode=exec", "img", "cmd")

checkError("--health-cmd: command must not be empty",
"--health-cmd= ", "--health-cmd-mode=exec", "img", "cmd")

checkError("--no-healthcheck conflicts with --health-* options",
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
}

func TestParseLoggingOpts(t *testing.T) {
// logging opts ko
if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" {
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/container_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Create a new container
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
| `--group-add` | `list` | | Add additional groups to join |
| `--health-cmd` | `string` | | Command to run to check health |
| `--health-cmd-mode` | `string` | `shell` | Healthcheck command mode: `shell` wraps the command in CMD-SHELL, `exec` uses the exec form (CMD) required for shell-less images |
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/container_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Create and run a new container from an image
| [`--gpus`](#gpus) | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
| `--group-add` | `list` | | Add additional groups to join |
| `--health-cmd` | `string` | | Command to run to check health |
| `--health-cmd-mode` | `string` | `shell` | Healthcheck command mode: `shell` wraps the command in CMD-SHELL, `exec` uses the exec form (CMD) required for shell-less images |
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Create a new container
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
| `--group-add` | `list` | | Add additional groups to join |
| `--health-cmd` | `string` | | Command to run to check health |
| `--health-cmd-mode` | `string` | `shell` | Healthcheck command mode: `shell` wraps the command in CMD-SHELL, `exec` uses the exec form (CMD) required for shell-less images |
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Create and run a new container from an image
| `--gpus` | `gpu-request` | | GPU devices to add to the container ('all' to pass all GPUs) |
| `--group-add` | `list` | | Add additional groups to join |
| `--health-cmd` | `string` | | Command to run to check health |
| `--health-cmd-mode` | `string` | `shell` | Healthcheck command mode: `shell` wraps the command in CMD-SHELL, `exec` uses the exec form (CMD) required for shell-less images |
| `--health-interval` | `duration` | `0s` | Time between running the check (ms\|s\|m\|h) (default 0s) |
| `--health-retries` | `int` | `0` | Consecutive failures needed to report unhealthy |
| `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) |
Expand Down