Signed-off-by: Josh Horwitz <horwitzja@gmail.com>
| ... | ... |
@@ -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> |