From 40a670c61e395ac0e0776ce77e54b1cdd0efa5cb Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 13 Nov 2019 15:34:23 +0100 Subject: [PATCH 1/6] Support format option in docker app ls Using either plain JSON or a go template Signed-off-by: Nicolas De Loof --- internal/commands/list.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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) From 9b4f291649d6cf1b6377b28683ff3fff1e5b7fee Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 13 Nov 2019 16:55:51 +0100 Subject: [PATCH 2/6] Introduce --format option for docker app image ls Signed-off-by: Nicolas De Loof --- internal/commands/image/list.go | 123 ++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 31 deletions(-) diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index 952d582cf..8c3e3031f 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -2,12 +2,16 @@ package image import ( "bytes" + "encoding/json" "fmt" "io" "strings" "text/tabwriter" "time" + "github.com/docker/cli/templates" + "github.com/pkg/errors" + "github.com/docker/app/internal/packager" "github.com/docker/app/internal/relocated" "github.com/docker/app/internal/store" @@ -20,13 +24,14 @@ import ( ) type imageListOption struct { - quiet bool - digests bool + quiet bool + digests bool + template string } type imageListColumn struct { header string - value func(p pkg) string + value func(desc imageDesc) string } func listCmd(dockerCli command.Cli) *cobra.Command { @@ -52,6 +57,8 @@ 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.template, "format", "f", "", "Format the output using the given syntax or Go template") + cmd.Flags().SetAnnotation("format", "experimentalCLI", []string{"true"}) //nolint:errcheck return cmd } @@ -94,10 +101,32 @@ func getPackages(bundleStore store.BundleStore, references []reference.Reference func printImages(dockerCli command.Cli, refs []pkg, options imageListOption) error { w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) + + list := []imageDesc{} + for _, ref := range refs { + list = append(list, getImageDesc(ref)) + } + + if options.template == "json" { + bytes, err := json.MarshalIndent(list, "", " ") + if err != nil { + return errors.Errorf("Failed to marshall json: %s", err) + } + _, err = dockerCli.Out().Write(bytes) + return err + } + if options.template != "" { + tmpl, err := templates.Parse(options.template) + if err != nil { + return errors.Errorf("Template parsing error: %s", err) + } + return tmpl.Execute(dockerCli.Out(), list) + } + listColumns := getImageListColumns(options) printHeaders(w, listColumns) - for _, ref := range refs { - printValues(w, ref, listColumns) + for _, desc := range list { + printValues(w, desc, listColumns) } return w.Flush() @@ -137,55 +166,87 @@ func printHeaders(w io.Writer, listColumns []imageListColumn) { fmt.Fprintln(w, strings.Join(headers, "\t")) } -func printValues(w io.Writer, ref pkg, listColumns []imageListColumn) { +func printValues(w io.Writer, desc imageDesc, listColumns []imageListColumn) { var values []string for _, column := range listColumns { - values = append(values, column.value(ref)) + values = append(values, column.value(desc)) } fmt.Fprintln(w, strings.Join(values, "\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.Duration `json:"created,omitempty"` +} + +func getImageDesc(p pkg) imageDesc { + var id string + id, _ = getImageID(p) + var repository string + if n, ok := p.ref.(reference.Named); ok { + repository = reference.FamiliarName(n) + } + var tag string + if t, ok := p.ref.(reference.Tagged); ok { + tag = t.Tag() + } + var digest string + if t, ok := p.ref.(reference.Digested); ok { + digest = t.Digest().String() + } + var created time.Duration + if payload, err := packager.CustomPayload(p.bundle.Bundle); err == nil { + if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok { + created = time.Now().UTC().Sub(createdPayload.CreatedTime()) + } + } + return imageDesc{ + ID: id, + Name: p.bundle.Name, + Repository: repository, + Tag: tag, + Digest: digest, + Created: created, + } +} + func getImageListColumns(options imageListOption) []imageListColumn { columns := []imageListColumn{ - {"REPOSITORY", func(p pkg) string { - if n, ok := p.ref.(reference.Named); ok { - return reference.FamiliarName(n) + {"REPOSITORY", func(desc imageDesc) string { + if desc.Repository != "" { + return desc.Repository } return "" }}, - {"TAG", func(p pkg) string { - if t, ok := p.ref.(reference.Tagged); ok { - return t.Tag() + {"TAG", func(desc imageDesc) string { + if desc.Tag != "" { + return desc.Tag } return "" }}, } if options.digests { - columns = append(columns, imageListColumn{"DIGEST", func(p pkg) string { - if t, ok := p.ref.(reference.Digested); ok { - return t.Digest().String() + columns = append(columns, imageListColumn{"DIGEST", func(desc imageDesc) string { + if desc.Digest != "" { + return desc.Digest } return "" }}) } columns = append(columns, - imageListColumn{"APP IMAGE ID", func(p pkg) string { - id, err := getImageID(p) - if err != nil { - return "" - } - return id + imageListColumn{"APP IMAGE ID", func(desc imageDesc) string { + return desc.ID }}, - imageListColumn{"APP NAME", func(p pkg) string { - return p.bundle.Name + imageListColumn{"APP NAME", func(desc imageDesc) string { + return desc.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" + imageListColumn{"CREATED", func(desc imageDesc) string { + if desc.Created > 0 { + return units.HumanDuration(desc.Created) + " ago" } return "" }}, From 92bf8cc93e90a84d8a86408c897c61bf59418150 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 14 Nov 2019 10:59:47 +0100 Subject: [PATCH 3/6] simplification: remove intermediate pkg struct Signed-off-by: Nicolas De Loof --- internal/commands/image/list.go | 157 +++++++++++--------------------- 1 file changed, 52 insertions(+), 105 deletions(-) diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index 8c3e3031f..c33204c18 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -9,17 +9,16 @@ import ( "text/tabwriter" "time" - "github.com/docker/cli/templates" - "github.com/pkg/errors" - "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/config" + "github.com/docker/cli/templates" "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -29,11 +28,6 @@ type imageListOption struct { template string } -type imageListColumn struct { - header string - value func(desc imageDesc) string -} - func listCmd(dockerCli command.Cli) *cobra.Command { options := imageListOption{} cmd := &cobra.Command{ @@ -64,49 +58,35 @@ func listCmd(dockerCli command.Cli) *cobra.Command { } func runList(dockerCli command.Cli, options imageListOption, bundleStore store.BundleStore) error { - bundles, err := bundleStore.List() - if err != nil { - return err - } - - pkgs, err := getPackages(bundleStore, bundles) + images, err := getImageDescriptors(bundleStore) if err != nil { return err } if options.quiet { - return printImageIDs(dockerCli, pkgs) + return printImageIDs(dockerCli, images) } - return printImages(dockerCli, pkgs, options) + return printImages(dockerCli, images, options) } -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 + images[i] = getImageDesc(b, ref) } - - return packages, nil + return images, nil } -func printImages(dockerCli command.Cli, refs []pkg, options imageListOption) error { - w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) - - list := []imageDesc{} - for _, ref := range refs { - list = append(list, getImageDesc(ref)) - } - +func printImages(dockerCli command.Cli, list []imageDesc, options imageListOption) error { if options.template == "json" { bytes, err := json.MarshalIndent(list, "", " ") if err != nil { @@ -123,34 +103,29 @@ func printImages(dockerCli command.Cli, refs []pkg, options imageListOption) err return tmpl.Execute(dockerCli.Out(), list) } - listColumns := getImageListColumns(options) - printHeaders(w, listColumns) + w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) + printHeaders(w, options.digests) for _, desc := range list { - printValues(w, desc, listColumns) + desc.println(w, options.digests) } return w.Flush() } -func printImageIDs(dockerCli command.Cli, refs []pkg) error { +func printImageIDs(dockerCli command.Cli, refs []imageDesc) error { var buf bytes.Buffer - for _, ref := range refs { - id, err := getImageID(ref) - if err != nil { - return err - } - fmt.Fprintln(&buf, id) + fmt.Fprintln(&buf, ref.ID) } fmt.Fprint(dockerCli.Out(), buf.String()) return 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 } @@ -158,22 +133,15 @@ 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) +func printHeaders(w io.Writer, digests bool) { + headers := []string{"REPOSITORY", "TAG"} + if digests { + headers = append(headers, "DIGEST") } + headers = append(headers, "APP IMAGE ID", "APP NAME", "CREATED") fmt.Fprintln(w, strings.Join(headers, "\t")) } -func printValues(w io.Writer, desc imageDesc, listColumns []imageListColumn) { - var values []string - for _, column := range listColumns { - values = append(values, column.value(desc)) - } - fmt.Fprintln(w, strings.Join(values, "\t")) -} - type imageDesc struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -183,30 +151,30 @@ type imageDesc struct { Created time.Duration `json:"created,omitempty"` } -func getImageDesc(p pkg) imageDesc { +func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc { var id string - id, _ = getImageID(p) + id, _ = getImageID(bundle, ref) var repository string - if n, ok := p.ref.(reference.Named); ok { + if n, ok := ref.(reference.Named); ok { repository = reference.FamiliarName(n) } var tag string - if t, ok := p.ref.(reference.Tagged); ok { + if t, ok := ref.(reference.Tagged); ok { tag = t.Tag() } var digest string - if t, ok := p.ref.(reference.Digested); ok { + if t, ok := ref.(reference.Digested); ok { digest = t.Digest().String() } var created time.Duration - if payload, err := packager.CustomPayload(p.bundle.Bundle); err == nil { + if payload, err := packager.CustomPayload(bundle.Bundle); err == nil { if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok { created = time.Now().UTC().Sub(createdPayload.CreatedTime()) } } return imageDesc{ ID: id, - Name: p.bundle.Name, + Name: bundle.Name, Repository: repository, Tag: tag, Digest: digest, @@ -214,47 +182,26 @@ func getImageDesc(p pkg) imageDesc { } } -func getImageListColumns(options imageListOption) []imageListColumn { - columns := []imageListColumn{ - {"REPOSITORY", func(desc imageDesc) string { - if desc.Repository != "" { - return desc.Repository - } - return "" - }}, - {"TAG", func(desc imageDesc) string { - if desc.Tag != "" { - return desc.Tag - } - return "" - }}, +func (desc imageDesc) humanDuration() string { + if desc.Created > 0 { + return units.HumanDuration(desc.Created) + " ago" } - if options.digests { - columns = append(columns, imageListColumn{"DIGEST", func(desc imageDesc) string { - if desc.Digest != "" { - return desc.Digest - } - return "" - }}) + return "" +} + +func (desc imageDesc) println(w io.Writer, digests bool) { + values := []string{} + values = append(values, orNone(desc.Repository), orNone(desc.Tag)) + if digests { + values = append(values, orNone(desc.Digest)) } - columns = append(columns, - imageListColumn{"APP IMAGE ID", func(desc imageDesc) string { - return desc.ID - }}, - imageListColumn{"APP NAME", func(desc imageDesc) string { - return desc.Name - }}, - imageListColumn{"CREATED", func(desc imageDesc) string { - if desc.Created > 0 { - return units.HumanDuration(desc.Created) + " ago" - } - return "" - }}, - ) - return columns + values = append(values, desc.ID, desc.Name, desc.humanDuration()) + fmt.Fprintln(w, strings.Join(values, "\t")) } -type pkg struct { - ref reference.Reference - bundle *relocated.Bundle +func orNone(s string) string { + if s != "" { + return s + } + return "" } From 81921ff1ae35b0f51e8dbba5621197993e1eae1e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 15 Nov 2019 14:17:20 +0100 Subject: [PATCH 4/6] Use a formatter for app image ls Signed-off-by: Nicolas De Loof --- internal/commands/image/formatter.go | 123 +++++++++++++++++++++++++++ internal/commands/image/list.go | 111 +++++------------------- 2 files changed, 142 insertions(+), 92 deletions(-) create mode 100644 internal/commands/image/formatter.go diff --git a/internal/commands/image/formatter.go b/internal/commands/image/formatter.go new file mode 100644 index 000000000..57e910f05 --- /dev/null +++ b/internal/commands/image/formatter.go @@ -0,0 +1,123 @@ +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}}{{.Name}}\t\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t" + + imageIDHeader = "APP 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 +} + +// ImageWrite writes the formatter images using the ImageContext +func ImageWrite(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 { + 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 c33204c18..55b92c0ff 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -1,31 +1,24 @@ package image import ( - "bytes" - "encoding/json" - "fmt" - "io" - "strings" - "text/tabwriter" "time" + "github.com/docker/cli/cli/command/formatter" + "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/config" - "github.com/docker/cli/templates" "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" - "github.com/pkg/errors" "github.com/spf13/cobra" ) type imageListOption struct { - quiet bool - digests bool - template string + quiet bool + digests bool + format string } func listCmd(dockerCli command.Cli) *cobra.Command { @@ -51,7 +44,7 @@ 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.template, "format", "f", "", "Format the output using the given syntax or Go template") + 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 @@ -63,10 +56,12 @@ func runList(dockerCli command.Cli, options imageListOption, bundleStore store.B return err } - if options.quiet { - return printImageIDs(dockerCli, images) + ctx := formatter.Context{ + Output: dockerCli.Out(), + Format: NewImageFormat(options.format, options.quiet, options.digests), } - return printImages(dockerCli, images, options) + + return ImageWrite(ctx, images) } func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) { @@ -86,41 +81,6 @@ func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) { return images, nil } -func printImages(dockerCli command.Cli, list []imageDesc, options imageListOption) error { - if options.template == "json" { - bytes, err := json.MarshalIndent(list, "", " ") - if err != nil { - return errors.Errorf("Failed to marshall json: %s", err) - } - _, err = dockerCli.Out().Write(bytes) - return err - } - if options.template != "" { - tmpl, err := templates.Parse(options.template) - if err != nil { - return errors.Errorf("Template parsing error: %s", err) - } - return tmpl.Execute(dockerCli.Out(), list) - } - - w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0) - printHeaders(w, options.digests) - for _, desc := range list { - desc.println(w, options.digests) - } - - return w.Flush() -} - -func printImageIDs(dockerCli command.Cli, refs []imageDesc) error { - var buf bytes.Buffer - for _, ref := range refs { - fmt.Fprintln(&buf, ref.ID) - } - fmt.Fprint(dockerCli.Out(), buf.String()) - return nil -} - func getImageID(bundle *relocated.Bundle, ref reference.Reference) (string, error) { id, ok := ref.(store.ID) if !ok { @@ -133,22 +93,13 @@ func getImageID(bundle *relocated.Bundle, ref reference.Reference) (string, erro return stringid.TruncateID(id.String()), nil } -func printHeaders(w io.Writer, digests bool) { - headers := []string{"REPOSITORY", "TAG"} - if digests { - headers = append(headers, "DIGEST") - } - headers = append(headers, "APP IMAGE ID", "APP NAME", "CREATED") - 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.Duration `json:"created,omitempty"` + 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 getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc { @@ -166,10 +117,10 @@ func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc { if t, ok := ref.(reference.Digested); ok { digest = t.Digest().String() } - var created time.Duration + var created time.Time if payload, err := packager.CustomPayload(bundle.Bundle); err == nil { if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok { - created = time.Now().UTC().Sub(createdPayload.CreatedTime()) + created = createdPayload.CreatedTime() } } return imageDesc{ @@ -181,27 +132,3 @@ func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc { Created: created, } } - -func (desc imageDesc) humanDuration() string { - if desc.Created > 0 { - return units.HumanDuration(desc.Created) + " ago" - } - return "" -} - -func (desc imageDesc) println(w io.Writer, digests bool) { - values := []string{} - values = append(values, orNone(desc.Repository), orNone(desc.Tag)) - if digests { - values = append(values, orNone(desc.Digest)) - } - values = append(values, desc.ID, desc.Name, desc.humanDuration()) - fmt.Fprintln(w, strings.Join(values, "\t")) -} - -func orNone(s string) string { - if s != "" { - return s - } - return "" -} From 04a69f8bb642472a8502b1fd2bd967d19ea065c7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 19 Nov 2019 10:02:30 +0100 Subject: [PATCH 5/6] Get a happy-linter Signed-off-by: Nicolas De Loof --- Gopkg.lock | 2 ++ internal/commands/image/formatter.go | 4 ++-- internal/commands/image/list.go | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) 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/internal/commands/image/formatter.go b/internal/commands/image/formatter.go index 57e910f05..9863388b4 100644 --- a/internal/commands/image/formatter.go +++ b/internal/commands/image/formatter.go @@ -40,8 +40,8 @@ func NewImageFormat(source string, quiet bool, digest bool) formatter.Format { return format } -// ImageWrite writes the formatter images using the ImageContext -func ImageWrite(ctx formatter.Context, images []imageDesc) error { +// 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) } diff --git a/internal/commands/image/list.go b/internal/commands/image/list.go index 55b92c0ff..4ab51ad88 100644 --- a/internal/commands/image/list.go +++ b/internal/commands/image/list.go @@ -3,12 +3,11 @@ package image import ( "time" - "github.com/docker/cli/cli/command/formatter" - "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" @@ -61,7 +60,7 @@ func runList(dockerCli command.Cli, options imageListOption, bundleStore store.B Format: NewImageFormat(options.format, options.quiet, options.digests), } - return ImageWrite(ctx, images) + return Write(ctx, images) } func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) { From d5e1ff57bcbc014039e66d1955f6cbe8c1fff807 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 19 Nov 2019 13:47:39 +0100 Subject: [PATCH 6/6] table minwidth is 20 set by cli/command/formatter/formatter.go#postFromat Signed-off-by: Nicolas De Loof --- e2e/images_test.go | 87 ++++++++++++++-------------- internal/commands/image/formatter.go | 7 ++- internal/commands/image/list_test.go | 32 ++++++---- 3 files changed, 70 insertions(+), 56 deletions(-) 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 index 9863388b4..2a842cca3 100644 --- a/internal/commands/image/formatter.go +++ b/internal/commands/image/formatter.go @@ -10,9 +10,9 @@ import ( 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}}{{.Name}}\t\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 ID" + imageIDHeader = "APP IMAGE ID" repositoryHeader = "REPOSITORY" tagHeader = "TAG" digestHeader = "DIGEST" @@ -112,6 +112,9 @@ func (c *imageContext) Tag() string { } func (c *imageContext) Digest() string { + if c.i.Digest == "" { + return "" + } return c.i.Digest } 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}, }, }