From 4c8f9ebc3264cb3355b1a913f6e8ffd9001341ba Mon Sep 17 00:00:00 2001 From: Mohammed Thaha Date: Mon, 6 Apr 2026 02:26:36 +0530 Subject: [PATCH 1/2] container/ps: add HealthCheck formatter field Signed-off-by: Mohammed Thaha --- cli/command/formatter/container.go | 23 ++++++++++++++++++++++ cli/command/formatter/container_test.go | 4 ++++ docs/reference/commandline/container_ls.md | 1 + 3 files changed, 28 insertions(+) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index e17313a6dc12..3c75f1b13b8d 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -28,6 +28,7 @@ const ( localVolumes = "LOCAL VOLUMES" networksHeader = "NETWORKS" platformHeader = "PLATFORM" + healthCheckHeader = "HEALTHCHECK" ) // Platform wraps a [ocispec.Platform] to implement the stringer interface. @@ -121,6 +122,7 @@ func NewContainerContext() *ContainerContext { "LocalVolumes": localVolumes, "Networks": networksHeader, "Platform": platformHeader, + "HealthCheck": healthCheckHeader, } return &containerCtx } @@ -352,6 +354,27 @@ func (c *ContainerContext) Networks() string { return strings.Join(networks, ",") } +// HealthCheck 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) HealthCheck() string { + if c.c.Health != nil && c.c.Health.Status != "" { + return string(c.c.Health.Status) + } + + // Fallback for daemons/API versions that include health only in Status text. + switch { + case strings.HasSuffix(c.c.Status, "(healthy)"): + return string(container.Healthy) + case strings.HasSuffix(c.c.Status, "(unhealthy)"): + return string(container.Unhealthy) + case strings.HasSuffix(c.c.Status, "(health: starting)"): + return string(container.Starting) + } + + 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..66c06afe9e33 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, + "HealthCheck": "", "ID": "containerID1", "Image": "ubuntu", "Labels": "", @@ -511,6 +512,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, + "HealthCheck": "", "ID": "containerID2", "Image": "ubuntu", "Labels": "", @@ -528,6 +530,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, + "HealthCheck": "", "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: "HealthCheck", 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..bc2dd75049bb 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -405,6 +405,7 @@ Valid placeholders for the Go template are listed below: | `.Ports` | Exposed ports. | | `.State` | Container status (for example; "created", "running", "exited"). | | `.Status` | Container status with details about duration and health-status. | +| `.HealthCheck`| Container health status ("starting", "healthy", "unhealthy", or "none"; empty when unavailable).| | `.Size` | Container disk size. | | `.Names` | Container names. | | `.Labels` | All labels assigned to the container. | From ef9f2b77710794bc74eaf99b611c5cc84ff122dc Mon Sep 17 00:00:00 2001 From: Mohammed Thaha Date: Sat, 9 May 2026 07:44:47 +0530 Subject: [PATCH 2/2] Address review feedback Signed-off-by: Mohammed Thaha --- cli/command/formatter/container.go | 47 +++++++++++++--------- cli/command/formatter/container_test.go | 8 ++-- docs/reference/commandline/container_ls.md | 2 +- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 3c75f1b13b8d..346ac80a08b7 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -21,14 +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" - healthCheckHeader = "HEALTHCHECK" + 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. @@ -122,7 +122,7 @@ func NewContainerContext() *ContainerContext { "LocalVolumes": localVolumes, "Networks": networksHeader, "Platform": platformHeader, - "HealthCheck": healthCheckHeader, + "HealthStatus": healthStatusHeader, } return &containerCtx } @@ -354,25 +354,32 @@ func (c *ContainerContext) Networks() string { return strings.Join(networks, ",") } -// HealthCheck returns the container's health status (for example, "healthy","unhealthy", or "starting"). +// 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) HealthCheck() string { +func (c *ContainerContext) HealthStatus() string { if c.c.Health != nil && c.c.Health.Status != "" { return string(c.c.Health.Status) } - // Fallback for daemons/API versions that include health only in Status text. - switch { - case strings.HasSuffix(c.c.Status, "(healthy)"): - return string(container.Healthy) - case strings.HasSuffix(c.c.Status, "(unhealthy)"): - return string(container.Unhealthy) - case strings.HasSuffix(c.c.Status, "(health: starting)"): - return string(container.Starting) + // 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 "" } - return "" + health = strings.TrimSuffix(health, ")") + health = strings.TrimPrefix(health, "health: ") + + parsedHealth := container.HealthStatus(health) + switch parsedHealth { + case container.Healthy, container.Unhealthy, container.Starting: + return health + default: + return "" + } } // DisplayablePorts returns formatted string representing open ports of container diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 66c06afe9e33..6a8c288beeb5 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -494,7 +494,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, - "HealthCheck": "", + "HealthStatus": "", "ID": "containerID1", "Image": "ubuntu", "Labels": "", @@ -512,7 +512,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, - "HealthCheck": "", + "HealthStatus": "", "ID": "containerID2", "Image": "ubuntu", "Labels": "", @@ -530,7 +530,7 @@ func TestContainerContextWriteJSON(t *testing.T) { { "Command": `""`, "CreatedAt": expectedCreated, - "HealthCheck": "", + "HealthStatus": "", "ID": "containerID3", "Image": "ubuntu", "Labels": "", @@ -618,7 +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: "HealthCheck", expected: ""}, + {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 bc2dd75049bb..a01a32869c08 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -405,7 +405,7 @@ Valid placeholders for the Go template are listed below: | `.Ports` | Exposed ports. | | `.State` | Container status (for example; "created", "running", "exited"). | | `.Status` | Container status with details about duration and health-status. | -| `.HealthCheck`| Container health status ("starting", "healthy", "unhealthy", or "none"; empty when unavailable).| +| `.HealthStatus` | Container health status ("starting", "healthy", "unhealthy"; empty when unavailable). | | `.Size` | Container disk size. | | `.Names` | Container names. | | `.Labels` | All labels assigned to the container. |