Skip to content

Commit 2649a76

Browse files
committed
feat(ps): show container health status in STATUS column
`docker ps` displays the health check results for healthchecked containers in the STATUS column: ```bash $ docker run -d --name health --health-cmd=true --health-interval=3s alpine sleep inf ace14dd125355ac04e8cbad9f1cf2eaaa7209d76cb648ccefb81e45b986c58f7 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ace14dd12535 alpine "sleep inf" 23 seconds ago Up 22 seconds (healthy) health ``` However, `nerdctl ps` doesn't show the health status. Therefore, to ensure compatibility with Docker, this change makes `nerdctl ps` display the health check results in the STATUS column for containers that perform health checks. After this change, the output looks like: ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 02c64243528a docker.io/library/alpine:latest "sleep inf" 7 seconds ago Up (health: starting) hoge ``` ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 690aa502978c docker.io/library/alpine:latest "sleep inf" 1 second ago Up (healthy) hoge ``` ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 02c64243528a docker.io/library/alpine:latest "sleep inf" 2 hours ago Up (unhealthy) hoge ``` Signed-off-by: Hayato Kiwata <dev@haytok.jp>
1 parent ad00cb4 commit 2649a76

2 files changed

Lines changed: 131 additions & 0 deletions

File tree

cmd/nerdctl/container/container_list_linux_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ import (
2626

2727
"gotest.tools/v3/assert"
2828

29+
"github.com/containerd/nerdctl/mod/tigron/expect"
30+
"github.com/containerd/nerdctl/mod/tigron/require"
2931
"github.com/containerd/nerdctl/mod/tigron/test"
3032
"github.com/containerd/nerdctl/mod/tigron/tig"
3133

3234
"github.com/containerd/nerdctl/v2/pkg/formatter"
35+
"github.com/containerd/nerdctl/v2/pkg/healthcheck"
3336
"github.com/containerd/nerdctl/v2/pkg/strutil"
3437
"github.com/containerd/nerdctl/v2/pkg/tabutil"
3538
"github.com/containerd/nerdctl/v2/pkg/testutil"
@@ -699,3 +702,117 @@ func TestContainerListStatusFilter(t *testing.T) {
699702

700703
testCase.Run(t)
701704
}
705+
706+
func TestContainerListWithHealthStatus(t *testing.T) {
707+
testCase := nerdtest.Setup()
708+
709+
testCase.Require = require.All(
710+
require.Not(nerdtest.Docker),
711+
require.Not(nerdtest.Rootless),
712+
)
713+
714+
testCase.SubTests = []*test.Case{
715+
{
716+
Description: "ps shows healthy status after a successful probe",
717+
Setup: func(data test.Data, helpers test.Helpers) {
718+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
719+
"--health-cmd", "true", "--health-interval", "3s",
720+
testutil.CommonImage, "sleep", nerdtest.Infinity,
721+
)
722+
helpers.Ensure("container", "healthcheck", data.Identifier())
723+
},
724+
Cleanup: func(data test.Data, helpers test.Helpers) {
725+
helpers.Anyhow("rm", "-f", data.Identifier())
726+
},
727+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
728+
return helpers.Command("ps", "--filter", "name="+data.Identifier())
729+
},
730+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
731+
return &test.Expected{
732+
ExitCode: expect.ExitCodeSuccess,
733+
Output: expect.Contains(fmt.Sprintf("(%s)", healthcheck.Healthy)),
734+
}
735+
},
736+
},
737+
{
738+
Description: "ps shows starting status",
739+
Setup: func(data test.Data, helpers test.Helpers) {
740+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
741+
"--health-cmd", "exit 1",
742+
"--health-interval", "1s",
743+
"--health-start-period", "60s",
744+
"--health-retries", "2",
745+
testutil.CommonImage, "sleep", nerdtest.Infinity)
746+
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
747+
helpers.Ensure("container", "healthcheck", data.Identifier())
748+
},
749+
Cleanup: func(data test.Data, helpers test.Helpers) {
750+
helpers.Anyhow("rm", "-f", data.Identifier())
751+
},
752+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
753+
return helpers.Command("ps", "--filter", "name="+data.Identifier())
754+
},
755+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
756+
return &test.Expected{
757+
ExitCode: expect.ExitCodeSuccess,
758+
Output: expect.Contains(fmt.Sprintf("(health: %s)", healthcheck.Starting)),
759+
}
760+
},
761+
},
762+
{
763+
Description: "ps shows unhealthy status",
764+
Setup: func(data test.Data, helpers test.Helpers) {
765+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
766+
"--health-cmd", "not-a-real-cmd",
767+
"--health-interval", "1s",
768+
"--health-retries", "1",
769+
testutil.CommonImage, "sleep", nerdtest.Infinity)
770+
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
771+
helpers.Ensure("container", "healthcheck", data.Identifier())
772+
},
773+
Cleanup: func(data test.Data, helpers test.Helpers) {
774+
helpers.Anyhow("rm", "-f", data.Identifier())
775+
},
776+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
777+
return helpers.Command("ps", "--filter", "name="+data.Identifier())
778+
},
779+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
780+
return &test.Expected{
781+
ExitCode: expect.ExitCodeSuccess,
782+
Output: expect.Contains(fmt.Sprintf("(%s)", healthcheck.Unhealthy)),
783+
}
784+
},
785+
},
786+
{
787+
Description: "ps does not show health suffix for stopped containers",
788+
Setup: func(data test.Data, helpers test.Helpers) {
789+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
790+
"--health-cmd", "true",
791+
testutil.CommonImage, "sleep", nerdtest.Infinity)
792+
helpers.Ensure("container", "healthcheck", data.Identifier())
793+
helpers.Ensure("stop", data.Identifier())
794+
},
795+
Cleanup: func(data test.Data, helpers test.Helpers) {
796+
helpers.Anyhow("rm", "-f", data.Identifier())
797+
},
798+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
799+
return helpers.Command("ps", "-a", "--filter", "name="+data.Identifier())
800+
},
801+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
802+
return &test.Expected{
803+
ExitCode: expect.ExitCodeSuccess,
804+
Output: expect.All(
805+
expect.Contains("Exited"),
806+
expect.DoesNotContain(
807+
"(health:",
808+
fmt.Sprintf("(%s)", healthcheck.Healthy),
809+
fmt.Sprintf("(%s)", healthcheck.Unhealthy),
810+
),
811+
),
812+
}
813+
},
814+
},
815+
}
816+
817+
testCase.Run(t)
818+
}

pkg/cmd/container/list.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/containerd/nerdctl/v2/pkg/containerdutil"
3737
"github.com/containerd/nerdctl/v2/pkg/containerutil"
3838
"github.com/containerd/nerdctl/v2/pkg/formatter"
39+
"github.com/containerd/nerdctl/v2/pkg/healthcheck"
3940
"github.com/containerd/nerdctl/v2/pkg/imgutil"
4041
"github.com/containerd/nerdctl/v2/pkg/labels"
4142
"github.com/containerd/nerdctl/v2/pkg/portutil"
@@ -161,6 +162,19 @@ func prepareContainers(ctx context.Context, client *containerd.Client, container
161162
var status string
162163
if s, ok := statusPerContainer[c.ID()]; ok {
163164
status = s
165+
if strings.HasPrefix(status, "Up") && info.Labels[labels.HealthState] != "" {
166+
healthState, err := healthcheck.HealthStateFromJSON(info.Labels[labels.HealthState])
167+
if err != nil {
168+
log.G(ctx).WithError(err).Debugf("failed to parse health state for container %s", c.ID())
169+
} else {
170+
switch healthState.Status {
171+
case healthcheck.Healthy, healthcheck.Unhealthy:
172+
status = fmt.Sprintf("%s (%s)", status, healthState.Status)
173+
case healthcheck.Starting:
174+
status = fmt.Sprintf("%s (health: %s)", status, healthState.Status)
175+
}
176+
}
177+
}
164178
} else {
165179
return nil, fmt.Errorf("can't get container %s status", c.ID())
166180
}

0 commit comments

Comments
 (0)