github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/formatter/image.go (about) 1 package formatter 2 3 import ( 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 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(image types.ImageSummary) bool { 30 if len(image.RepoTags) == 0 && len(image.RepoDigests) == 0 { 31 return true 32 } 33 return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.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 []types.ImageSummary) 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 []types.ImageSummary, format func(subContext SubContext) error) error { 91 for _, image := range images { 92 formatted := []*imageContext{} 93 if isDangling(image) { 94 formatted = append(formatted, &imageContext{ 95 trunc: ctx.Trunc, 96 i: image, 97 repo: "<none>", 98 tag: "<none>", 99 digest: "<none>", 100 }) 101 } else { 102 formatted = imageFormatTaggedAndDigest(ctx, image) 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, image types.ImageSummary) []*imageContext { 114 repoTags := map[string][]string{} 115 repoDigests := map[string][]string{} 116 images := []*imageContext{} 117 118 for _, refString := range image.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 image.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 image := &imageContext{ 141 trunc: ctx.Trunc, 142 i: image, 143 repo: repo, 144 tag: tag, 145 digest: digest, 146 } 147 images = append(images, image) 148 } 149 150 for repo, tags := range repoTags { 151 digests := repoDigests[repo] 152 153 // Do not display digests as their own row 154 delete(repoDigests, repo) 155 156 if !needDigest(ctx) { 157 // Ignore digest references, just show tag once 158 digests = nil 159 } 160 161 for _, tag := range tags { 162 if len(digests) == 0 { 163 addImage(repo, tag, "<none>") 164 continue 165 } 166 // Display the digests for each tag 167 for _, dgst := range digests { 168 addImage(repo, tag, dgst) 169 } 170 171 } 172 } 173 174 // Show rows for remaining digest only references 175 for repo, digests := range repoDigests { 176 // If digests are displayed, show row per digest 177 if ctx.Digest { 178 for _, dgst := range digests { 179 addImage(repo, "<none>", dgst) 180 } 181 } else { 182 addImage(repo, "<none>", "") 183 } 184 } 185 return images 186 } 187 188 type imageContext struct { 189 HeaderContext 190 trunc bool 191 i types.ImageSummary 192 repo string 193 tag string 194 digest string 195 } 196 197 func newImageContext() *imageContext { 198 imageCtx := imageContext{} 199 imageCtx.Header = SubHeaderContext{ 200 "ID": imageIDHeader, 201 "Repository": repositoryHeader, 202 "Tag": tagHeader, 203 "Digest": digestHeader, 204 "CreatedSince": CreatedSinceHeader, 205 "CreatedAt": CreatedAtHeader, 206 "Size": SizeHeader, 207 "Containers": containersHeader, 208 "VirtualSize": SizeHeader, 209 "SharedSize": sharedSizeHeader, 210 "UniqueSize": uniqueSizeHeader, 211 } 212 return &imageCtx 213 } 214 215 func (c *imageContext) MarshalJSON() ([]byte, error) { 216 return MarshalJSON(c) 217 } 218 219 func (c *imageContext) ID() string { 220 if c.trunc { 221 return stringid.TruncateID(c.i.ID) 222 } 223 return c.i.ID 224 } 225 226 func (c *imageContext) Repository() string { 227 return c.repo 228 } 229 230 func (c *imageContext) Tag() string { 231 return c.tag 232 } 233 234 func (c *imageContext) Digest() string { 235 return c.digest 236 } 237 238 func (c *imageContext) CreatedSince() string { 239 createdAt := time.Unix(c.i.Created, 0) 240 241 if createdAt.IsZero() { 242 return "" 243 } 244 245 return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" 246 } 247 248 func (c *imageContext) CreatedAt() string { 249 return time.Unix(c.i.Created, 0).String() 250 } 251 252 func (c *imageContext) Size() string { 253 return units.HumanSizeWithPrecision(float64(c.i.Size), 3) 254 } 255 256 func (c *imageContext) Containers() string { 257 if c.i.Containers == -1 { 258 return "N/A" 259 } 260 return fmt.Sprintf("%d", c.i.Containers) 261 } 262 263 func (c *imageContext) VirtualSize() string { 264 return units.HumanSize(float64(c.i.VirtualSize)) 265 } 266 267 func (c *imageContext) SharedSize() string { 268 if c.i.SharedSize == -1 { 269 return "N/A" 270 } 271 return units.HumanSize(float64(c.i.SharedSize)) 272 } 273 274 func (c *imageContext) UniqueSize() string { 275 if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { 276 return "N/A" 277 } 278 return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize)) 279 }