From 1a149a0ea59b6653e0ba14599476bfe19c4c33f3 Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Fri, 15 Jul 2016 14:21:19 -0400 Subject: [PATCH] Adds container health support to docker ps filter Signed-off-by: Josh Horwitz --- api/types/types.go | 7 +-- container/state.go | 19 ++++++++ container/state_test.go | 22 ++++++++++ daemon/list.go | 17 ++++++++ docs/reference/api/docker_remote_api.md | 1 + docs/reference/api/docker_remote_api_v1.25.md | 3 +- docs/reference/commandline/ps.md | 2 + integration-cli/docker_cli_health_test.go | 8 ++-- integration-cli/docker_cli_ps_test.go | 43 ++++++++++++++++++- man/docker-ps.1.md | 2 + 10 files changed, 116 insertions(+), 8 deletions(-) diff --git a/api/types/types.go b/api/types/types.go index fee9e3cb65..0477592e19 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -280,9 +280,10 @@ type HealthcheckResult struct { // Health states const ( - Starting = "starting" // Starting indicates that the container is not yet ready - Healthy = "healthy" // Healthy indicates that the container is running correctly - Unhealthy = "unhealthy" // Unhealthy indicates that the container has a problem + NoHealthcheck = "none" // Indicates there is no healthcheck + Starting = "starting" // Starting indicates that the container is not yet ready + Healthy = "healthy" // Healthy indicates that the container is running correctly + Unhealthy = "unhealthy" // Unhealthy indicates that the container has a problem ) // Health stores information about the container's healthcheck results diff --git a/container/state.go b/container/state.go index 2628fc5b43..4dd2ecec69 100644 --- a/container/state.go +++ b/container/state.go @@ -7,6 +7,7 @@ import ( "golang.org/x/net/context" + "github.com/docker/docker/api/types" "github.com/docker/go-units" ) @@ -78,6 +79,7 @@ func (s *State) String() string { if h := s.Health; h != nil { return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)), h.String()) } + return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt))) } @@ -100,6 +102,23 @@ func (s *State) String() string { return fmt.Sprintf("Exited (%d) %s ago", s.ExitCodeValue, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt))) } +// HealthString returns a single string to describe health status. +func (s *State) HealthString() string { + if s.Health == nil { + return types.NoHealthcheck + } + + return s.Health.String() +} + +// IsValidHealthString checks if the provided string is a valid container health status or not. +func IsValidHealthString(s string) bool { + return s == types.Starting || + s == types.Healthy || + s == types.Unhealthy || + s == types.NoHealthcheck +} + // StateString returns a single string to describe state func (s *State) StateString() string { if s.Running { diff --git a/container/state_test.go b/container/state_test.go index 2e15bd7f41..c9a7bb4b7b 100644 --- a/container/state_test.go +++ b/container/state_test.go @@ -4,8 +4,30 @@ import ( "sync/atomic" "testing" "time" + + "github.com/docker/docker/api/types" ) +func TestIsValidHealthString(t *testing.T) { + contexts := []struct { + Health string + Expected bool + }{ + {types.Healthy, true}, + {types.Unhealthy, true}, + {types.Starting, true}, + {types.NoHealthcheck, true}, + {"fail", false}, + } + + for _, c := range contexts { + v := IsValidHealthString(c.Health) + if v != c.Expected { + t.Fatalf("Expected %t, but got %t", c.Expected, v) + } + } +} + func TestStateRunStop(t *testing.T) { s := NewState() for i := 1; i < 3; i++ { // full lifecycle two times diff --git a/daemon/list.go b/daemon/list.go index 9d9f1d5e3a..94e6dc6c59 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -33,6 +33,7 @@ var acceptedPsFilterTags = map[string]bool{ "label": true, "name": true, "status": true, + "health": true, "since": true, "volume": true, "network": true, @@ -258,6 +259,17 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte } } + err = psFilters.WalkValues("health", func(value string) error { + if !container.IsValidHealthString(value) { + return fmt.Errorf("Unrecognised filter value for health: %s", value) + } + + return nil + }) + if err != nil { + return nil, err + } + var beforeContFilter, sinceContFilter *container.Container err = psFilters.WalkValues("before", func(value string) error { @@ -384,6 +396,11 @@ func includeContainerInList(container *container.Container, ctx *listContext) it return excludeContainer } + // Do not include container if its health doesn't match the filter + if !ctx.filters.ExactMatch("health", container.State.HealthString()) { + return excludeContainer + } + if ctx.filters.Include("volume") { volumesByName := make(map[string]*volume.MountPoint) for _, m := range container.MountPoints { diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 9acc56e0c6..87d4061790 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -136,6 +136,7 @@ This section lists each version from latest to oldest. Each listing includes a * `POST /containers/create` now takes `AutoRemove` in HostConfig, to enable auto-removal of the container on daemon side when the container's process exits. * `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. * `GET /containers/json` now accepts `removing` as a valid value for the `status` filter. +* `GET /containers/json` now supports filtering containers by `health` status. * `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. * `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies. * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`). diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index b3483d53a3..430726a5d1 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -241,7 +241,8 @@ List containers - `since`=(`` or ``) - `volume`=(`` or ``) - `network`=(`` or ``) - + - `health`=(`starting`|`healthy`|`unhealthy`|`none`) + **Status codes**: - **200** – no error diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md index c48b1ff1f9..94dc3048c2 100644 --- a/docs/reference/commandline/ps.md +++ b/docs/reference/commandline/ps.md @@ -33,6 +33,7 @@ Options: - ancestor=([:tag]||) containers created from an image or a descendant. - is-task=(true|false) + - health=(starting|healthy|unhealthy|none) --format string Pretty-print containers using a Go template --help Print usage -n, --last int Show n last created containers (includes all states) (default -1) @@ -81,6 +82,7 @@ The currently supported filters are: * isolation (default|process|hyperv) (Windows daemon only) * volume (volume name or mount point) - filters containers that mount volumes. * network (network id or name) - filters containers connected to the provided network +* health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status #### Label diff --git a/integration-cli/docker_cli_health_test.go b/integration-cli/docker_cli_health_test.go index 5cfbfdf32b..6b7baebd00 100644 --- a/integration-cli/docker_cli_health_test.go +++ b/integration-cli/docker_cli_health_test.go @@ -2,12 +2,14 @@ package main import ( "encoding/json" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/integration/checker" - "github.com/go-check/check" + "strconv" "strings" "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/integration/checker" + "github.com/go-check/check" ) func waitForStatus(c *check.C, name string, prev string, expected string) { diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index 33e9f51732..f3bdb39d00 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -227,6 +227,48 @@ func (s *DockerSuite) TestPsListContainersFilterStatus(c *check.C) { } } +func (s *DockerSuite) TestPsListContainersFilterHealth(c *check.C) { + // Test legacy no health check + out, _ := runSleepingContainer(c, "--name=none_legacy") + containerID := strings.TrimSpace(out) + + waitForContainer(containerID) + + out, _ = dockerCmd(c, "ps", "-q", "-l", "--no-trunc", "--filter=health=none") + containerOut := strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected id %s, got %s for legacy none filter, output: %q", containerID, containerOut, out)) + + // Test no health check specified explicitly + out, _ = runSleepingContainer(c, "--name=none", "--no-healthcheck") + containerID = strings.TrimSpace(out) + + waitForContainer(containerID) + + out, _ = dockerCmd(c, "ps", "-q", "-l", "--no-trunc", "--filter=health=none") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected id %s, got %s for none filter, output: %q", containerID, containerOut, out)) + + // Test failing health check + out, _ = runSleepingContainer(c, "--name=failing_container", "--health-cmd=exit 1", "--health-interval=1s") + containerID = strings.TrimSpace(out) + + waitForHealthStatus(c, "failing_container", "starting", "unhealthy") + + out, _ = dockerCmd(c, "ps", "-q", "--no-trunc", "--filter=health=unhealthy") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected containerID %s, got %s for unhealthy filter, output: %q", containerID, containerOut, out)) + + // Check passing healthcheck + out, _ = runSleepingContainer(c, "--name=passing_container", "--health-cmd=exit 0", "--health-interval=1s") + containerID = strings.TrimSpace(out) + + waitForHealthStatus(c, "passing_container", "starting", "healthy") + + out, _ = dockerCmd(c, "ps", "-q", "--no-trunc", "--filter=health=healthy") + containerOut = strings.TrimSpace(out) + c.Assert(containerOut, checker.Equals, containerID, check.Commentf("Expected containerID %s, got %s for healthy filter, output: %q", containerID, containerOut, out)) +} + func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) { // start container out, _ := dockerCmd(c, "run", "-d", "busybox") @@ -239,7 +281,6 @@ func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) { out, _ = dockerCmd(c, "ps", "-a", "-q", "--filter=id="+firstID) containerOut := strings.TrimSpace(out) c.Assert(containerOut, checker.Equals, firstID[:12], check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID[:12], containerOut, out)) - } func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) { diff --git a/man/docker-ps.1.md b/man/docker-ps.1.md index fd6c4e78fa..d9aa39f8fd 100644 --- a/man/docker-ps.1.md +++ b/man/docker-ps.1.md @@ -38,6 +38,7 @@ the running containers. - ancestor=([:tag]||) - containers created from an image or a descendant. - volume=(|) - network=(|) - containers connected to the provided network + - health=(starting|healthy|unhealthy|none) - filters containers based on healthcheck status **--format**="*TEMPLATE*" Pretty-print containers using a Go template. @@ -141,3 +142,4 @@ June 2014, updated by Sven Dowideit August 2014, updated by Sven Dowideit November 2014, updated by Sven Dowideit February 2015, updated by André Martins +October 2016, updated by Josh Horwitz