github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podman/images.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "sort" 8 "strings" 9 "time" 10 "unicode" 11 12 "github.com/containers/buildah/pkg/formats" 13 "github.com/containers/libpod/cmd/podman/cliconfig" 14 "github.com/containers/libpod/libpod/image" 15 "github.com/containers/libpod/pkg/adapter" 16 units "github.com/docker/go-units" 17 digest "github.com/opencontainers/go-digest" 18 "github.com/pkg/errors" 19 "github.com/sirupsen/logrus" 20 "github.com/spf13/cobra" 21 ) 22 23 type imagesTemplateParams struct { 24 Repository string 25 Tag string 26 ID string 27 Digest digest.Digest 28 Digests []digest.Digest 29 CreatedAt time.Time 30 CreatedSince string 31 Size string 32 ReadOnly bool 33 History string 34 } 35 36 type imagesJSONParams struct { 37 ID string `json:"ID"` 38 Name []string `json:"Names"` 39 Created string `json:"Created"` 40 Digest digest.Digest `json:"Digest"` 41 Digests []digest.Digest `json:"Digests"` 42 CreatedAt time.Time `json:"CreatedAt"` 43 Size *uint64 `json:"Size"` 44 ReadOnly bool `json:"ReadOnly"` 45 History []string `json:"History"` 46 } 47 48 type imagesOptions struct { 49 quiet bool 50 noHeading bool 51 noTrunc bool 52 digests bool 53 format string 54 outputformat string 55 sort string 56 all bool 57 history bool 58 } 59 60 // Type declaration and functions for sorting the images output 61 type imagesSorted []imagesTemplateParams 62 63 func (a imagesSorted) Len() int { return len(a) } 64 func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 65 66 type imagesSortedCreated struct{ imagesSorted } 67 68 func (a imagesSortedCreated) Less(i, j int) bool { 69 return a.imagesSorted[i].CreatedAt.After(a.imagesSorted[j].CreatedAt) 70 } 71 72 type imagesSortedID struct{ imagesSorted } 73 74 func (a imagesSortedID) Less(i, j int) bool { return a.imagesSorted[i].ID < a.imagesSorted[j].ID } 75 76 type imagesSortedTag struct{ imagesSorted } 77 78 func (a imagesSortedTag) Less(i, j int) bool { return a.imagesSorted[i].Tag < a.imagesSorted[j].Tag } 79 80 type imagesSortedRepository struct{ imagesSorted } 81 82 func (a imagesSortedRepository) Less(i, j int) bool { 83 return a.imagesSorted[i].Repository < a.imagesSorted[j].Repository 84 } 85 86 type imagesSortedSize struct{ imagesSorted } 87 88 func (a imagesSortedSize) Less(i, j int) bool { 89 size1, _ := units.FromHumanSize(a.imagesSorted[i].Size) 90 size2, _ := units.FromHumanSize(a.imagesSorted[j].Size) 91 return size1 < size2 92 } 93 94 var ( 95 imagesCommand cliconfig.ImagesValues 96 imagesDescription = "Lists images previously pulled to the system or created on the system." 97 98 _imagesCommand = cobra.Command{ 99 Use: "images [flags] [IMAGE]", 100 Short: "List images in local storage", 101 Long: imagesDescription, 102 RunE: func(cmd *cobra.Command, args []string) error { 103 imagesCommand.InputArgs = args 104 imagesCommand.GlobalFlags = MainGlobalOpts 105 imagesCommand.Remote = remoteclient 106 return imagesCmd(&imagesCommand) 107 }, 108 Example: `podman images --format json 109 podman images --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}" 110 podman images --filter dangling=true`, 111 } 112 ) 113 114 func imagesInit(command *cliconfig.ImagesValues) { 115 command.SetHelpTemplate(HelpTemplate()) 116 command.SetUsageTemplate(UsageTemplate()) 117 118 flags := command.Flags() 119 flags.BoolVarP(&command.All, "all", "a", false, "Show all images (default hides intermediate images)") 120 flags.BoolVar(&command.Digests, "digests", false, "Show digests") 121 flags.StringSliceVarP(&command.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") 122 flags.StringVar(&command.Format, "format", "", "Change the output format to JSON or a Go template") 123 flags.BoolVarP(&command.Noheading, "noheading", "n", false, "Do not print column headings") 124 // TODO Need to learn how to deal with second name being a string instead of a char. 125 // This needs to be "no-trunc, notruncate" 126 flags.BoolVar(&command.NoTrunc, "no-trunc", false, "Do not truncate output") 127 flags.BoolVarP(&command.Quiet, "quiet", "q", false, "Display only image IDs") 128 flags.StringVar(&command.Sort, "sort", "created", "Sort by created, id, repository, size, or tag") 129 flags.BoolVarP(&command.History, "history", "", false, "Display the image name history") 130 131 } 132 133 func init() { 134 imagesCommand.Command = &_imagesCommand 135 imagesInit(&imagesCommand) 136 } 137 138 func imagesCmd(c *cliconfig.ImagesValues) error { 139 var ( 140 image string 141 ) 142 143 ctx := getContext() 144 runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand) 145 if err != nil { 146 return errors.Wrapf(err, "Could not get runtime") 147 } 148 defer runtime.DeferredShutdown(false) 149 if len(c.InputArgs) == 1 { 150 image = c.InputArgs[0] 151 } 152 if len(c.InputArgs) > 1 { 153 return errors.New("'podman images' requires at most 1 argument") 154 } 155 if len(c.Filter) > 0 && image != "" { 156 return errors.New("can not specify an image and a filter") 157 } 158 filters := c.Filter 159 if len(filters) < 1 && len(image) > 0 { 160 filters = append(filters, fmt.Sprintf("reference=%s", image)) 161 } 162 163 var sortValues = map[string]bool{ 164 "created": true, 165 "id": true, 166 "repository": true, 167 "size": true, 168 "tag": true, 169 } 170 if !sortValues[c.Sort] { 171 keys := make([]string, 0, len(sortValues)) 172 for k := range sortValues { 173 keys = append(keys, k) 174 } 175 return errors.Errorf("invalid sort value %q, required values: %s", c.Sort, strings.Join(keys, ", ")) 176 } 177 178 opts := imagesOptions{ 179 quiet: c.Quiet, 180 noHeading: c.Noheading, 181 noTrunc: c.NoTrunc, 182 digests: c.Digests, 183 format: c.Format, 184 sort: c.Sort, 185 all: c.All, 186 history: c.History, 187 } 188 189 outputformat := opts.setOutputFormat() 190 // These fields were renamed, so we need to provide backward compat for 191 // the old names. 192 if strings.Contains(outputformat, "{{.Created}}") { 193 outputformat = strings.Replace(outputformat, "{{.Created}}", "{{.CreatedSince}}", -1) 194 } 195 if strings.Contains(outputformat, "{{.CreatedTime}}") { 196 outputformat = strings.Replace(outputformat, "{{.CreatedTime}}", "{{.CreatedAt}}", -1) 197 } 198 opts.outputformat = outputformat 199 200 filteredImages, err := runtime.GetFilteredImages(filters, false) 201 if err != nil { 202 return errors.Wrapf(err, "unable to get images") 203 } 204 205 for _, image := range filteredImages { 206 if image.IsReadOnly() { 207 opts.outputformat += "{{.ReadOnly}}\t" 208 break 209 } 210 } 211 return generateImagesOutput(ctx, filteredImages, opts) 212 } 213 214 func (i imagesOptions) setOutputFormat() string { 215 if i.format != "" { 216 // "\t" from the command line is not being recognized as a tab 217 // replacing the string "\t" to a tab character if the user passes in "\t" 218 return strings.Replace(i.format, `\t`, "\t", -1) 219 } 220 if i.quiet { 221 return formats.IDString 222 } 223 format := "table {{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t" 224 if i.noHeading { 225 format = "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t" 226 } 227 if i.digests { 228 format += "{{.Digest}}\t" 229 } 230 format += "{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t" 231 if i.history { 232 format += "{{if .History}}{{.History}}{{else}}<none>{{end}}\t" 233 } 234 return format 235 } 236 237 // imagesToGeneric creates an empty array of interfaces for output 238 func imagesToGeneric(templParams []imagesTemplateParams, jsonParams []imagesJSONParams) []interface{} { 239 genericParams := []interface{}{} 240 if len(templParams) > 0 { 241 for _, v := range templParams { 242 genericParams = append(genericParams, interface{}(v)) 243 } 244 return genericParams 245 } 246 for _, v := range jsonParams { 247 genericParams = append(genericParams, interface{}(v)) 248 } 249 return genericParams 250 } 251 252 func sortImagesOutput(sortBy string, imagesOutput imagesSorted) imagesSorted { 253 switch sortBy { 254 case "id": 255 sort.Sort(imagesSortedID{imagesOutput}) 256 case "size": 257 sort.Sort(imagesSortedSize{imagesOutput}) 258 case "tag": 259 sort.Sort(imagesSortedTag{imagesOutput}) 260 case "repository": 261 sort.Sort(imagesSortedRepository{imagesOutput}) 262 default: 263 // default is created time 264 sort.Sort(imagesSortedCreated{imagesOutput}) 265 } 266 return imagesOutput 267 } 268 269 // getImagesTemplateOutput returns the images information to be printed in human readable format 270 func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) imagesSorted { 271 var imagesOutput imagesSorted 272 for _, img := range images { 273 // If all is false and the image doesn't have a name, check to see if the top layer of the image is a parent 274 // to another image's top layer. If it is, then it is an intermediate image so don't print out if the --all flag 275 // is not set. 276 isParent, err := img.IsParent(ctx) 277 if err != nil { 278 logrus.Errorf("error checking if image is a parent %q: %v", img.ID(), err) 279 } 280 if !opts.all && len(img.Names()) == 0 && isParent { 281 continue 282 } 283 createdTime := img.Created() 284 285 imageID := "sha256:" + img.ID() 286 if !opts.noTrunc { 287 imageID = shortID(img.ID()) 288 } 289 290 // get all specified repo:tag and repo@digest pairs and print them separately 291 repopairs, err := image.ReposToMap(img.Names()) 292 if err != nil { 293 logrus.Errorf("error finding tag/digest for %s", img.ID()) 294 } 295 outer: 296 for repo, tags := range repopairs { 297 for _, tag := range tags { 298 size, err := img.Size(ctx) 299 var sizeStr string 300 if err != nil { 301 sizeStr = err.Error() 302 } else { 303 sizeStr = units.HumanSizeWithPrecision(float64(*size), 3) 304 lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber) 305 sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:] 306 } 307 var imageDigest digest.Digest 308 if len(tag) == 71 && strings.HasPrefix(tag, "sha256:") { 309 imageDigest = digest.Digest(tag) 310 tag = "" 311 } else if img.Digest() != "" { 312 imageDigest = img.Digest() 313 } 314 params := imagesTemplateParams{ 315 Repository: repo, 316 Tag: tag, 317 ID: imageID, 318 Digest: imageDigest, 319 Digests: img.Digests(), 320 CreatedAt: createdTime, 321 CreatedSince: units.HumanDuration(time.Since(createdTime)) + " ago", 322 Size: sizeStr, 323 ReadOnly: img.IsReadOnly(), 324 History: strings.Join(img.NamesHistory(), ", "), 325 } 326 imagesOutput = append(imagesOutput, params) 327 if opts.quiet { // Show only one image ID when quiet 328 break outer 329 } 330 } 331 } 332 } 333 334 // Sort images by created time 335 sortImagesOutput(opts.sort, imagesOutput) 336 return imagesOutput 337 } 338 339 // getImagesJSONOutput returns the images information in its raw form 340 func getImagesJSONOutput(ctx context.Context, images []*adapter.ContainerImage) []imagesJSONParams { 341 imagesOutput := []imagesJSONParams{} 342 for _, img := range images { 343 size, err := img.Size(ctx) 344 if err != nil { 345 size = nil 346 } 347 params := imagesJSONParams{ 348 ID: img.ID(), 349 Name: img.Names(), 350 Digest: img.Digest(), 351 Digests: img.Digests(), 352 Created: units.HumanDuration(time.Since(img.Created())) + " ago", 353 CreatedAt: img.Created(), 354 Size: size, 355 ReadOnly: img.IsReadOnly(), 356 History: img.NamesHistory(), 357 } 358 imagesOutput = append(imagesOutput, params) 359 } 360 return imagesOutput 361 } 362 363 // generateImagesOutput generates the images based on the format provided 364 365 func generateImagesOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) error { 366 templateMap := GenImageOutputMap() 367 var out formats.Writer 368 369 switch opts.format { 370 case formats.JSONString: 371 imagesOutput := getImagesJSONOutput(ctx, images) 372 out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)} 373 default: 374 imagesOutput := getImagesTemplateOutput(ctx, images, opts) 375 out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.outputformat, Fields: templateMap} 376 } 377 return out.Out() 378 } 379 380 // GenImageOutputMap generates the map used for outputting the images header 381 // without requiring a populated image. This replaces the previous HeaderMap 382 // call. 383 func GenImageOutputMap() map[string]string { 384 io := imagesTemplateParams{} 385 v := reflect.Indirect(reflect.ValueOf(io)) 386 values := make(map[string]string) 387 388 for i := 0; i < v.NumField(); i++ { 389 key := v.Type().Field(i).Name 390 value := key 391 if value == "ID" { 392 value = "Image" + value 393 } 394 395 if value == "ReadOnly" { 396 values[key] = "R/O" 397 continue 398 } 399 if value == "CreatedSince" { 400 value = "created" 401 } 402 values[key] = strings.ToUpper(splitCamelCase(value)) 403 } 404 return values 405 }