1package formatter
2
3import (
4	"fmt"
5	"time"
6
7	"github.com/docker/distribution/reference"
8	"github.com/docker/docker/api/types"
9	"github.com/docker/docker/pkg/stringid"
10	units "github.com/docker/go-units"
11)
12
13const (
14	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
15	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
16
17	imageIDHeader    = "IMAGE ID"
18	repositoryHeader = "REPOSITORY"
19	tagHeader        = "TAG"
20	digestHeader     = "DIGEST"
21)
22
23// ImageContext contains image specific information required by the formatter, encapsulate a Context struct.
24type ImageContext struct {
25	Context
26	Digest bool
27}
28
29func isDangling(image types.ImageSummary) bool {
30	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
31}
32
33// NewImageFormat returns a format for rendering an ImageContext
34func NewImageFormat(source string, quiet bool, digest bool) Format {
35	switch source {
36	case TableFormatKey:
37		switch {
38		case quiet:
39			return DefaultQuietFormat
40		case digest:
41			return defaultImageTableFormatWithDigest
42		default:
43			return defaultImageTableFormat
44		}
45	case RawFormatKey:
46		switch {
47		case quiet:
48			return `image_id: {{.ID}}`
49		case digest:
50			return `repository: {{ .Repository }}
51tag: {{.Tag}}
52digest: {{.Digest}}
53image_id: {{.ID}}
54created_at: {{.CreatedAt}}
55virtual_size: {{.Size}}
56`
57		default:
58			return `repository: {{ .Repository }}
59tag: {{.Tag}}
60image_id: {{.ID}}
61created_at: {{.CreatedAt}}
62virtual_size: {{.Size}}
63`
64		}
65	}
66
67	format := Format(source)
68	if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
69		format += "\t{{.Digest}}"
70	}
71	return format
72}
73
74// ImageWrite writes the formatter images using the ImageContext
75func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
76	render := func(format func(subContext SubContext) error) error {
77		return imageFormat(ctx, images, format)
78	}
79	return ctx.Write(newImageContext(), render)
80}
81
82// needDigest determines whether the image digest should be ignored or not when writing image context
83func needDigest(ctx ImageContext) bool {
84	return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
85}
86
87func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext SubContext) error) error {
88	for _, image := range images {
89		formatted := []*imageContext{}
90		if isDangling(image) {
91			formatted = append(formatted, &imageContext{
92				trunc:  ctx.Trunc,
93				i:      image,
94				repo:   "<none>",
95				tag:    "<none>",
96				digest: "<none>",
97			})
98		} else {
99			formatted = imageFormatTaggedAndDigest(ctx, image)
100		}
101		for _, imageCtx := range formatted {
102			if err := format(imageCtx); err != nil {
103				return err
104			}
105		}
106	}
107	return nil
108}
109
110func imageFormatTaggedAndDigest(ctx ImageContext, image types.ImageSummary) []*imageContext {
111	repoTags := map[string][]string{}
112	repoDigests := map[string][]string{}
113	images := []*imageContext{}
114
115	for _, refString := range image.RepoTags {
116		ref, err := reference.ParseNormalizedNamed(refString)
117		if err != nil {
118			continue
119		}
120		if nt, ok := ref.(reference.NamedTagged); ok {
121			familiarRef := reference.FamiliarName(ref)
122			repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
123		}
124	}
125	for _, refString := range image.RepoDigests {
126		ref, err := reference.ParseNormalizedNamed(refString)
127		if err != nil {
128			continue
129		}
130		if c, ok := ref.(reference.Canonical); ok {
131			familiarRef := reference.FamiliarName(ref)
132			repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
133		}
134	}
135
136	addImage := func(repo, tag, digest string) {
137		image := &imageContext{
138			trunc:  ctx.Trunc,
139			i:      image,
140			repo:   repo,
141			tag:    tag,
142			digest: digest,
143		}
144		images = append(images, image)
145	}
146
147	for repo, tags := range repoTags {
148		digests := repoDigests[repo]
149
150		// Do not display digests as their own row
151		delete(repoDigests, repo)
152
153		if !needDigest(ctx) {
154			// Ignore digest references, just show tag once
155			digests = nil
156		}
157
158		for _, tag := range tags {
159			if len(digests) == 0 {
160				addImage(repo, tag, "<none>")
161				continue
162			}
163			// Display the digests for each tag
164			for _, dgst := range digests {
165				addImage(repo, tag, dgst)
166			}
167
168		}
169	}
170
171	// Show rows for remaining digest only references
172	for repo, digests := range repoDigests {
173		// If digests are displayed, show row per digest
174		if ctx.Digest {
175			for _, dgst := range digests {
176				addImage(repo, "<none>", dgst)
177			}
178		} else {
179			addImage(repo, "<none>", "")
180
181		}
182	}
183	return images
184}
185
186type imageContext struct {
187	HeaderContext
188	trunc  bool
189	i      types.ImageSummary
190	repo   string
191	tag    string
192	digest string
193}
194
195func newImageContext() *imageContext {
196	imageCtx := imageContext{}
197	imageCtx.Header = SubHeaderContext{
198		"ID":           imageIDHeader,
199		"Repository":   repositoryHeader,
200		"Tag":          tagHeader,
201		"Digest":       digestHeader,
202		"CreatedSince": CreatedSinceHeader,
203		"CreatedAt":    CreatedAtHeader,
204		"Size":         SizeHeader,
205		"Containers":   containersHeader,
206		"VirtualSize":  SizeHeader,
207		"SharedSize":   sharedSizeHeader,
208		"UniqueSize":   uniqueSizeHeader,
209	}
210	return &imageCtx
211}
212
213func (c *imageContext) MarshalJSON() ([]byte, error) {
214	return MarshalJSON(c)
215}
216
217func (c *imageContext) ID() string {
218	if c.trunc {
219		return stringid.TruncateID(c.i.ID)
220	}
221	return c.i.ID
222}
223
224func (c *imageContext) Repository() string {
225	return c.repo
226}
227
228func (c *imageContext) Tag() string {
229	return c.tag
230}
231
232func (c *imageContext) Digest() string {
233	return c.digest
234}
235
236func (c *imageContext) CreatedSince() string {
237	createdAt := time.Unix(c.i.Created, 0)
238
239	if createdAt.IsZero() {
240		return ""
241	}
242
243	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
244}
245
246func (c *imageContext) CreatedAt() string {
247	return time.Unix(c.i.Created, 0).String()
248}
249
250func (c *imageContext) Size() string {
251	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
252}
253
254func (c *imageContext) Containers() string {
255	if c.i.Containers == -1 {
256		return "N/A"
257	}
258	return fmt.Sprintf("%d", c.i.Containers)
259}
260
261func (c *imageContext) VirtualSize() string {
262	return units.HumanSize(float64(c.i.VirtualSize))
263}
264
265func (c *imageContext) SharedSize() string {
266	if c.i.SharedSize == -1 {
267		return "N/A"
268	}
269	return units.HumanSize(float64(c.i.SharedSize))
270}
271
272func (c *imageContext) UniqueSize() string {
273	if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
274		return "N/A"
275	}
276	return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize))
277}
278