Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 81921ff

Browse files
committed
Use a formatter for app image ls
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 92bf8cc commit 81921ff

File tree

2 files changed

+142
-92
lines changed

2 files changed

+142
-92
lines changed

internal/commands/image/formatter.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package image
2+
3+
import (
4+
"time"
5+
6+
"github.com/docker/cli/cli/command/formatter"
7+
"github.com/docker/docker/pkg/stringid"
8+
"github.com/docker/go-units"
9+
)
10+
11+
const (
12+
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Name}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t"
13+
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}{{.Name}}\t\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t"
14+
15+
imageIDHeader = "APP ID"
16+
repositoryHeader = "REPOSITORY"
17+
tagHeader = "TAG"
18+
digestHeader = "DIGEST"
19+
imageNameHeader = "APP NAME"
20+
)
21+
22+
// NewImageFormat returns a format for rendering an ImageContext
23+
func NewImageFormat(source string, quiet bool, digest bool) formatter.Format {
24+
switch source {
25+
case formatter.TableFormatKey:
26+
switch {
27+
case quiet:
28+
return formatter.DefaultQuietFormat
29+
case digest:
30+
return defaultImageTableFormatWithDigest
31+
default:
32+
return defaultImageTableFormat
33+
}
34+
}
35+
36+
format := formatter.Format(source)
37+
if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
38+
format += "\t{{.Digest}}"
39+
}
40+
return format
41+
}
42+
43+
// ImageWrite writes the formatter images using the ImageContext
44+
func ImageWrite(ctx formatter.Context, images []imageDesc) error {
45+
render := func(format func(subContext formatter.SubContext) error) error {
46+
return imageFormat(ctx, images, format)
47+
}
48+
return ctx.Write(newImageContext(), render)
49+
}
50+
51+
func imageFormat(ctx formatter.Context, images []imageDesc, format func(subContext formatter.SubContext) error) error {
52+
for _, image := range images {
53+
img := &imageContext{
54+
trunc: ctx.Trunc,
55+
i: image}
56+
if err := format(img); err != nil {
57+
return err
58+
}
59+
}
60+
return nil
61+
}
62+
63+
type imageContext struct {
64+
formatter.HeaderContext
65+
trunc bool
66+
i imageDesc
67+
}
68+
69+
func newImageContext() *imageContext {
70+
imageCtx := imageContext{}
71+
imageCtx.Header = formatter.SubHeaderContext{
72+
"ID": imageIDHeader,
73+
"Name": imageNameHeader,
74+
"Repository": repositoryHeader,
75+
"Tag": tagHeader,
76+
"Digest": digestHeader,
77+
"CreatedSince": formatter.CreatedSinceHeader,
78+
}
79+
return &imageCtx
80+
}
81+
82+
func (c *imageContext) MarshalJSON() ([]byte, error) {
83+
return formatter.MarshalJSON(c)
84+
}
85+
86+
func (c *imageContext) ID() string {
87+
if c.trunc {
88+
return stringid.TruncateID(c.i.ID)
89+
}
90+
return c.i.ID
91+
}
92+
93+
func (c *imageContext) Name() string {
94+
if c.i.Name == "" {
95+
return "<none>"
96+
}
97+
return c.i.Name
98+
}
99+
100+
func (c *imageContext) Repository() string {
101+
if c.i.Repository == "" {
102+
return "<none>"
103+
}
104+
return c.i.Repository
105+
}
106+
107+
func (c *imageContext) Tag() string {
108+
if c.i.Tag == "" {
109+
return "<none>"
110+
}
111+
return c.i.Tag
112+
}
113+
114+
func (c *imageContext) Digest() string {
115+
return c.i.Digest
116+
}
117+
118+
func (c *imageContext) CreatedSince() string {
119+
if c.i.Created.IsZero() {
120+
return ""
121+
}
122+
return units.HumanDuration(time.Now().UTC().Sub(c.i.Created)) + " ago"
123+
}

internal/commands/image/list.go

Lines changed: 19 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
package image
22

33
import (
4-
"bytes"
5-
"encoding/json"
6-
"fmt"
7-
"io"
8-
"strings"
9-
"text/tabwriter"
104
"time"
115

6+
"github.com/docker/cli/cli/command/formatter"
7+
128
"github.com/docker/app/internal/packager"
139
"github.com/docker/app/internal/relocated"
1410
"github.com/docker/app/internal/store"
1511
"github.com/docker/cli/cli/command"
1612
"github.com/docker/cli/cli/config"
17-
"github.com/docker/cli/templates"
1813
"github.com/docker/distribution/reference"
1914
"github.com/docker/docker/pkg/stringid"
20-
units "github.com/docker/go-units"
21-
"github.com/pkg/errors"
2215
"github.com/spf13/cobra"
2316
)
2417

2518
type imageListOption struct {
26-
quiet bool
27-
digests bool
28-
template string
19+
quiet bool
20+
digests bool
21+
format string
2922
}
3023

3124
func listCmd(dockerCli command.Cli) *cobra.Command {
@@ -51,7 +44,7 @@ func listCmd(dockerCli command.Cli) *cobra.Command {
5144
flags := cmd.Flags()
5245
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show numeric IDs")
5346
flags.BoolVarP(&options.digests, "digests", "", false, "Show image digests")
54-
cmd.Flags().StringVarP(&options.template, "format", "f", "", "Format the output using the given syntax or Go template")
47+
cmd.Flags().StringVarP(&options.format, "format", "f", "table", "Format the output using the given syntax or Go template")
5548
cmd.Flags().SetAnnotation("format", "experimentalCLI", []string{"true"}) //nolint:errcheck
5649

5750
return cmd
@@ -63,10 +56,12 @@ func runList(dockerCli command.Cli, options imageListOption, bundleStore store.B
6356
return err
6457
}
6558

66-
if options.quiet {
67-
return printImageIDs(dockerCli, images)
59+
ctx := formatter.Context{
60+
Output: dockerCli.Out(),
61+
Format: NewImageFormat(options.format, options.quiet, options.digests),
6862
}
69-
return printImages(dockerCli, images, options)
63+
64+
return ImageWrite(ctx, images)
7065
}
7166

7267
func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) {
@@ -86,41 +81,6 @@ func getImageDescriptors(bundleStore store.BundleStore) ([]imageDesc, error) {
8681
return images, nil
8782
}
8883

89-
func printImages(dockerCli command.Cli, list []imageDesc, options imageListOption) error {
90-
if options.template == "json" {
91-
bytes, err := json.MarshalIndent(list, "", " ")
92-
if err != nil {
93-
return errors.Errorf("Failed to marshall json: %s", err)
94-
}
95-
_, err = dockerCli.Out().Write(bytes)
96-
return err
97-
}
98-
if options.template != "" {
99-
tmpl, err := templates.Parse(options.template)
100-
if err != nil {
101-
return errors.Errorf("Template parsing error: %s", err)
102-
}
103-
return tmpl.Execute(dockerCli.Out(), list)
104-
}
105-
106-
w := tabwriter.NewWriter(dockerCli.Out(), 0, 0, 1, ' ', 0)
107-
printHeaders(w, options.digests)
108-
for _, desc := range list {
109-
desc.println(w, options.digests)
110-
}
111-
112-
return w.Flush()
113-
}
114-
115-
func printImageIDs(dockerCli command.Cli, refs []imageDesc) error {
116-
var buf bytes.Buffer
117-
for _, ref := range refs {
118-
fmt.Fprintln(&buf, ref.ID)
119-
}
120-
fmt.Fprint(dockerCli.Out(), buf.String())
121-
return nil
122-
}
123-
12484
func getImageID(bundle *relocated.Bundle, ref reference.Reference) (string, error) {
12585
id, ok := ref.(store.ID)
12686
if !ok {
@@ -133,22 +93,13 @@ func getImageID(bundle *relocated.Bundle, ref reference.Reference) (string, erro
13393
return stringid.TruncateID(id.String()), nil
13494
}
13595

136-
func printHeaders(w io.Writer, digests bool) {
137-
headers := []string{"REPOSITORY", "TAG"}
138-
if digests {
139-
headers = append(headers, "DIGEST")
140-
}
141-
headers = append(headers, "APP IMAGE ID", "APP NAME", "CREATED")
142-
fmt.Fprintln(w, strings.Join(headers, "\t"))
143-
}
144-
14596
type imageDesc struct {
146-
ID string `json:"id,omitempty"`
147-
Name string `json:"name,omitempty"`
148-
Repository string `json:"repository,omitempty"`
149-
Tag string `json:"tag,omitempty"`
150-
Digest string `json:"digest,omitempty"`
151-
Created time.Duration `json:"created,omitempty"`
97+
ID string `json:"id,omitempty"`
98+
Name string `json:"name,omitempty"`
99+
Repository string `json:"repository,omitempty"`
100+
Tag string `json:"tag,omitempty"`
101+
Digest string `json:"digest,omitempty"`
102+
Created time.Time `json:"created,omitempty"`
152103
}
153104

154105
func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc {
@@ -166,10 +117,10 @@ func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc {
166117
if t, ok := ref.(reference.Digested); ok {
167118
digest = t.Digest().String()
168119
}
169-
var created time.Duration
120+
var created time.Time
170121
if payload, err := packager.CustomPayload(bundle.Bundle); err == nil {
171122
if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok {
172-
created = time.Now().UTC().Sub(createdPayload.CreatedTime())
123+
created = createdPayload.CreatedTime()
173124
}
174125
}
175126
return imageDesc{
@@ -181,27 +132,3 @@ func getImageDesc(bundle *relocated.Bundle, ref reference.Reference) imageDesc {
181132
Created: created,
182133
}
183134
}
184-
185-
func (desc imageDesc) humanDuration() string {
186-
if desc.Created > 0 {
187-
return units.HumanDuration(desc.Created) + " ago"
188-
}
189-
return ""
190-
}
191-
192-
func (desc imageDesc) println(w io.Writer, digests bool) {
193-
values := []string{}
194-
values = append(values, orNone(desc.Repository), orNone(desc.Tag))
195-
if digests {
196-
values = append(values, orNone(desc.Digest))
197-
}
198-
values = append(values, desc.ID, desc.Name, desc.humanDuration())
199-
fmt.Fprintln(w, strings.Join(values, "\t"))
200-
}
201-
202-
func orNone(s string) string {
203-
if s != "" {
204-
return s
205-
}
206-
return "<none>"
207-
}

0 commit comments

Comments
 (0)