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