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