Signed-off-by: Josh Horwitz <horwitzja@gmail.com>
Josh Horwitz authored on 2016/07/16 03:21:19... | ... |
@@ -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> |