align `--format` flag and UX with docker cli

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2022-12-11 10:59:01 +01:00 committed by Nicolas De loof
parent bc568eeb9b
commit 8c39b5b7fd
19 changed files with 83 additions and 47 deletions

View File

@ -35,7 +35,8 @@ import (
type imageOptions struct {
*projectOptions
Quiet bool
Quiet bool
Format string
}
func imagesCommand(p *projectOptions, backend api.Service) *cobra.Command {
@ -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
}
@ -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)
@ -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")
}

View File

@ -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")

View File

@ -83,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")

View File

@ -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")
}
@ -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,
@ -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")

View File

@ -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"
)

View File

@ -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, "":
return PrintPrettySection(outWriter, writerFn, headers...)
case TemplateLegacyJSON:
switch reflect.TypeOf(toJSON).Kind() {

View File

@ -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 |

View File

@ -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. |

View File

@ -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] |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
@ -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,
}

View File

@ -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) {

View File

@ -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) {

View File

@ -17,6 +17,7 @@
package e2e
import (
"encoding/json"
"fmt"
"io"
"net/http"
@ -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,

View File

@ -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))

View File

@ -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")
@ -48,7 +49,7 @@ 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")
@ -56,7 +57,7 @@ func TestRestart(t *testing.T) {
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")

View File

@ -36,12 +36,6 @@ func TestStartStop(t *testing.T) {
return fmt.Sprintf("%s\\s+%s\\(%d\\)", projectName, status, 2)
}
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) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up",
"-d")
@ -51,8 +45,8 @@ func TestStartStop(t *testing.T) {
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
testify.Regexp(t, getServiceRegx("simple", "running"), res.Stdout())
testify.Regexp(t, getServiceRegx("another", "running"), res.Stdout())
assertServiceStatus(t, projectName, "simple", "Up", res.Stdout())
assertServiceStatus(t, projectName, "another", "Up", res.Stdout())
})
t.Run("stop project", func(t *testing.T) {
@ -68,8 +62,8 @@ func TestStartStop(t *testing.T) {
assert.Assert(t, !strings.Contains(res.Combined(), "e2e-start-stop-no-dependencies-words-1"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--all")
testify.Regexp(t, getServiceRegx("simple", "exited"), res.Stdout())
testify.Regexp(t, getServiceRegx("another", "exited"), res.Stdout())
assertServiceStatus(t, projectName, "simple", "Exited", res.Stdout())
assertServiceStatus(t, projectName, "another", "Exited", res.Stdout())
})
t.Run("start project", func(t *testing.T) {