Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

align compose ps output with docker ps #10065

Merged
merged 2 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions cmd/compose/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import (

type imageOptions struct {
*projectOptions
Quiet bool
Quiet bool
Format string
}

func imagesCommand(p *projectOptions, backend api.Service) *cobra.Command {
Expand All @@ -50,6 +51,7 @@ func imagesCommand(p *projectOptions, backend api.Service) *cobra.Command {
}),
ValidArgsFunction: completeServiceNames(p),
}
imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json].")
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return imgCmd
}
Expand Down Expand Up @@ -88,7 +90,7 @@ func runImages(ctx context.Context, backend api.Service, opts imageOptions, serv
return images[i].ContainerName < images[j].ContainerName
})

return formatter.Print(images, formatter.PRETTY, os.Stdout,
return formatter.Print(images, opts.Format, os.Stdout,
func(w io.Writer) {
for _, img := range images {
id := stringid.TruncateID(img.ID)
Expand All @@ -104,5 +106,5 @@ func runImages(ctx context.Context, backend api.Service, opts imageOptions, serv
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, repo, tag, id, size)
}
},
"Container", "Repository", "Tag", "Image Id", "Size")
"CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
}
2 changes: 1 addition & 1 deletion cmd/compose/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func listCommand(backend api.Service) *cobra.Command {
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(),
}
lsCmd.Flags().StringVar(&lsOpts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json].")
lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display IDs.")
lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided.")
lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects")
Expand Down
17 changes: 8 additions & 9 deletions cmd/compose/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import (
"sort"
"strconv"
"strings"
"time"

"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types"

formatter2 "github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -81,7 +83,7 @@ func psCommand(p *projectOptions, backend api.Service) *cobra.Command {
ValidArgsFunction: completeServiceNames(p),
}
flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json]")
flags.StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).")
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
Expand Down Expand Up @@ -142,21 +144,18 @@ SERVICES:

return formatter.Print(containers, opts.Format, os.Stdout,
writer(containers),
"NAME", "COMMAND", "SERVICE", "STATUS", "PORTS")
"NAME", "IMAGE", "COMMAND", "SERVICE", "CREATED", "STATUS", "PORTS")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be weird to show the value/path from dockerfile for services that Compose built (instead of the auto-generated image name)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. We would get this path from the current compose file, so this would only work when ran with a --file but not with --project-name, and even with this would assume the current compose file is in sync with listed containers.

}

func writer(containers []api.ContainerSummary) func(w io.Writer) {
return func(w io.Writer) {
for _, container := range containers {
ports := displayablePorts(container)
status := container.State
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)
}
createdAt := time.Unix(container.Created, 0)
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
status := container.Status
command := formatter2.Ellipsis(container.Command, 20)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", container.Name, strconv.Quote(command), container.Service, status, ports)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", container.Name, container.Image, strconv.Quote(command), container.Service, created, status, ports)
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions cmd/compose/ps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ import (
"github.com/stretchr/testify/assert"
)

func TestPsPretty(t *testing.T) {
func TestPsTable(t *testing.T) {
ctx := context.Background()
origStdout := os.Stdout
t.Cleanup(func() {
os.Stdout = origStdout
})
dir := t.TempDir()
f, err := os.Create(filepath.Join(dir, "output.txt"))
out := filepath.Join(dir, "output.txt")
f, err := os.Create(out)
if err != nil {
t.Fatal("could not create output file")
}
Expand All @@ -51,8 +52,9 @@ func TestPsPretty(t *testing.T) {
DoAndReturn(func(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
return []api.ContainerSummary{
{
ID: "abc123",
Name: "ABC",
ID: "abc123",
Name: "ABC",
Image: "foo/bar",
Publishers: api.PortPublishers{
{
TargetPort: 8080,
Expand All @@ -76,8 +78,7 @@ func TestPsPretty(t *testing.T) {
_, err = f.Seek(0, 0)
assert.NoError(t, err)

output := make([]byte, 256)
_, err = f.Read(output)
output, err := os.ReadFile(out)
assert.NoError(t, err)

assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
Expand Down
5 changes: 4 additions & 1 deletion cmd/formatter/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package formatter

const (
// JSON is the constant for Json formats on list commands
// JSON Print in JSON format
JSON = "json"
// TemplateLegacyJSON the legacy json formatting value using go template
TemplateLegacyJSON = "{{json.}}"
// PRETTY is the constant for default formats on list commands
// Deprecated: use TABLE
PRETTY = "pretty"
// TABLE Print output in table format with column headers (default)
TABLE = "table"
)
2 changes: 1 addition & 1 deletion cmd/formatter/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
// Print prints formatted lists in different formats
func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
switch strings.ToLower(format) {
case PRETTY, "":
case TABLE, PRETTY, "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hurray for backwards compatibility 🎉

return PrintPrettySection(outWriter, writerFn, headers...)
case TemplateLegacyJSON:
switch reflect.TypeOf(toJSON).Kind() {
Expand Down
1 change: 1 addition & 0 deletions docs/reference/compose_images.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ List images used by the created containers

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | `table` | Format the output. Values: [table \| json]. |
| `-q`, `--quiet` | | | Only display IDs |


Expand Down
2 changes: 1 addition & 1 deletion docs/reference/compose_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ List running compose projects
| --- | --- | --- | --- |
| `-a`, `--all` | | | Show all stopped Compose projects |
| `--filter` | `filter` | | Filter output based on conditions provided. |
| `--format` | `string` | `pretty` | Format the output. Values: [pretty \| json]. |
| `--format` | `string` | `table` | Format the output. Values: [table \| json]. |
| `-q`, `--quiet` | | | Only display IDs. |


Expand Down
2 changes: 1 addition & 1 deletion docs/reference/compose_ps.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ List containers
| --- | --- | --- | --- |
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
| [`--format`](#format) | `string` | `pretty` | Format the output. Values: [pretty \| json] |
| [`--format`](#format) | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | | | Only display IDs |
| `--services` | | | Display services |
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/docker_compose_images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ usage: docker compose images [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json].'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/docker_compose_ls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ options:
swarm: false
- option: format
value_type: string
default_value: pretty
description: 'Format the output. Values: [pretty | json].'
default_value: table
description: 'Format the output. Values: [table | json].'
deprecated: false
hidden: false
experimental: false
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/docker_compose_ps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ options:
swarm: false
- option: format
value_type: string
default_value: pretty
description: 'Format the output. Values: [pretty | json]'
default_value: table
description: 'Format the output. Values: [table | json]'
details_url: '#format'
deprecated: false
hidden: false
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,13 @@ type PortPublisher struct {
type ContainerSummary struct {
ID string
Name string
Image any
Command string
Project string
Service string
Created int64
State string
Status string
Health string
ExitCode int
Publishers PortPublishers
Expand Down
3 changes: 3 additions & 0 deletions pkg/compose/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
summary[i] = api.ContainerSummary{
ID: container.ID,
Name: getCanonicalContainerName(container),
Image: container.Image,
Project: container.Labels[api.ProjectLabel],
Service: container.Labels[api.ServiceLabel],
Command: container.Command,
State: container.State,
Status: container.Status,
Created: container.Created,
Health: health,
ExitCode: exitCode,
Publishers: publishers,
Expand Down
12 changes: 8 additions & 4 deletions pkg/compose/ps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ func TestPs(t *testing.T) {
containers, err := tested.Ps(ctx, strings.ToLower(testProject), compose.PsOptions{})

expected := []compose.ContainerSummary{
{ID: "123", Name: "123", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "healthy", Publishers: nil},
{ID: "456", Name: "456", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90,
PublishedPort: 80}}},
{ID: "789", Name: "789", Project: strings.ToLower(testProject), Service: "service2", State: "exited", Health: "", ExitCode: 130, Publishers: nil},
{ID: "123", Name: "123", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "healthy", Publishers: nil},
{ID: "456", Name: "456", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "",
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}}},
{ID: "789", Name: "789", Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
State: "exited", Health: "", ExitCode: 130, Publishers: nil},
}
assert.NilError(t, err)
assert.DeepEqual(t, containers, expected)
Expand All @@ -74,6 +77,7 @@ func containerDetails(service string, id string, status string, health string, e
container := moby.Container{
ID: id,
Names: []string{"/" + id},
Image: "foo",
Labels: containerLabels(service, false),
State: status,
}
Expand Down
6 changes: 2 additions & 4 deletions pkg/e2e/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,11 @@ func TestLocalComposeUp(t *testing.T) {

t.Run("check healthcheck output", func(t *testing.T) {
c.WaitForCmdResult(t, c.NewDockerComposeCmd(t, "-p", projectName, "ps", "--format", "json"),
StdoutContains(`"Name":"compose-e2e-demo-web-1","Command":"/dispatcher","Project":"compose-e2e-demo","Service":"web","State":"running","Health":"healthy"`),
IsHealthy(projectName+"-web-1"),
5*time.Second, 1*time.Second)

res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `NAME COMMAND SERVICE STATUS PORTS`})
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-web-1 "/dispatcher" web running (healthy) 0.0.0.0:90->80/tcp`})
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-db-1 "docker-entrypoint.s…" db running 5432/tcp`})
assertServiceStatus(t, projectName, "web", "(healthy)", res.Stdout())
})

t.Run("images", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/e2e/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestCopy(t *testing.T) {

t.Run("make sure service is running", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `nginx running`})
assertServiceStatus(t, projectName, "nginx", "Up", res.Stdout())
})

t.Run("copy to container copies the file to the all containers by default", func(t *testing.T) {
Expand Down
22 changes: 22 additions & 0 deletions pkg/e2e/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package e2e

import (
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -314,6 +315,27 @@ func StdoutContains(expected string) func(*icmd.Result) bool {
}
}

func IsHealthy(service string) func(res *icmd.Result) bool {
return func(res *icmd.Result) bool {
type state struct {
Name string `json:"name"`
Health string `json:"health"`
}

ps := []state{}
err := json.Unmarshal([]byte(res.Stdout()), &ps)
if err != nil {
return false
}
for _, state := range ps {
if state.Name == service && state.Health == "healthy" {
return true
}
}
return false
}
}

// WaitForCmdResult try to execute a cmd until resulting output matches given predicate
func (c *CLI) WaitForCmdResult(
t testing.TB,
Expand Down
2 changes: 1 addition & 1 deletion pkg/e2e/ps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestPs(t *testing.T) {

assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined())

t.Run("pretty", func(t *testing.T) {
t.Run("table", func(t *testing.T) {
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps")
lines := strings.Split(res.Stdout(), "\n")
assert.Equal(t, 4, len(lines))
Expand Down
17 changes: 9 additions & 8 deletions pkg/e2e/restart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ import (
"gotest.tools/v3/assert"
)

func assertServiceStatus(t *testing.T, projectName, service, status string, ps string) {
// match output with random spaces like:
// e2e-start-stop-db-1 alpine:latest "echo hello" db 1 minutes ago Exited (0) 1 minutes ago
regx := fmt.Sprintf("%s-%s-1.+%s\\s+.+%s.+", projectName, service, service, status)
testify.Regexp(t, regx, ps)
}

func TestRestart(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-restart"

getServiceRegx := func(service string, status string) string {
// match output with random spaces like:
// e2e-start-stop-db-1 "echo hello" db running
return fmt.Sprintf("%s-%s-1.+%s\\s+%s", projectName, service, service, status)
}

t.Run("Up a project", func(t *testing.T) {
// This is just to ensure the containers do NOT exist
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
Expand All @@ -48,15 +49,15 @@ func TestRestart(t *testing.T) {
StdoutContains(`"State":"exited"`), 10*time.Second, 1*time.Second)

res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "-a")
testify.Regexp(t, getServiceRegx("restart", "exited"), res.Stdout())
assertServiceStatus(t, projectName, "restart", "Exited", res.Stdout())

c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--project-name", projectName, "restart")

// Give the same time but it must NOT exit
time.Sleep(time.Second)

res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
testify.Regexp(t, getServiceRegx("restart", "running"), res.Stdout())
assertServiceStatus(t, projectName, "restart", "Up", res.Stdout())

// Clean up
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
Expand Down
Loading