diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index e17313a6dc12..410b5cdce51e 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -21,13 +21,14 @@ import ( const ( defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" - namesHeader = "NAMES" - commandHeader = "COMMAND" - runningForHeader = "CREATED" - mountsHeader = "MOUNTS" - localVolumes = "LOCAL VOLUMES" - networksHeader = "NETWORKS" - platformHeader = "PLATFORM" + namesHeader = "NAMES" + commandHeader = "COMMAND" + runningForHeader = "CREATED" + mountsHeader = "MOUNTS" + localVolumes = "LOCAL VOLUMES" + networksHeader = "NETWORKS" + platformHeader = "PLATFORM" + healthStatusHeader = "HEALTH STATUS" ) // Platform wraps a [ocispec.Platform] to implement the stringer interface. @@ -121,6 +122,7 @@ func NewContainerContext() *ContainerContext { "LocalVolumes": localVolumes, "Networks": networksHeader, "Platform": platformHeader, + "HealthStatus": healthStatusHeader, } return &containerCtx } @@ -352,6 +354,35 @@ func (c *ContainerContext) Networks() string { return strings.Join(networks, ",") } +// HealthStatus returns the container's health status (for example, "healthy","unhealthy", or "starting"). +// If no healthcheck is configured, an empty +// string is returned. +func (c *ContainerContext) HealthStatus() string { + if c.c.Health != nil && c.c.Health.Status != "" { + return string(c.c.Health.Status) + } + + // Fallback for API versions before v1.52, which include health only in Status text; + // see https://github.com/moby/moby/pull/50281 + // see https://github.com/moby/moby/blob/docker-v29.4.3/daemon/container/health.go#L18-L43 + _, health, ok := strings.Cut(c.c.Status, "(") + if !ok || !strings.HasSuffix(health, ")") { + return "" + } + + health = strings.TrimSuffix(health, ")") + health = strings.TrimPrefix(health, "health: ") + + switch container.HealthStatus(health) { + case container.Healthy, container.Unhealthy, container.Starting: + return health + case container.NoHealthcheck: + return "" + default: + return "" + } +} + // DisplayablePorts returns formatted string representing open ports of container // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" // it's used by command 'docker ps' diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index fe7b57dd7f52..1c68aeaea059 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -494,6 +494,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, + "HealthStatus": "", "ID": "containerID1", "Image": "ubuntu", "Labels": "", @@ -511,6 +512,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, + "HealthStatus": "", "ID": "containerID2", "Image": "ubuntu", "Labels": "", @@ -528,6 +530,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, + "HealthStatus": "", "ID": "containerID3", "Image": "ubuntu", "Labels": "", @@ -615,6 +618,7 @@ func TestContainerBackCompat(t *testing.T) { {field: "Image", expected: "docker.io/library/ubuntu"}, {field: "Command", expected: `"/bin/sh"`}, {field: "CreatedAt", expected: time.Unix(createdAtTime.Unix(), 0).String()}, + {field: "HealthStatus", expected: ""}, {field: "RunningFor", expected: "12 months ago"}, {field: "Ports", expected: "8080/tcp"}, {field: "Status", expected: "running"}, diff --git a/docs/reference/commandline/container_ls.md b/docs/reference/commandline/container_ls.md index a19f7a5e0410..5b2481fe13e1 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -395,22 +395,23 @@ template. Valid placeholders for the Go template are listed below: -| Placeholder | Description | -|:--------------|:------------------------------------------------------------------------------------------------| -| `.ID` | Container ID | -| `.Image` | Image ID | -| `.Command` | Quoted command | -| `.CreatedAt` | Time when the container was created. | -| `.RunningFor` | Elapsed time since the container was started. | -| `.Ports` | Exposed ports. | -| `.State` | Container status (for example; "created", "running", "exited"). | -| `.Status` | Container status with details about duration and health-status. | -| `.Size` | Container disk size. | -| `.Names` | Container names. | -| `.Labels` | All labels assigned to the container. | -| `.Label` | Value of a specific label for this container. For example `'{{.Label "com.docker.swarm.cpu"}}'` | -| `.Mounts` | Names of the volumes mounted in this container. | -| `.Networks` | Names of the networks attached to this container. | +| Placeholder | Description | +|:----------------|:------------------------------------------------------------------------------------------------| +| `.ID` | Container ID | +| `.Image` | Image ID | +| `.Command` | Quoted command | +| `.CreatedAt` | Time when the container was created. | +| `.RunningFor` | Elapsed time since the container was started. | +| `.Ports` | Exposed ports. | +| `.State` | Container status (for example; "created", "running", "exited"). | +| `.Status` | Container status with details about duration and health-status. | +| `.HealthStatus` | Container health status ("starting", "healthy", "unhealthy"; empty when unavailable). | +| `.Size` | Container disk size. | +| `.Names` | Container names. | +| `.Labels` | All labels assigned to the container. | +| `.Label` | Value of a specific label for this container. For example `'{{.Label "com.docker.swarm.cpu"}}'` | +| `.Mounts` | Names of the volumes mounted in this container. | +| `.Networks` | Names of the networks attached to this container. | When using the `--format` option, the `ps` command will either output the data exactly as the template declares or, when using the `table` directive, includes