Repository: docker/compose-cli
Pull request #1675 (2021-05-17 07:06:56)

ps shows healthcheck only for running container
**What I did**
updated ps output so
- it does not show healthcheck status but for running container
- it includes exit code for exited containers
- it sort ports for predictable output

**Related issue**
close https://github.com/docker/compose-cli/issues/1671
close https://github.com/docker/compose-cli/issues/1672




Patch:
diff --git a/api/compose/api.go b/api/compose/api.go
index 63ab1271..fca9ba51 100644
--- a/api/compose/api.go
+++ b/api/compose/api.go
@@ -284,6 +284,7 @@ type ContainerSummary struct {
 	Service    string
 	State      string
 	Health     string
+	ExitCode   int
 	Publishers []PortPublisher
 }
 
diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go
index c67ea580..1379bce2 100644
--- a/cli/cmd/compose/ps.go
+++ b/cli/cmd/compose/ps.go
@@ -103,8 +103,10 @@ func runPs(ctx context.Context, backend compose.Service, services []string, opts
 					}
 				}
 				status := container.State
-				if container.Health != "" {
+				if status == "running" && container.Health != "" {
 					status = fmt.Sprintf("%s (%s)", container.State, container.Health)
+				} else if status == "exited" || status == "dead" {
+					status = fmt.Sprintf("%s (%d)", container.State, container.ExitCode)
 				}
 				_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", container.Name, container.Service, status, strings.Join(ports, ", "))
 			}
diff --git a/local/compose/ps.go b/local/compose/ps.go
index 99cd6c13..95b0f4dd 100644
--- a/local/compose/ps.go
+++ b/local/compose/ps.go
@@ -19,10 +19,10 @@ package compose
 import (
 	"context"
 	"fmt"
-
-	"golang.org/x/sync/errgroup"
+	"sort"
 
 	"github.com/docker/compose-cli/api/compose"
+	"golang.org/x/sync/errgroup"
 )
 
 func (s *composeService) Ps(ctx context.Context, projectName string, options compose.PsOptions) ([]compose.ContainerSummary, error) {
@@ -42,6 +42,9 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options com
 		i := i
 		eg.Go(func() error {
 			var publishers []compose.PortPublisher
+			sort.Slice(container.Ports, func(i, j int) bool {
+				return container.Ports[i].PrivatePort < container.Ports[j].PrivatePort
+			})
 			for _, p := range container.Ports {
 				var url string
 				if p.PublicPort != 0 {
@@ -60,9 +63,19 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options com
 				return err
 			}
 
-			var health string
-			if inspect.State != nil && inspect.State.Health != nil {
-				health = inspect.State.Health.Status
+			var (
+				health   string
+				exitCode int
+			)
+			if inspect.State != nil {
+				switch inspect.State.Status {
+				case "running":
+					if inspect.State.Health != nil {
+						health = inspect.State.Health.Status
+					}
+				case "exited", "dead":
+					exitCode = inspect.State.ExitCode
+				}
 			}
 
 			summary[i] = compose.ContainerSummary{
@@ -72,6 +85,7 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options com
 				Service:    container.Labels[serviceLabel],
 				State:      container.State,
 				Health:     health,
+				ExitCode:   exitCode,
 				Publishers: publishers,
 			}
 			return nil


Test patch:
diff --git a/local/compose/ps_test.go b/local/compose/ps_test.go
index 3ea5faac..c21de7b5 100644
--- a/local/compose/ps_test.go
+++ b/local/compose/ps_test.go
@@ -40,10 +40,10 @@ func TestPs(t *testing.T) {
 	args := filters.NewArgs(projectFilter(testProject))
 	args.Add("label", "com.docker.compose.oneoff=False")
 	listOpts := apitypes.ContainerListOptions{Filters: args, All: true}
-	c1, inspect1 := containerDetails("service1", "123", "Running", "healthy")
-	c2, inspect2 := containerDetails("service1", "456", "Running", "")
+	c1, inspect1 := containerDetails("service1", "123", "running", "healthy", 0)
+	c2, inspect2 := containerDetails("service1", "456", "running", "", 0)
 	c2.Ports = []apitypes.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
-	c3, inspect3 := containerDetails("service2", "789", "Running", "")
+	c3, inspect3 := containerDetails("service2", "789", "exited", "", 130)
 	api.EXPECT().ContainerList(ctx, listOpts).Return([]apitypes.Container{c1, c2, c3}, nil)
 	api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
 	api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
@@ -52,45 +52,21 @@ func TestPs(t *testing.T) {
 	containers, err := tested.Ps(ctx, testProject, compose.PsOptions{})
 
 	expected := []compose.ContainerSummary{
-		{ID: "123", Name: "123", Project: testProject, Service: "service1", State: "Running", Health: "healthy", Publishers: nil},
-		{ID: "456", Name: "456", Project: testProject, Service: "service1", State: "Running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost:80", TargetPort: 90, PublishedPort: 80}}},
-		{ID: "789", Name: "789", Project: testProject, Service: "service2", State: "Running", Health: "", Publishers: nil},
+		{ID: "123", Name: "123", Project: testProject, Service: "service1", State: "running", Health: "healthy", Publishers: nil},
+		{ID: "456", Name: "456", Project: testProject, Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost:80", TargetPort: 90, PublishedPort: 80}}},
+		{ID: "789", Name: "789", Project: testProject, Service: "service2", State: "exited", Health: "", ExitCode: 130, Publishers: nil},
 	}
 	assert.NilError(t, err)
 	assert.DeepEqual(t, containers, expected)
 }
 
-func TestPsAll(t *testing.T) {
-	mockCtrl := gomock.NewController(t)
-	defer mockCtrl.Finish()
-	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
-
-	ctx := context.Background()
-	listOpts := apitypes.ContainerListOptions{Filters: filters.NewArgs(projectFilter(testProject)), All: true}
-	c1, inspect1 := containerDetails("service1", "123", "Running", "healthy")
-	c2, inspect2 := containerDetails("service1", "456", "Stopped", "")
-	api.EXPECT().ContainerList(ctx, listOpts).Return([]apitypes.Container{c1, c2}, nil)
-	api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
-	api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
-
-	containers, err := tested.Ps(ctx, testProject, compose.PsOptions{All: true})
-
-	expected := []compose.ContainerSummary{
-		{ID: "123", Name: "123", Project: testProject, Service: "service1", State: "Running", Health: "healthy", Publishers: nil},
-		{ID: "456", Name: "456", Project: testProject, Service: "service1", State: "Stopped", Health: "", Publishers: nil},
-	}
-	assert.NilError(t, err)
-	assert.DeepEqual(t, containers, expected)
-}
-
-func containerDetails(service string, id string, status string, health string) (apitypes.Container, apitypes.ContainerJSON) {
+func containerDetails(service string, id string, status string, health string, exitCode int) (apitypes.Container, apitypes.ContainerJSON) {
 	container := apitypes.Container{
 		ID:     id,
 		Names:  []string{"/" + id},
 		Labels: containerLabels(service),
 		State:  status,
 	}
-	inspect := apitypes.ContainerJSON{ContainerJSONBase: &apitypes.ContainerJSONBase{State: &apitypes.ContainerState{Status: status, Health: &apitypes.Health{Status: health}}}}
+	inspect := apitypes.ContainerJSON{ContainerJSONBase: &apitypes.ContainerJSONBase{State: &apitypes.ContainerState{Status: status, Health: &apitypes.Health{Status: health}, ExitCode: exitCode}}}
 	return container, inspect
 }