Browse code

Adds container health support to docker ps filter

Signed-off-by: Josh Horwitz <horwitzja@gmail.com>

Josh Horwitz authored on 2016/07/16 03:21:19
Showing 10 changed files
... ...
@@ -280,9 +280,10 @@ type HealthcheckResult struct {
280 280
 
281 281
 // Health states
282 282
 const (
283
-	Starting  = "starting"  // Starting indicates that the container is not yet ready
284
-	Healthy   = "healthy"   // Healthy indicates that the container is running correctly
285
-	Unhealthy = "unhealthy" // Unhealthy indicates that the container has a problem
283
+	NoHealthcheck = "none"      // Indicates there is no healthcheck
284
+	Starting      = "starting"  // Starting indicates that the container is not yet ready
285
+	Healthy       = "healthy"   // Healthy indicates that the container is running correctly
286
+	Unhealthy     = "unhealthy" // Unhealthy indicates that the container has a problem
286 287
 )
287 288
 
288 289
 // Health stores information about the container's healthcheck results
... ...
@@ -7,6 +7,7 @@ import (
7 7
 
8 8
 	"golang.org/x/net/context"
9 9
 
10
+	"github.com/docker/docker/api/types"
10 11
 	"github.com/docker/go-units"
11 12
 )
12 13
 
... ...
@@ -78,6 +79,7 @@ func (s *State) String() string {
78 78
 		if h := s.Health; h != nil {
79 79
 			return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)), h.String())
80 80
 		}
81
+
81 82
 		return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
82 83
 	}
83 84
 
... ...
@@ -100,6 +102,23 @@ func (s *State) String() string {
100 100
 	return fmt.Sprintf("Exited (%d) %s ago", s.ExitCodeValue, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
101 101
 }
102 102
 
103
+// HealthString returns a single string to describe health status.
104
+func (s *State) HealthString() string {
105
+	if s.Health == nil {
106
+		return types.NoHealthcheck
107
+	}
108
+
109
+	return s.Health.String()
110
+}
111
+
112
+// IsValidHealthString checks if the provided string is a valid container health status or not.
113
+func IsValidHealthString(s string) bool {
114
+	return s == types.Starting ||
115
+		s == types.Healthy ||
116
+		s == types.Unhealthy ||
117
+		s == types.NoHealthcheck
118
+}
119
+
103 120
 // StateString returns a single string to describe state
104 121
 func (s *State) StateString() string {
105 122
 	if s.Running {
... ...
@@ -4,8 +4,30 @@ import (
4 4
 	"sync/atomic"
5 5
 	"testing"
6 6
 	"time"
7
+
8
+	"github.com/docker/docker/api/types"
7 9
 )
8 10
 
11
+func TestIsValidHealthString(t *testing.T) {
12
+	contexts := []struct {
13
+		Health   string
14
+		Expected bool
15
+	}{
16
+		{types.Healthy, true},
17
+		{types.Unhealthy, true},
18
+		{types.Starting, true},
19
+		{types.NoHealthcheck, true},
20
+		{"fail", false},
21
+	}
22
+
23
+	for _, c := range contexts {
24
+		v := IsValidHealthString(c.Health)
25
+		if v != c.Expected {
26
+			t.Fatalf("Expected %t, but got %t", c.Expected, v)
27
+		}
28
+	}
29
+}
30
+
9 31
 func TestStateRunStop(t *testing.T) {
10 32
 	s := NewState()
11 33
 	for i := 1; i < 3; i++ { // full lifecycle two times
... ...
@@ -33,6 +33,7 @@ var acceptedPsFilterTags = map[string]bool{
33 33
 	"label":     true,
34 34
 	"name":      true,
35 35
 	"status":    true,
36
+	"health":    true,
36 37
 	"since":     true,
37 38
 	"volume":    true,
38 39
 	"network":   true,
... ...
@@ -258,6 +259,17 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte
258 258
 		}
259 259
 	}
260 260
 
261
+	err = psFilters.WalkValues("health", func(value string) error {
262
+		if !container.IsValidHealthString(value) {
263
+			return fmt.Errorf("Unrecognised filter value for health: %s", value)
264
+		}
265
+
266
+		return nil
267
+	})
268
+	if err != nil {
269
+		return nil, err
270
+	}
271
+
261 272
 	var beforeContFilter, sinceContFilter *container.Container
262 273
 
263 274
 	err = psFilters.WalkValues("before", func(value string) error {
... ...
@@ -384,6 +396,11 @@ func includeContainerInList(container *container.Container, ctx *listContext) it
384 384
 		return excludeContainer
385 385
 	}
386 386
 
387
+	// Do not include container if its health doesn't match the filter
388
+	if !ctx.filters.ExactMatch("health", container.State.HealthString()) {
389
+		return excludeContainer
390
+	}
391
+
387 392
 	if ctx.filters.Include("volume") {
388 393
 		volumesByName := make(map[string]*volume.MountPoint)
389 394
 		for _, m := range container.MountPoints {
... ...
@@ -136,6 +136,7 @@ This section lists each version from latest to oldest.  Each listing includes a
136 136
 * `POST /containers/create` now takes `AutoRemove` in HostConfig, to enable auto-removal of the container on daemon side when the container's process exits.
137 137
 * `GET /containers/json` and `GET /containers/(id or name)/json` now return `"removing"` as a value for the `State.Status` field if the container is being removed. Previously, "exited" was returned as status.
138 138
 * `GET /containers/json` now accepts `removing` as a valid value for the `status` filter.
139
+* `GET /containers/json` now supports filtering containers by `health` status. 
139 140
 * `DELETE /volumes/(name)` now accepts a `force` query parameter to force removal of volumes that were already removed out of band by the volume driver plugin.
140 141
 * `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies.
141 142
 * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`).
... ...
@@ -241,7 +241,8 @@ List containers
241 241
   -   `since`=(`<container id>` or `<container name>`)
242 242
   -   `volume`=(`<volume name>` or `<mount point destination>`)
243 243
   -   `network`=(`<network id>` or `<network name>`)
244
-
244
+  -   `health`=(`starting`|`healthy`|`unhealthy`|`none`)
245
+ 
245 246
 **Status codes**:
246 247
 
247 248
 -   **200** – no error
... ...
@@ -33,6 +33,7 @@ Options:
33 33
                         - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>)
34 34
                           containers created from an image or a descendant.
35 35
                         - is-task=(true|false)
36
+                        - health=(starting|healthy|unhealthy|none)
36 37
       --format string   Pretty-print containers using a Go template
37 38
       --help            Print usage
38 39
   -n, --last int        Show n last created containers (includes all states) (default -1)
... ...
@@ -81,6 +82,7 @@ The currently supported filters are:
81 81
 * isolation (default|process|hyperv)   (Windows daemon only)
82 82
 * volume (volume name or mount point) - filters containers that mount volumes.
83 83
 * network (network id or name) - filters containers connected to the provided network
84
+* health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status
84 85
 
85 86
 #### Label
86 87
 
... ...
@@ -2,12 +2,14 @@ package main
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
-	"github.com/docker/docker/api/types"
6
-	"github.com/docker/docker/pkg/integration/checker"
7
-	"github.com/go-check/check"
5
+
8 6
 	"strconv"
9 7
 	"strings"
10 8
 	"time"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/pkg/integration/checker"
12
+	"github.com/go-check/check"
11 13
 )
12 14
 
13 15
 func waitForStatus(c *check.C, name string, prev string, expected string) {
... ...
@@ -227,6 +227,48 @@ func (s *DockerSuite) TestPsListContainersFilterStatus(c *check.C) {
227 227
 	}
228 228
 }
229 229
 
230
+func (s *DockerSuite) TestPsListContainersFilterHealth(c *check.C) {
231
+	// Test legacy no health check
232
+	out, _ := runSleepingContainer(c, "--name=none_legacy")
233
+	containerID := strings.TrimSpace(out)
234
+
235
+	waitForContainer(containerID)
236
+
237
+	out, _ = dockerCmd(c, "ps", "-q", "-l", "--no-trunc", "--filter=health=none")
238
+	containerOut := strings.TrimSpace(out)
239
+	c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected id %s, got %s for legacy none filter, output: %q", containerID, containerOut, out))
240
+
241
+	// Test no health check specified explicitly
242
+	out, _ = runSleepingContainer(c, "--name=none", "--no-healthcheck")
243
+	containerID = strings.TrimSpace(out)
244
+
245
+	waitForContainer(containerID)
246
+
247
+	out, _ = dockerCmd(c, "ps", "-q", "-l", "--no-trunc", "--filter=health=none")
248
+	containerOut = strings.TrimSpace(out)
249
+	c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected id %s, got %s for none filter, output: %q", containerID, containerOut, out))
250
+
251
+	// Test failing health check
252
+	out, _ = runSleepingContainer(c, "--name=failing_container", "--health-cmd=exit 1", "--health-interval=1s")
253
+	containerID = strings.TrimSpace(out)
254
+
255
+	waitForHealthStatus(c, "failing_container", "starting", "unhealthy")
256
+
257
+	out, _ = dockerCmd(c, "ps", "-q", "--no-trunc", "--filter=health=unhealthy")
258
+	containerOut = strings.TrimSpace(out)
259
+	c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected containerID %s, got %s for unhealthy filter, output: %q", containerID, containerOut, out))
260
+
261
+	// Check passing healthcheck
262
+	out, _ = runSleepingContainer(c, "--name=passing_container", "--health-cmd=exit 0", "--health-interval=1s")
263
+	containerID = strings.TrimSpace(out)
264
+
265
+	waitForHealthStatus(c, "passing_container", "starting", "healthy")
266
+
267
+	out, _ = dockerCmd(c, "ps", "-q", "--no-trunc", "--filter=health=healthy")
268
+	containerOut = strings.TrimSpace(out)
269
+	c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected containerID %s, got %s for healthy filter, output: %q", containerID, containerOut, out))
270
+}
271
+
230 272
 func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) {
231 273
 	// start container
232 274
 	out, _ := dockerCmd(c, "run", "-d", "busybox")
... ...
@@ -239,7 +281,6 @@ func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) {
239 239
 	out, _ = dockerCmd(c, "ps", "-a", "-q", "--filter=id="+firstID)
240 240
 	containerOut := strings.TrimSpace(out)
241 241
 	c.Assert(containerOut, checker.Equals, firstID[:12], check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID[:12], containerOut, out))
242
-
243 242
 }
244 243
 
245 244
 func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) {
... ...
@@ -38,6 +38,7 @@ the running containers.
38 38
    - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - containers created from an image or a descendant.
39 39
    - volume=(<volume-name>|<mount-point-destination>)
40 40
    - network=(<network-name>|<network-id>) - containers connected to the provided network
41
+   - health=(starting|healthy|unhealthy|none) - filters containers based on healthcheck status
41 42
 
42 43
 **--format**="*TEMPLATE*"
43 44
    Pretty-print containers using a Go template.
... ...
@@ -141,3 +142,4 @@ June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
141 141
 August 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
142 142
 November 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
143 143
 February 2015, updated by André Martins <martins@noironetworks.com>
144
+October 2016, updated by Josh Horwitz <horwitzja@gmail.com>