diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go
index 256b30d5..29c6f90e 100644
--- a/cmd/compose/compose.go
+++ b/cmd/compose/compose.go
@@ -275,7 +275,7 @@ func RunningAsStandalone() bool {
}
// RootCommand returns the compose command with its child commands
-func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
+func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
// filter out useless commandConn.CloseWrite warning message that can occur
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
@@ -307,7 +307,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
return cmd.Help()
}
if version {
- return versionCommand(streams).Execute()
+ return versionCommand(dockerCli).Execute()
}
_ = cmd.Help()
return dockercli.StatusError{
@@ -345,11 +345,11 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
ansi = v
}
- formatter.SetANSIMode(streams, ansi)
+ formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
ui.NoColor()
- formatter.SetANSIMode(streams, formatter.Never)
+ formatter.SetANSIMode(dockerCli, formatter.Never)
}
switch ansi {
@@ -426,26 +426,26 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
}
c.AddCommand(
- upCommand(&opts, streams, backend),
+ upCommand(&opts, dockerCli, backend),
downCommand(&opts, backend),
startCommand(&opts, backend),
restartCommand(&opts, backend),
stopCommand(&opts, backend),
- psCommand(&opts, streams, backend),
- listCommand(streams, backend),
- logsCommand(&opts, streams, backend),
- configCommand(&opts, streams, backend),
+ psCommand(&opts, dockerCli, backend),
+ listCommand(dockerCli, backend),
+ logsCommand(&opts, dockerCli, backend),
+ configCommand(&opts, dockerCli, backend),
killCommand(&opts, backend),
- runCommand(&opts, streams, backend),
+ runCommand(&opts, dockerCli, backend),
removeCommand(&opts, backend),
- execCommand(&opts, streams, backend),
+ execCommand(&opts, dockerCli, backend),
pauseCommand(&opts, backend),
unpauseCommand(&opts, backend),
- topCommand(&opts, streams, backend),
- eventsCommand(&opts, streams, backend),
- portCommand(&opts, streams, backend),
- imagesCommand(&opts, streams, backend),
- versionCommand(streams),
+ topCommand(&opts, dockerCli, backend),
+ eventsCommand(&opts, dockerCli, backend),
+ portCommand(&opts, dockerCli, backend),
+ imagesCommand(&opts, dockerCli, backend),
+ versionCommand(dockerCli),
buildCommand(&opts, &progress, backend),
pushCommand(&opts, backend),
pullCommand(&opts, backend),
diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go
index 007cc2a9..1386234e 100644
--- a/cmd/compose/ps.go
+++ b/cmd/compose/ps.go
@@ -19,22 +19,18 @@ package compose
import (
"context"
"fmt"
- "io"
"sort"
- "strconv"
"strings"
- "time"
"github.com/docker/compose/v2/cmd/formatter"
+ "github.com/docker/compose/v2/pkg/api"
"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/docker/cli/cli/command"
+ cliformatter "github.com/docker/cli/cli/command/formatter"
+ cliflags "github.com/docker/cli/cli/flags"
"github.com/pkg/errors"
"github.com/spf13/cobra"
-
- "github.com/docker/compose/v2/pkg/api"
)
type psOptions struct {
@@ -66,7 +62,7 @@ func (p *psOptions) parseFilter() error {
return nil
}
-func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
+func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
@@ -77,12 +73,12 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
return opts.parseFilter()
},
RunE: Adapt(func(ctx context.Context, args []string) error {
- return runPs(ctx, streams, backend, args, opts)
+ return runPs(ctx, dockerCli, backend, args, opts)
}),
ValidArgsFunction: completeServiceNames(p),
}
flags := psCmd.Flags()
- flags.StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
+ flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
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")
@@ -91,7 +87,7 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
return psCmd
}
-func runPs(ctx context.Context, streams api.Streams, backend api.Service, services []string, opts psOptions) error {
+func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
project, name, err := opts.projectOrName(services...)
if err != nil {
return err
@@ -125,38 +121,32 @@ func runPs(ctx context.Context, streams api.Streams, backend api.Service, servic
if opts.Quiet {
for _, c := range containers {
- fmt.Fprintln(streams.Out(), c.ID)
+ fmt.Fprintln(dockerCli.Out(), c.ID)
}
return nil
}
if opts.Services {
services := []string{}
- for _, s := range containers {
- if !utils.StringContains(services, s.Service) {
- services = append(services, s.Service)
+ for _, c := range containers {
+ s := c.Service
+ if !utils.StringContains(services, s) {
+ services = append(services, s)
}
}
- fmt.Fprintln(streams.Out(), strings.Join(services, "\n"))
+ fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
return nil
}
- return formatter.Print(containers, opts.Format, streams.Out(),
- writer(containers),
- "NAME", "IMAGE", "COMMAND", "SERVICE", "CREATED", "STATUS", "PORTS")
-}
-
-func writer(containers []api.ContainerSummary) func(w io.Writer) {
- return func(w io.Writer) {
- for _, container := range containers {
- ports := displayablePorts(container)
- 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\t%s\t%s\n", container.Name, container.Image, strconv.Quote(command), container.Service, created, status, ports)
- }
+ if opts.Format == "" {
+ opts.Format = dockerCli.ConfigFile().PsFormat
}
+
+ containerCtx := cliformatter.Context{
+ Output: dockerCli.Out(),
+ Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
+ }
+ return formatter.ContainerWrite(containerCtx, containers)
}
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
@@ -177,21 +167,3 @@ func hasStatus(c api.ContainerSummary, statuses []string) bool {
}
return false
}
-
-func displayablePorts(c api.ContainerSummary) string {
- if c.Publishers == nil {
- return ""
- }
-
- ports := make([]types.Port, len(c.Publishers))
- for i, pub := range c.Publishers {
- ports[i] = types.Port{
- IP: pub.URL,
- PrivatePort: uint16(pub.TargetPort),
- PublicPort: uint16(pub.PublishedPort),
- Type: pub.Protocol,
- }
- }
-
- return formatter2.DisplayablePorts(ports)
-}
diff --git a/cmd/compose/ps_test.go b/cmd/compose/ps_test.go
index 621214ff..c526f831 100644
--- a/cmd/compose/ps_test.go
+++ b/cmd/compose/ps_test.go
@@ -18,11 +18,11 @@ package compose
import (
"context"
- "io"
"os"
"path/filepath"
"testing"
+ "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
@@ -69,7 +69,11 @@ func TestPsTable(t *testing.T) {
}).AnyTimes()
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
- err = runPs(ctx, stream{out: streams.NewOut(f)}, backend, nil, opts)
+ stdout := streams.NewOut(f)
+ cli := mocks.NewMockCli(ctrl)
+ cli.EXPECT().Out().Return(stdout).AnyTimes()
+ cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
+ err = runPs(ctx, cli, backend, nil, opts)
assert.NoError(t, err)
_, err = f.Seek(0, 0)
@@ -80,21 +84,3 @@ func TestPsTable(t *testing.T) {
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
}
-
-type stream struct {
- out *streams.Out
- err io.Writer
- in *streams.In
-}
-
-func (s stream) Out() *streams.Out {
- return s.out
-}
-
-func (s stream) Err() io.Writer {
- return s.err
-}
-
-func (s stream) In() *streams.In {
- return s.in
-}
diff --git a/cmd/formatter/container.go b/cmd/formatter/container.go
new file mode 100644
index 00000000..872635ad
--- /dev/null
+++ b/cmd/formatter/container.go
@@ -0,0 +1,196 @@
+/*
+ Copyright 2020 Docker Compose CLI authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package formatter
+
+import (
+ "time"
+
+ "github.com/docker/cli/cli/command/formatter"
+ "github.com/docker/compose/v2/pkg/api"
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/pkg/stringid"
+ "github.com/docker/go-units"
+)
+
+const (
+ defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
+
+ nameHeader = "NAME"
+ serviceHeader = "SERVICE"
+ commandHeader = "COMMAND"
+ runningForHeader = "CREATED"
+ mountsHeader = "MOUNTS"
+ localVolumes = "LOCAL VOLUMES"
+ networksHeader = "NETWORKS"
+)
+
+// NewContainerFormat returns a Format for rendering using a Context
+func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
+ switch source {
+ case formatter.TableFormatKey, "": // table formatting is the default if none is set.
+ if quiet {
+ return formatter.DefaultQuietFormat
+ }
+ format := defaultContainerTableFormat
+ if size {
+ format += `\t{{.Size}}`
+ }
+ return formatter.Format(format)
+ case formatter.RawFormatKey:
+ if quiet {
+ return `container_id: {{.ID}}`
+ }
+ format := `container_id: {{.ID}}
+image: {{.Image}}
+command: {{.Command}}
+created_at: {{.CreatedAt}}
+state: {{- pad .State 1 0}}
+status: {{- pad .Status 1 0}}
+names: {{.Names}}
+labels: {{- pad .Labels 1 0}}
+ports: {{- pad .Ports 1 0}}
+`
+ if size {
+ format += `size: {{.Size}}\n`
+ }
+ return formatter.Format(format)
+ default: // custom format
+ if quiet {
+ return formatter.DefaultQuietFormat
+ }
+ return formatter.Format(source)
+ }
+}
+
+// ContainerWrite renders the context for a list of containers
+func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
+ render := func(format func(subContext formatter.SubContext) error) error {
+ for _, container := range containers {
+ err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ return ctx.Write(NewContainerContext(), render)
+}
+
+// ContainerContext is a struct used for rendering a list of containers in a Go template.
+type ContainerContext struct {
+ formatter.HeaderContext
+ trunc bool
+ c api.ContainerSummary
+
+ // FieldsUsed is used in the pre-processing step to detect which fields are
+ // used in the template. It's currently only used to detect use of the .Size
+ // field which (if used) automatically sets the '--size' option when making
+ // the API call.
+ FieldsUsed map[string]interface{}
+}
+
+// NewContainerContext creates a new context for rendering containers
+func NewContainerContext() *ContainerContext {
+ containerCtx := ContainerContext{}
+ containerCtx.Header = formatter.SubHeaderContext{
+ "ID": formatter.ContainerIDHeader,
+ "Name": nameHeader,
+ "Service": serviceHeader,
+ "Image": formatter.ImageHeader,
+ "Command": commandHeader,
+ "CreatedAt": formatter.CreatedAtHeader,
+ "RunningFor": runningForHeader,
+ "Ports": formatter.PortsHeader,
+ "State": formatter.StateHeader,
+ "Status": formatter.StatusHeader,
+ "Size": formatter.SizeHeader,
+ "Labels": formatter.LabelsHeader,
+ }
+ return &containerCtx
+}
+
+// MarshalJSON makes ContainerContext implement json.Marshaler
+func (c *ContainerContext) MarshalJSON() ([]byte, error) {
+ return formatter.MarshalJSON(c)
+}
+
+// ID returns the container's ID as a string. Depending on the `--no-trunc`
+// option being set, the full or truncated ID is returned.
+func (c *ContainerContext) ID() string {
+ if c.trunc {
+ return stringid.TruncateID(c.c.ID)
+ }
+ return c.c.ID
+}
+
+func (c *ContainerContext) Name() string {
+ return c.c.Name
+}
+
+func (c *ContainerContext) Service() string {
+ return c.c.Service
+}
+
+func (c *ContainerContext) Image() string {
+ return c.c.Image
+}
+
+func (c *ContainerContext) Command() string {
+ return c.c.Command
+}
+
+func (c *ContainerContext) CreatedAt() string {
+ return time.Unix(c.c.Created, 0).String()
+}
+
+func (c *ContainerContext) RunningFor() string {
+ createdAt := time.Unix(c.c.Created, 0)
+ return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
+}
+
+func (c *ContainerContext) ExitCode() int {
+ return c.c.ExitCode
+}
+
+func (c *ContainerContext) State() string {
+ return c.c.State
+}
+
+func (c *ContainerContext) Status() string {
+ return c.c.Status
+}
+
+func (c *ContainerContext) Health() string {
+ return c.c.Health
+}
+
+func (c *ContainerContext) Publishers() api.PortPublishers {
+ return c.c.Publishers
+}
+
+func (c *ContainerContext) Ports() string {
+ var ports []types.Port
+ for _, publisher := range c.c.Publishers {
+ ports = append(ports, types.Port{
+ IP: publisher.URL,
+ PrivatePort: uint16(publisher.TargetPort),
+ PublicPort: uint16(publisher.PublishedPort),
+ Type: publisher.Protocol,
+ })
+ }
+ return formatter.DisplayablePorts(ports)
+}
diff --git a/docs/reference/compose_ps.md b/docs/reference/compose_ps.md
index 4e97a8a8..15c7b1b6 100644
--- a/docs/reference/compose_ps.md
+++ b/docs/reference/compose_ps.md
@@ -5,15 +5,15 @@ List containers
### Options
-| Name | Type | Default | Description |
-|:----------------------|:--------------|:--------|:--------------------------------------------------------------------------------------------------------------|
-| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
-| `--dry-run` | | | Execute command in dry run mode |
-| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
-| [`--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] |
+| Name | Type | Default | Description |
+|:----------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
+| `--dry-run` | | | Execute command in dry run mode |
+| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
+| [`--format`](#format) | `string` | `table` | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
+| `-q`, `--quiet` | | | Only display IDs |
+| `--services` | | | Display services |
+| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
diff --git a/docs/reference/docker_compose_ps.yaml b/docs/reference/docker_compose_ps.yaml
index a8c76c3c..d51d0ac9 100644
--- a/docs/reference/docker_compose_ps.yaml
+++ b/docs/reference/docker_compose_ps.yaml
@@ -46,7 +46,13 @@ options:
- option: format
value_type: string
default_value: table
- description: 'Format the output. Values: [table | json]'
+ description: |-
+ Format output using a custom template:
+ 'table': Print output in table format with column headers (default)
+ 'table TEMPLATE': Print output in table format using the given Go template
+ 'json': Print in JSON format
+ 'TEMPLATE': Print output using the given Go template.
+ Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
details_url: '#format'
deprecated: false
hidden: false
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 41dda691..16e3b3a5 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -392,7 +392,7 @@ type PortPublisher struct {
type ContainerSummary struct {
ID string
Name string
- Image any
+ Image string
Command string
Project string
Service string
diff --git a/pkg/e2e/assert.go b/pkg/e2e/assert.go
index 5cfcc1d5..accc9f59 100644
--- a/pkg/e2e/assert.go
+++ b/pkg/e2e/assert.go
@@ -29,18 +29,16 @@ import (
func RequireServiceState(t testing.TB, cli *CLI, service string, state string) {
t.Helper()
psRes := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
- var psOut []map[string]interface{}
- require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &psOut),
+ var svc map[string]interface{}
+ require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &svc),
"Invalid `compose ps` JSON output")
- for _, svc := range psOut {
- require.Equal(t, service, svc["Service"],
- "Found ps output for unexpected service")
- require.Equalf(t,
- strings.ToLower(state),
- strings.ToLower(svc["State"].(string)),
- "Service %q (%s) not in expected state",
- service, svc["Name"],
- )
- }
+ require.Equal(t, service, svc["Service"],
+ "Found ps output for unexpected service")
+ require.Equalf(t,
+ strings.ToLower(state),
+ strings.ToLower(svc["State"].(string)),
+ "Service %q (%s) not in expected state",
+ service, svc["Name"],
+ )
}
diff --git a/pkg/e2e/framework.go b/pkg/e2e/framework.go
index cb3255d0..e9ded780 100644
--- a/pkg/e2e/framework.go
+++ b/pkg/e2e/framework.go
@@ -361,13 +361,14 @@ func IsHealthy(service string) func(res *icmd.Result) bool {
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" {
+ decoder := json.NewDecoder(strings.NewReader(res.Stdout()))
+ for decoder.More() {
+ ps := state{}
+ err := decoder.Decode(&ps)
+ if err != nil {
+ return false
+ }
+ if ps.Name == service && ps.Health == "healthy" {
return true
}
}
diff --git a/pkg/e2e/pause_test.go b/pkg/e2e/pause_test.go
index e5060ae3..c3699eb8 100644
--- a/pkg/e2e/pause_test.go
+++ b/pkg/e2e/pause_test.go
@@ -138,16 +138,14 @@ func urlForService(t testing.TB, cli *CLI, service string, targetPort int) strin
func publishedPortForService(t testing.TB, cli *CLI, service string, targetPort int) int {
t.Helper()
res := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
- var psOut []struct {
+ var svc struct {
Publishers []struct {
TargetPort int
PublishedPort int
}
}
- require.NoError(t, json.Unmarshal([]byte(res.Stdout()), &psOut),
+ require.NoError(t, json.Unmarshal([]byte(res.Stdout()), &svc),
"Failed to parse `%s` output", res.Cmd.String())
- require.Len(t, psOut, 1, "Expected exactly 1 service")
- svc := psOut[0]
for _, pp := range svc.Publishers {
if pp.TargetPort == targetPort {
return pp.PublishedPort
diff --git a/pkg/e2e/ps_test.go b/pkg/e2e/ps_test.go
index 51e6b24f..ccaa3c2b 100644
--- a/pkg/e2e/ps_test.go
+++ b/pkg/e2e/ps_test.go
@@ -63,8 +63,12 @@ func TestPs(t *testing.T) {
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps",
"--format", "json")
var output []api.ContainerSummary
- err := json.Unmarshal([]byte(res.Stdout()), &output)
- require.NoError(t, err, "Failed to unmarshal ps JSON output")
+ dec := json.NewDecoder(strings.NewReader(res.Stdout()))
+ for dec.More() {
+ var s api.ContainerSummary
+ require.NoError(t, dec.Decode(&s), "Failed to unmarshal ps JSON output")
+ output = append(output, s)
+ }
count := 0
assert.Equal(t, 2, len(output))