Skip to content

Commit f860289

Browse files
authored
Merge pull request moby#27369 from cezarsa/hc
Add --health-* flags to service create and update
2 parents 9aa7501 + 7bd2611 commit f860289

21 files changed

Lines changed: 1516 additions & 440 deletions

File tree

api/types/swarm/container.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ package swarm
33
import (
44
"time"
55

6+
"github.com/docker/docker/api/types/container"
67
"github.com/docker/docker/api/types/mount"
78
)
89

910
// ContainerSpec represents the spec of a container.
1011
type ContainerSpec struct {
11-
Image string `json:",omitempty"`
12-
Labels map[string]string `json:",omitempty"`
13-
Command []string `json:",omitempty"`
14-
Args []string `json:",omitempty"`
15-
Env []string `json:",omitempty"`
16-
Dir string `json:",omitempty"`
17-
User string `json:",omitempty"`
18-
Groups []string `json:",omitempty"`
19-
TTY bool `json:",omitempty"`
20-
Mounts []mount.Mount `json:",omitempty"`
21-
StopGracePeriod *time.Duration `json:",omitempty"`
12+
Image string `json:",omitempty"`
13+
Labels map[string]string `json:",omitempty"`
14+
Command []string `json:",omitempty"`
15+
Args []string `json:",omitempty"`
16+
Env []string `json:",omitempty"`
17+
Dir string `json:",omitempty"`
18+
User string `json:",omitempty"`
19+
Groups []string `json:",omitempty"`
20+
TTY bool `json:",omitempty"`
21+
Mounts []mount.Mount `json:",omitempty"`
22+
StopGracePeriod *time.Duration `json:",omitempty"`
23+
Healthcheck *container.HealthConfig `json:",omitempty"`
2224
}

cli/command/service/opts.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/docker/docker/api/types/container"
1112
mounttypes "github.com/docker/docker/api/types/mount"
1213
"github.com/docker/docker/api/types/swarm"
1314
"github.com/docker/docker/opts"
@@ -68,6 +69,25 @@ func (c *nanoCPUs) Value() int64 {
6869
return int64(*c)
6970
}
7071

72+
// PositiveDurationOpt is an option type for time.Duration that uses a pointer.
73+
// It bahave similarly to DurationOpt but only allows positive duration values.
74+
type PositiveDurationOpt struct {
75+
DurationOpt
76+
}
77+
78+
// Set a new value on the option. Setting a negative duration value will cause
79+
// an error to be returned.
80+
func (d *PositiveDurationOpt) Set(s string) error {
81+
err := d.DurationOpt.Set(s)
82+
if err != nil {
83+
return err
84+
}
85+
if *d.DurationOpt.value < 0 {
86+
return fmt.Errorf("duration cannot be negative")
87+
}
88+
return nil
89+
}
90+
7191
// DurationOpt is an option type for time.Duration that uses a pointer. This
7292
// allows us to get nil values outside, instead of defaulting to 0
7393
type DurationOpt struct {
@@ -377,6 +397,47 @@ func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
377397
}
378398
}
379399

400+
type healthCheckOptions struct {
401+
cmd string
402+
interval PositiveDurationOpt
403+
timeout PositiveDurationOpt
404+
retries int
405+
noHealthcheck bool
406+
}
407+
408+
func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) {
409+
var healthConfig *container.HealthConfig
410+
haveHealthSettings := opts.cmd != "" ||
411+
opts.interval.Value() != nil ||
412+
opts.timeout.Value() != nil ||
413+
opts.retries != 0
414+
if opts.noHealthcheck {
415+
if haveHealthSettings {
416+
return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
417+
}
418+
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
419+
} else if haveHealthSettings {
420+
var test []string
421+
if opts.cmd != "" {
422+
test = []string{"CMD-SHELL", opts.cmd}
423+
}
424+
var interval, timeout time.Duration
425+
if ptr := opts.interval.Value(); ptr != nil {
426+
interval = *ptr
427+
}
428+
if ptr := opts.timeout.Value(); ptr != nil {
429+
timeout = *ptr
430+
}
431+
healthConfig = &container.HealthConfig{
432+
Test: test,
433+
Interval: interval,
434+
Timeout: timeout,
435+
Retries: opts.retries,
436+
}
437+
}
438+
return healthConfig, nil
439+
}
440+
380441
// ValidatePort validates a string is in the expected format for a port definition
381442
func ValidatePort(value string) (string, error) {
382443
portMappings, err := nat.ParsePortSpec(value)
@@ -416,6 +477,8 @@ type serviceOptions struct {
416477
registryAuth bool
417478

418479
logDriver logDriverOptions
480+
481+
healthcheck healthCheckOptions
419482
}
420483

421484
func newServiceOptions() *serviceOptions {
@@ -490,6 +553,12 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
490553
EndpointSpec: opts.endpoint.ToEndpointSpec(),
491554
}
492555

556+
healthConfig, err := opts.healthcheck.toHealthConfig()
557+
if err != nil {
558+
return service, err
559+
}
560+
service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig
561+
493562
switch opts.mode {
494563
case "global":
495564
if opts.replicas.Value() != nil {
@@ -541,6 +610,12 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
541610

542611
flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
543612
flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
613+
614+
flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health")
615+
flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check")
616+
flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run")
617+
flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy")
618+
flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
544619
}
545620

546621
const (
@@ -589,4 +664,9 @@ const (
589664
flagRegistryAuth = "with-registry-auth"
590665
flagLogDriver = "log-driver"
591666
flagLogOpt = "log-opt"
667+
flagHealthCmd = "health-cmd"
668+
flagHealthInterval = "health-interval"
669+
flagHealthRetries = "health-retries"
670+
flagHealthTimeout = "health-timeout"
671+
flagNoHealthcheck = "no-healthcheck"
592672
)

cli/command/service/opts_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package service
22

33
import (
4+
"reflect"
45
"testing"
56
"time"
67

8+
"github.com/docker/docker/api/types/container"
79
mounttypes "github.com/docker/docker/api/types/mount"
810
"github.com/docker/docker/pkg/testutil/assert"
911
)
@@ -40,6 +42,15 @@ func TestDurationOptSetAndValue(t *testing.T) {
4042
var duration DurationOpt
4143
assert.NilError(t, duration.Set("300s"))
4244
assert.Equal(t, *duration.Value(), time.Duration(300*10e8))
45+
assert.NilError(t, duration.Set("-300s"))
46+
assert.Equal(t, *duration.Value(), time.Duration(-300*10e8))
47+
}
48+
49+
func TestPositiveDurationOptSetAndValue(t *testing.T) {
50+
var duration PositiveDurationOpt
51+
assert.NilError(t, duration.Set("300s"))
52+
assert.Equal(t, *duration.Value(), time.Duration(300*10e8))
53+
assert.Error(t, duration.Set("-300s"), "cannot be negative")
4354
}
4455

4556
func TestUint64OptString(t *testing.T) {
@@ -201,3 +212,41 @@ func TestMountOptTypeConflict(t *testing.T) {
201212
assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
202213
assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
203214
}
215+
216+
func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
217+
dur := time.Second
218+
opt := healthCheckOptions{
219+
cmd: "curl",
220+
interval: PositiveDurationOpt{DurationOpt{value: &dur}},
221+
timeout: PositiveDurationOpt{DurationOpt{value: &dur}},
222+
retries: 10,
223+
}
224+
config, err := opt.toHealthConfig()
225+
assert.NilError(t, err)
226+
assert.Equal(t, reflect.DeepEqual(config, &container.HealthConfig{
227+
Test: []string{"CMD-SHELL", "curl"},
228+
Interval: time.Second,
229+
Timeout: time.Second,
230+
Retries: 10,
231+
}), true)
232+
}
233+
234+
func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) {
235+
opt := healthCheckOptions{
236+
noHealthcheck: true,
237+
}
238+
config, err := opt.toHealthConfig()
239+
assert.NilError(t, err)
240+
assert.Equal(t, reflect.DeepEqual(config, &container.HealthConfig{
241+
Test: []string{"NONE"},
242+
}), true)
243+
}
244+
245+
func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
246+
opt := healthCheckOptions{
247+
cmd: "curl",
248+
noHealthcheck: true,
249+
}
250+
_, err := opt.toHealthConfig()
251+
assert.Error(t, err, "--no-healthcheck conflicts with --health-* options")
252+
}

cli/command/service/update.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"golang.org/x/net/context"
1010

1111
"github.com/docker/docker/api/types"
12+
"github.com/docker/docker/api/types/container"
1213
mounttypes "github.com/docker/docker/api/types/mount"
1314
"github.com/docker/docker/api/types/swarm"
1415
"github.com/docker/docker/cli"
@@ -266,6 +267,10 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
266267
spec.TaskTemplate.ForceUpdate++
267268
}
268269

270+
if err := updateHealthcheck(flags, cspec); err != nil {
271+
return err
272+
}
273+
269274
return nil
270275
}
271276

@@ -537,3 +542,48 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
537542

538543
return nil
539544
}
545+
546+
func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error {
547+
if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) {
548+
return nil
549+
}
550+
if containerSpec.Healthcheck == nil {
551+
containerSpec.Healthcheck = &container.HealthConfig{}
552+
}
553+
noHealthcheck, err := flags.GetBool(flagNoHealthcheck)
554+
if err != nil {
555+
return err
556+
}
557+
if noHealthcheck {
558+
if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) {
559+
containerSpec.Healthcheck = &container.HealthConfig{
560+
Test: []string{"NONE"},
561+
}
562+
return nil
563+
}
564+
return fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
565+
}
566+
if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" {
567+
containerSpec.Healthcheck.Test = nil
568+
}
569+
if flags.Changed(flagHealthInterval) {
570+
val := *flags.Lookup(flagHealthInterval).Value.(*PositiveDurationOpt).Value()
571+
containerSpec.Healthcheck.Interval = val
572+
}
573+
if flags.Changed(flagHealthTimeout) {
574+
val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value()
575+
containerSpec.Healthcheck.Timeout = val
576+
}
577+
if flags.Changed(flagHealthRetries) {
578+
containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries)
579+
}
580+
if flags.Changed(flagHealthCmd) {
581+
cmd, _ := flags.GetString(flagHealthCmd)
582+
if cmd != "" {
583+
containerSpec.Healthcheck.Test = []string{"CMD-SHELL", cmd}
584+
} else {
585+
containerSpec.Healthcheck.Test = nil
586+
}
587+
}
588+
return nil
589+
}

cli/command/service/update_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package service
22

33
import (
4+
"reflect"
45
"sort"
56
"testing"
7+
"time"
68

9+
"github.com/docker/docker/api/types/container"
710
mounttypes "github.com/docker/docker/api/types/mount"
811
"github.com/docker/docker/api/types/swarm"
912
"github.com/docker/docker/pkg/testutil/assert"
@@ -196,3 +199,79 @@ func TestUpdatePortsConflictingFlags(t *testing.T) {
196199
err := updatePorts(flags, &portConfigs)
197200
assert.Error(t, err, "conflicting port mapping")
198201
}
202+
203+
func TestUpdateHealthcheckTable(t *testing.T) {
204+
type test struct {
205+
flags [][2]string
206+
initial *container.HealthConfig
207+
expected *container.HealthConfig
208+
err string
209+
}
210+
testCases := []test{
211+
{
212+
flags: [][2]string{{"no-healthcheck", "true"}},
213+
initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10},
214+
expected: &container.HealthConfig{Test: []string{"NONE"}},
215+
},
216+
{
217+
flags: [][2]string{{"health-cmd", "cmd1"}},
218+
initial: &container.HealthConfig{Test: []string{"NONE"}},
219+
expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}},
220+
},
221+
{
222+
flags: [][2]string{{"health-retries", "10"}},
223+
initial: &container.HealthConfig{Test: []string{"NONE"}},
224+
expected: &container.HealthConfig{Retries: 10},
225+
},
226+
{
227+
flags: [][2]string{{"health-retries", "10"}},
228+
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
229+
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
230+
},
231+
{
232+
flags: [][2]string{{"health-interval", "1m"}},
233+
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
234+
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute},
235+
},
236+
{
237+
flags: [][2]string{{"health-cmd", ""}},
238+
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
239+
expected: &container.HealthConfig{Retries: 10},
240+
},
241+
{
242+
flags: [][2]string{{"health-retries", "0"}},
243+
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
244+
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
245+
},
246+
{
247+
flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}},
248+
err: "--no-healthcheck conflicts with --health-* options",
249+
},
250+
{
251+
flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}},
252+
err: "--no-healthcheck conflicts with --health-* options",
253+
},
254+
{
255+
flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}},
256+
err: "--no-healthcheck conflicts with --health-* options",
257+
},
258+
}
259+
for i, c := range testCases {
260+
flags := newUpdateCommand(nil).Flags()
261+
for _, flag := range c.flags {
262+
flags.Set(flag[0], flag[1])
263+
}
264+
cspec := &swarm.ContainerSpec{
265+
Healthcheck: c.initial,
266+
}
267+
err := updateHealthcheck(flags, cspec)
268+
if c.err != "" {
269+
assert.Error(t, err, c.err)
270+
} else {
271+
assert.NilError(t, err)
272+
if !reflect.DeepEqual(cspec.Healthcheck, c.expected) {
273+
t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck)
274+
}
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)