diff --git a/Gopkg.lock b/Gopkg.lock index 0b35835a5..a721ecf32 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1417,6 +1417,7 @@ "github.com/docker/cli/cli-plugins/manager", "github.com/docker/cli/cli-plugins/plugin", "github.com/docker/cli/cli/command", + "github.com/docker/cli/cli/command/formatter", "github.com/docker/cli/cli/command/stack", "github.com/docker/cli/cli/command/stack/options", "github.com/docker/cli/cli/command/stack/swarm", @@ -1433,6 +1434,7 @@ "github.com/docker/cli/cli/context/store", "github.com/docker/cli/cli/flags", "github.com/docker/cli/opts", + "github.com/docker/cli/templates", "github.com/docker/cnab-to-oci/relocation", "github.com/docker/cnab-to-oci/remotes", "github.com/docker/distribution/reference", diff --git a/e2e/images_test.go b/e2e/images_test.go index c3af0f015..eacca742e 100644 --- a/e2e/images_test.go +++ b/e2e/images_test.go @@ -66,11 +66,12 @@ func TestImageList(t *testing.T) { insertBundles(t, cmd) - expected := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago + expected := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago[ ]* ` + expectImageListOutput(t, cmd, expected) }) } @@ -87,10 +88,10 @@ func TestImageListDigests(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd insertBundles(t, cmd) - expected := `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago + expected := `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago[ ]* ` expectImageListDigestsOutput(t, cmd, expected) }) @@ -121,7 +122,7 @@ Deleted: b-simple-app:latest`, Err: `b-simple-app:latest: reference not found`, }) - expectedOutput := "REPOSITORY TAG APP IMAGE ID APP NAME CREATED\n" + expectedOutput := "REPOSITORY TAG APP IMAGE ID APP NAME CREATED \n" expectImageListOutput(t, cmd, expectedOutput) }) } @@ -139,8 +140,8 @@ func TestImageTag(t *testing.T) { cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app", filepath.Join("testdata", "simple")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - singleImageExpectation := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago + singleImageExpectation := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* ` expectImageListOutput(t, cmd, singleImageExpectation) @@ -189,63 +190,63 @@ a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago // tag image with only names dockerAppImageTag("a-simple-app", "b-simple-app") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* `) // target tag dockerAppImageTag("a-simple-app", "a-simple-app:0.1") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* `) // source tag dockerAppImageTag("a-simple-app:0.1", "c-simple-app") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* `) // source and target tags dockerAppImageTag("a-simple-app:0.1", "b-simple-app:0.2") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* `) // given a new application cmd.Command = dockerCli.Command("app", "build", "--tag", "push-pull", filepath.Join("testdata", "push-pull")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago[ ]* `) // can be tagged to an existing tag dockerAppImageTag("push-pull", "b-simple-app:0.2") icmd.RunCmd(cmd).Assert(t, icmd.Success) - expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago -a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -b-simple-app 0.2 [a-f0-9]{12} push-pull [La-z0-9 ]+ ago -b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago -push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago + expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED[ ]* +a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +b-simple-app 0.2 [a-f0-9]{12} push-pull [La-z0-9 ]+ ago[ ]* +b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago[ ]* +push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago[ ]* `) }) } diff --git a/internal/commands/image/formatter.go b/internal/commands/image/formatter.go new file mode 100644 index 000000000..2a842cca3 --- /dev/null +++ b/internal/commands/image/formatter.go @@ -0,0 +1,126 @@ +package image + +import ( + "time" + + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-units" +) + +const ( + defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Name}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t" + defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.Name}}\t\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t" + + imageIDHeader = "APP IMAGE ID" + repositoryHeader = "REPOSITORY" + tagHeader = "TAG" + digestHeader = "DIGEST" + imageNameHeader = "APP NAME" +) + +// NewImageFormat returns a format for rendering an ImageContext +func NewImageFormat(source string, quiet bool, digest bool) formatter.Format { + switch source { + case formatter.TableFormatKey: + switch { + case quiet: + return formatter.DefaultQuietFormat + case digest: + return defaultImageTableFormatWithDigest + default: + return defaultImageTableFormat + } + } + + format := formatter.Format(source) + if format.IsTable() && digest && !format.Contains("{{.Digest}}") { + format += "\t{{.Digest}}" + } + return format +} + +// Write writes the formatter images using the ImageContext +func Write(ctx formatter.Context, images []imageDesc) error { + render := func(format func(subContext formatter.SubContext) error) error { + return imageFormat(ctx, images, format) + } + return ctx.Write(newImageContext(), render) +} + +func imageFormat(ctx formatter.Context, images []imageDesc, format func(subContext formatter.SubContext) error) error { + for _, image := range images { + img := &imageContext{ + trunc: ctx.Trunc, + i: image} + if err := format(img); err != nil { + return err + } + } + return nil +} + +type imageContext struct { + formatter.HeaderContext + trunc bool + i imageDesc +} + +func newImageContext() *imageContext { + imageCtx := imageContext{} + imageCtx.Header = formatter.SubHeaderContext{ + "ID": imageIDHeader, + "Name": imageNameHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + "Digest": digestHeader, + "CreatedSince": formatter.CreatedSinceHeader, + } + return &imageCtx +} + +func (c *imageContext) MarshalJSON() ([]byte, error) { + return formatter.MarshalJSON(c) +} + +func (c *imageContext) ID() string { + if c.trunc { + return stringid.TruncateID(c.i.ID) + } + return c.i.ID +} + +func (c *imageContext) Name() string { + if c.i.Name == "" { + return "" + } + return c.i.Name +} + +func (c *imageContext) Repository() string { + if c.i.Repository == "" { + return "" + } + return c.i.Repository +} + +func (c *imageContext) Tag() string { + if c.i.Tag == "" { + return "" + } + return c.i.Tag +} + +func (c *imageContext) Digest() string { + if c.i.Digest == "" { + return "" + } + return c.i.Digest +} + +func (c *imageContext) CreatedSince() string { + if c.i.Created.IsZero() { + return "" + } + return units.HumanDuration(time.Now().UTC().Sub(c.i.Created)) + " ago" +} diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index 952d582cf..4ab51ad88 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -1,32 +1,23 @@ package image import ( - "bytes" - "fmt" - "io" - "strings" - "text/tabwriter" "time" "github.com/docker/app/internal/packager" "github.com/docker/app/internal/relocated" "github.com/docker/app/internal/store" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/config" "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" "github.com/spf13/cobra" ) type imageListOption struct { quiet bool digests bool -} - -type imageListColumn struct { - header string - value func(p pkg) string + format string } func listCmd(dockerCli command.Cli) *cobra.Command { @@ -52,76 +43,48 @@ func listCmd(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show numeric IDs") flags.BoolVarP(&options.digests, "digests", "", false, "Show image digests") + cmd.Flags().StringVarP(&options.format, "format", "f", "table", "Format the output using the given syntax or Go template") + cmd.Flags().SetAnnotation("format", "experimentalCLI", []string{"true"}) //nolint:errcheck return cmd } func runList(dockerCli command.Cli, options imageListOption, bundleStore store.BundleStore) error { - bundles, err := bundleStore.List() + images, err := getImageDescriptors(bundleStore) if err != nil { return err } - pkgs, err := getPackages(bundleStore, bundles) - if err != nil { - return err + ctx := formatter.Context{ + Output: dockerCli.Out(), + Format: NewImageFormat(options.format, options.quiet, options.digests), } - if options.quiet { - return printImageIDs(dockerCli, pkgs) - } - return printImages(dockerCli, pkgs, options) + return Write(ctx, images) } -func getPackages(bundleStore store.BundleStore, references []reference.Reference) ([]pkg, error) { - packages := make([]pkg, len(references)) +func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) { + references, err := bundleStore.List() + if err != nil { + return nil, err + } + images := make([]imageDesc, len(references)) for i, ref := range references { b, err := bundleStore.Read(ref) if err != nil { return nil, err } - pk := pkg{ - bundle: b, - ref: ref, - } - - packages[i] = pk - } - - return packages, nil -} - -func printImages(dockerCli command.Cli, refs []pkg, options imageListOption) error { - w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) - listColumns := getImageListColumns(options) - printHeaders(w, listColumns) - for _, ref := range refs { - printValues(w, ref, listColumns) - } - - return w.Flush() -} - -func printImageIDs(dockerCli command.Cli, refs []pkg) error { - var buf bytes.Buffer - - for _, ref := range refs { - id, err := getImageID(ref) - if err != nil { - return err - } - fmt.Fprintln(&buf, id) + images[i] = getImageDesc(b, ref) } - fmt.Fprint(dockerCli.Out(), buf.String()) - return nil + return images, nil } -func getImageID(p pkg) (string, error) { - id, ok := p.ref.(store.ID) +func getImageID(bundle *relocated.Bundle, ref reference.Reference) (string, error) { + id, ok := ref.(store.ID) if !ok { var err error - id, err = store.FromBundle(p.bundle) + id, err = store.FromBundle(bundle) if err != nil { return "", err } @@ -129,71 +92,42 @@ func getImageID(p pkg) (string, error) { return stringid.TruncateID(id.String()), nil } -func printHeaders(w io.Writer, listColumns []imageListColumn) { - var headers []string - for _, column := range listColumns { - headers = append(headers, column.header) - } - fmt.Fprintln(w, strings.Join(headers, "\t")) +type imageDesc struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + Digest string `json:"digest,omitempty"` + Created time.Time `json:"created,omitempty"` } -func printValues(w io.Writer, ref pkg, listColumns []imageListColumn) { - var values []string - for _, column := range listColumns { - values = append(values, column.value(ref)) +func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc { + var id string + id, _ = getImageID(bundle, ref) + var repository string + if n, ok := ref.(reference.Named); ok { + repository = reference.FamiliarName(n) } - fmt.Fprintln(w, strings.Join(values, "\t")) -} - -func getImageListColumns(options imageListOption) []imageListColumn { - columns := []imageListColumn{ - {"REPOSITORY", func(p pkg) string { - if n, ok := p.ref.(reference.Named); ok { - return reference.FamiliarName(n) - } - return "" - }}, - {"TAG", func(p pkg) string { - if t, ok := p.ref.(reference.Tagged); ok { - return t.Tag() - } - return "" - }}, + var tag string + if t, ok := ref.(reference.Tagged); ok { + tag = t.Tag() } - if options.digests { - columns = append(columns, imageListColumn{"DIGEST", func(p pkg) string { - if t, ok := p.ref.(reference.Digested); ok { - return t.Digest().String() - } - return "" - }}) + var digest string + if t, ok := ref.(reference.Digested); ok { + digest = t.Digest().String() + } + var created time.Time + if payload, err := packager.CustomPayload(bundle.Bundle); err == nil { + if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok { + created = createdPayload.CreatedTime() + } + } + return imageDesc{ + ID: id, + Name: bundle.Name, + Repository: repository, + Tag: tag, + Digest: digest, + Created: created, } - columns = append(columns, - imageListColumn{"APP IMAGE ID", func(p pkg) string { - id, err := getImageID(p) - if err != nil { - return "" - } - return id - }}, - imageListColumn{"APP NAME", func(p pkg) string { - return p.bundle.Name - }}, - imageListColumn{"CREATED", func(p pkg) string { - payload, err := packager.CustomPayload(p.bundle.Bundle) - if err != nil { - return "" - } - if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok { - return units.HumanDuration(time.Now().UTC().Sub(createdPayload.CreatedTime())) + " ago" - } - return "" - }}, - ) - return columns -} - -type pkg struct { - ref reference.Reference - bundle *relocated.Bundle } diff --git a/internal/commands/image/list_test.go b/internal/commands/image/list_test.go index 7cd121991..3fe1e4d3e 100644 --- a/internal/commands/image/list_test.go +++ b/internal/commands/image/list_test.go @@ -83,21 +83,31 @@ func TestListCmd(t *testing.T) { }{ { name: "TestList", - expectedOutput: `REPOSITORY TAG APP IMAGE ID APP NAME CREATED -foo/bar 3f825b2d0657 Digested App -foo/bar 1.0 9aae408ee04f Foo App - a855ac937f2e Quiet App + expectedOutput: `REPOSITORY TAG APP IMAGE ID APP NAME CREATED +foo/bar 3f825b2d0657 Digested App N/A +foo/bar 1.0 9aae408ee04f Foo App N/A + a855ac937f2e Quiet App N/A `, - options: imageListOption{}, + options: imageListOption{format: "table"}, + }, + { + name: "TestTemplate", + expectedOutput: `APP IMAGE ID DIGEST +3f825b2d0657 sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 +9aae408ee04f +a855ac937f2e sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 +`, + options: imageListOption{format: "table {{.ID}}", digests: true}, }, { name: "TestListWithDigests", - expectedOutput: `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED -foo/bar sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 3f825b2d0657 Digested App -foo/bar 1.0 9aae408ee04f Foo App - sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App + //nolint:lll + expectedOutput: `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED +foo/bar sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 3f825b2d0657 Digested App N/A +foo/bar 1.0 9aae408ee04f Foo App N/A + sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App N/A `, - options: imageListOption{digests: true}, + options: imageListOption{format: "table", digests: true}, }, { name: "TestListWithQuiet", @@ -105,7 +115,7 @@ foo/bar 1.0 9aae408ee04f a855ac937f2e `, - options: imageListOption{quiet: true}, + options: imageListOption{format: "table", quiet: true}, }, } diff --git a/internal/commands/list.go b/internal/commands/list.go index b66c71f02..29b9703f7 100644 --- a/internal/commands/list.go +++ b/internal/commands/list.go @@ -12,7 +12,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" + "github.com/docker/cli/templates" units "github.com/docker/go-units" + "github.com/docker/go/canonical/json" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -36,25 +39,44 @@ var ( ) func listCmd(dockerCli command.Cli) *cobra.Command { + var template string cmd := &cobra.Command{ Use: "ls [OPTIONS]", Short: "List running Apps", Aliases: []string{"list"}, Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli) + return runList(dockerCli, template) }, } + cmd.Flags().StringVarP(&template, "format", "f", "", "Format the output using the given syntax or Go template") + cmd.Flags().SetAnnotation("format", "experimentalCLI", []string{"true"}) //nolint:errcheck return cmd } -func runList(dockerCli command.Cli) error { +func runList(dockerCli command.Cli, template string) error { installations, err := getInstallations(dockerCli.CurrentContext(), config.Dir()) if err != nil { return err } + if template == "json" { + bytes, err := json.MarshalIndent(installations, "", " ") + if err != nil { + return errors.Errorf("Failed to marshall json: %s", err) + } + _, err = dockerCli.Out().Write(bytes) + return err + } + if template != "" { + tmpl, err := templates.Parse(template) + if err != nil { + return errors.Errorf("Template parsing error: %s", err) + } + return tmpl.Execute(dockerCli.Out(), installations) + } + w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) printHeaders(w)