Skip to content

Commit 515e5da

Browse files
authored
Merge pull request moby#24698 from jhorwit2/jah/clist-health-filter-format
Fixes moby#24022 - Adds container health support to docker ps filter/format
2 parents 507b4e9 + 1a149a0 commit 515e5da

File tree

10 files changed

+116
-8
lines changed

10 files changed

+116
-8
lines changed

api/types/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,10 @@ type HealthcheckResult struct {
280280

281281
// Health states
282282
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
286287
)
287288

288289
// Health stores information about the container's healthcheck results

container/state.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"golang.org/x/net/context"
99

10+
"github.com/docker/docker/api/types"
1011
"github.com/docker/go-units"
1112
)
1213

@@ -78,6 +79,7 @@ func (s *State) String() string {
7879
if h := s.Health; h != nil {
7980
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)), h.String())
8081
}
82+
8183
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
8284
}
8385

@@ -100,6 +102,23 @@ func (s *State) String() string {
100102
return fmt.Sprintf("Exited (%d) %s ago", s.ExitCodeValue, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
101103
}
102104

105+
// HealthString returns a single string to describe health status.
106+
func (s *State) HealthString() string {
107+
if s.Health == nil {
108+
return types.NoHealthcheck
109+
}
110+
111+
return s.Health.String()
112+
}
113+
114+
// IsValidHealthString checks if the provided string is a valid container health status or not.
115+
func IsValidHealthString(s string) bool {
116+
return s == types.Starting ||
117+
s == types.Healthy ||
118+
s == types.Unhealthy ||
119+
s == types.NoHealthcheck
120+
}
121+
103122
// StateString returns a single string to describe state
104123
func (s *State) StateString() string {
105124
if s.Running {

container/state_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,30 @@ import (
44
"sync/atomic"
55
"testing"
66
"time"
7+
8+
"github.com/docker/docker/api/types"
79
)
810

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+
931
func TestStateRunStop(t *testing.T) {
1032
s := NewState()
1133
for i := 1; i < 3; i++ { // full lifecycle two times

daemon/list.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var acceptedPsFilterTags = map[string]bool{
3333
"label": true,
3434
"name": true,
3535
"status": true,
36+
"health": true,
3637
"since": true,
3738
"volume": true,
3839
"network": true,
@@ -261,6 +262,17 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte
261262
}
262263
}
263264

265+
err = psFilters.WalkValues("health", func(value string) error {
266+
if !container.IsValidHealthString(value) {
267+
return fmt.Errorf("Unrecognised filter value for health: %s", value)
268+
}
269+
270+
return nil
271+
})
272+
if err != nil {
273+
return nil, err
274+
}
275+
264276
var beforeContFilter, sinceContFilter *container.Container
265277

266278
err = psFilters.WalkValues("before", func(value string) error {
@@ -387,6 +399,11 @@ func includeContainerInList(container *container.Container, ctx *listContext) it
387399
return excludeContainer
388400
}
389401

402+
// Do not include container if its health doesn't match the filter
403+
if !ctx.filters.ExactMatch("health", container.State.HealthString()) {
404+
return excludeContainer
405+
}
406+
390407
if ctx.filters.Include("volume") {
391408
volumesByName := make(map[string]*volume.MountPoint)
392409
for _, m := range container.MountPoints {

docs/reference/api/docker_remote_api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ This section lists each version from latest to oldest. Each listing includes a
136136
* `POST /containers/create` now takes `AutoRemove` in HostConfig, to enable auto-removal of the container on daemon side when the container's process exits.
137137
* `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.
138138
* `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.
139140
* `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.
140141
* `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies.
141142
* `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`).

docs/reference/api/docker_remote_api_v1.25.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ List containers
241241
- `since`=(`<container id>` or `<container name>`)
242242
- `volume`=(`<volume name>` or `<mount point destination>`)
243243
- `network`=(`<network id>` or `<network name>`)
244-
244+
- `health`=(`starting`|`healthy`|`unhealthy`|`none`)
245+
245246
**Status codes**:
246247

247248
- **200** – no error

docs/reference/commandline/ps.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Options:
3333
- ancestor=(<image-name>[:tag]|<image-id>|<image@digest>)
3434
containers created from an image or a descendant.
3535
- is-task=(true|false)
36+
- health=(starting|healthy|unhealthy|none)
3637
--format string Pretty-print containers using a Go template
3738
--help Print usage
3839
-n, --last int Show n last created containers (includes all states) (default -1)
@@ -81,6 +82,7 @@ The currently supported filters are:
8182
* isolation (default|process|hyperv) (Windows daemon only)
8283
* volume (volume name or mount point) - filters containers that mount volumes.
8384
* network (network id or name) - filters containers connected to the provided network
85+
* health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status
8486

8587
#### Label
8688

integration-cli/docker_cli_health_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package main
22

33
import (
44
"encoding/json"
5-
"github.com/docker/docker/api/types"
6-
"github.com/docker/docker/pkg/integration/checker"
7-
"github.com/go-check/check"
5+
86
"strconv"
97
"strings"
108
"time"
9+
10+
"github.com/docker/docker/api/types"
11+
"github.com/docker/docker/pkg/integration/checker"
12+
"github.com/go-check/check"
1113
)
1214

1315
func waitForStatus(c *check.C, name string, prev string, expected string) {

integration-cli/docker_cli_ps_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,48 @@ func (s *DockerSuite) TestPsListContainersFilterStatus(c *check.C) {
227227
}
228228
}
229229

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+
230272
func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) {
231273
// start container
232274
out, _ := dockerCmd(c, "run", "-d", "busybox")
@@ -239,7 +281,6 @@ func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) {
239281
out, _ = dockerCmd(c, "ps", "-a", "-q", "--filter=id="+firstID)
240282
containerOut := strings.TrimSpace(out)
241283
c.Assert(containerOut, checker.Equals, firstID[:12], check.Commentf("Expected id %s, got %s for exited filter, output: %q", firstID[:12], containerOut, out))
242-
243284
}
244285

245286
func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) {

man/docker-ps.1.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ the running containers.
3838
- ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - containers created from an image or a descendant.
3939
- volume=(<volume-name>|<mount-point-destination>)
4040
- network=(<network-name>|<network-id>) - containers connected to the provided network
41+
- health=(starting|healthy|unhealthy|none) - filters containers based on healthcheck status
4142

4243
**--format**="*TEMPLATE*"
4344
Pretty-print containers using a Go template.
@@ -141,3 +142,4 @@ June 2014, updated by Sven Dowideit <[email protected]>
141142
August 2014, updated by Sven Dowideit <[email protected]>
142143
November 2014, updated by Sven Dowideit <[email protected]>
143144
February 2015, updated by André Martins <[email protected]>
145+
October 2016, updated by Josh Horwitz <[email protected]>

0 commit comments

Comments
 (0)