Просмотр исходного кода

Merge pull request #24698 from jhorwit2/jah/clist-health-filter-format

Fixes #24022 - Adds container health support to docker ps filter/format
Vincent Demeester 8 лет назад
Родитель
Сommit
515e5dade7

+ 4 - 3
api/types/types.go

@@ -280,9 +280,10 @@ type HealthcheckResult struct {
 
 
 // Health states
 // Health states
 const (
 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
 // Health stores information about the container's healthcheck results

+ 19 - 0
container/state.go

@@ -7,6 +7,7 @@ import (
 
 
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/go-units"
 	"github.com/docker/go-units"
 )
 )
 
 
@@ -78,6 +79,7 @@ func (s *State) String() string {
 		if h := s.Health; h != nil {
 		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 (%s)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)), h.String())
 		}
 		}
+
 		return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
 		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)))
 	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
 // StateString returns a single string to describe state
 func (s *State) StateString() string {
 func (s *State) StateString() string {
 	if s.Running {
 	if s.Running {

+ 22 - 0
container/state_test.go

@@ -4,8 +4,30 @@ import (
 	"sync/atomic"
 	"sync/atomic"
 	"testing"
 	"testing"
 	"time"
 	"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) {
 func TestStateRunStop(t *testing.T) {
 	s := NewState()
 	s := NewState()
 	for i := 1; i < 3; i++ { // full lifecycle two times
 	for i := 1; i < 3; i++ { // full lifecycle two times

+ 17 - 0
daemon/list.go

@@ -33,6 +33,7 @@ var acceptedPsFilterTags = map[string]bool{
 	"label":     true,
 	"label":     true,
 	"name":      true,
 	"name":      true,
 	"status":    true,
 	"status":    true,
+	"health":    true,
 	"since":     true,
 	"since":     true,
 	"volume":    true,
 	"volume":    true,
 	"network":   true,
 	"network":   true,
@@ -261,6 +262,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
 	var beforeContFilter, sinceContFilter *container.Container
 
 
 	err = psFilters.WalkValues("before", func(value string) error {
 	err = psFilters.WalkValues("before", func(value string) error {
@@ -387,6 +399,11 @@ func includeContainerInList(container *container.Container, ctx *listContext) it
 		return excludeContainer
 		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") {
 	if ctx.filters.Include("volume") {
 		volumesByName := make(map[string]*volume.MountPoint)
 		volumesByName := make(map[string]*volume.MountPoint)
 		for _, m := range container.MountPoints {
 		for _, m := range container.MountPoints {

+ 1 - 0
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.
 * `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` 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 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.
 * `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/` 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`).
 * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`).

+ 2 - 1
docs/reference/api/docker_remote_api_v1.25.md

@@ -241,7 +241,8 @@ List containers
   -   `since`=(`<container id>` or `<container name>`)
   -   `since`=(`<container id>` or `<container name>`)
   -   `volume`=(`<volume name>` or `<mount point destination>`)
   -   `volume`=(`<volume name>` or `<mount point destination>`)
   -   `network`=(`<network id>` or `<network name>`)
   -   `network`=(`<network id>` or `<network name>`)
-
+  -   `health`=(`starting`|`healthy`|`unhealthy`|`none`)
+ 
 **Status codes**:
 **Status codes**:
 
 
 -   **200** – no error
 -   **200** – no error

+ 2 - 0
docs/reference/commandline/ps.md

@@ -33,6 +33,7 @@ Options:
                         - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>)
                         - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>)
                           containers created from an image or a descendant.
                           containers created from an image or a descendant.
                         - is-task=(true|false)
                         - is-task=(true|false)
+                        - health=(starting|healthy|unhealthy|none)
       --format string   Pretty-print containers using a Go template
       --format string   Pretty-print containers using a Go template
       --help            Print usage
       --help            Print usage
   -n, --last int        Show n last created containers (includes all states) (default -1)
   -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)
 * isolation (default|process|hyperv)   (Windows daemon only)
 * volume (volume name or mount point) - filters containers that mount volumes.
 * volume (volume name or mount point) - filters containers that mount volumes.
 * network (network id or name) - filters containers connected to the provided network
 * network (network id or name) - filters containers connected to the provided network
+* health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status
 
 
 #### Label
 #### Label
 
 

+ 5 - 3
integration-cli/docker_cli_health_test.go

@@ -2,12 +2,14 @@ package main
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/pkg/integration/checker"
-	"github.com/go-check/check"
+
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"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) {
 func waitForStatus(c *check.C, name string, prev string, expected string) {

+ 42 - 1
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) {
 func (s *DockerSuite) TestPsListContainersFilterID(c *check.C) {
 	// start container
 	// start container
 	out, _ := dockerCmd(c, "run", "-d", "busybox")
 	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)
 	out, _ = dockerCmd(c, "ps", "-a", "-q", "--filter=id="+firstID)
 	containerOut := strings.TrimSpace(out)
 	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))
 	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) {
 func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) {

+ 2 - 0
man/docker-ps.1.md

@@ -38,6 +38,7 @@ the running containers.
    - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - containers created from an image or a descendant.
    - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - containers created from an image or a descendant.
    - volume=(<volume-name>|<mount-point-destination>)
    - volume=(<volume-name>|<mount-point-destination>)
    - network=(<network-name>|<network-id>) - containers connected to the provided network
    - network=(<network-name>|<network-id>) - containers connected to the provided network
+   - health=(starting|healthy|unhealthy|none) - filters containers based on healthcheck status
 
 
 **--format**="*TEMPLATE*"
 **--format**="*TEMPLATE*"
    Pretty-print containers using a Go template.
    Pretty-print containers using a Go template.
@@ -141,3 +142,4 @@ June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 August 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 August 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 November 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 November 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 February 2015, updated by André Martins <martins@noironetworks.com>
 February 2015, updated by André Martins <martins@noironetworks.com>
+October 2016, updated by Josh Horwitz <horwitzja@gmail.com>