浏览代码

align docker compose ps with docker CLI to support --format

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 年之前
父节点
当前提交
1054792b47

+ 16 - 16
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),

+ 21 - 49
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")
-}
+	if opts.Format == "" {
+		opts.Format = dockerCli.ConfigFile().PsFormat
+	}
 
-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)
-		}
+	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)
-}

+ 6 - 20
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
-}

+ 196 - 0
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)
+}

+ 9 - 9
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:<br>'table':            Print output in table format with column headers (default)<br>'table TEMPLATE':   Print output in table format using the given Go template<br>'json':             Print in JSON format<br>'TEMPLATE':         Print output using the given Go template.<br>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]                                                                                                                                                                                                                                                                                                                        |
 
 
 <!---MARKER_GEN_END-->

+ 7 - 1
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

+ 1 - 1
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

+ 10 - 12
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"],
+	)
 }

+ 8 - 7
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
 			}
 		}

+ 2 - 4
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

+ 6 - 2
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))