github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_list.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "encoding/json" 6 "runtime" 7 "sort" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/containerd/containerd/content" 13 cerrdefs "github.com/containerd/containerd/errdefs" 14 "github.com/containerd/containerd/images" 15 "github.com/containerd/containerd/labels" 16 "github.com/containerd/containerd/platforms" 17 cplatforms "github.com/containerd/containerd/platforms" 18 "github.com/containerd/containerd/snapshots" 19 "github.com/containerd/log" 20 "github.com/distribution/reference" 21 "github.com/docker/docker/api/types/backend" 22 "github.com/docker/docker/api/types/filters" 23 imagetypes "github.com/docker/docker/api/types/image" 24 timetypes "github.com/docker/docker/api/types/time" 25 "github.com/docker/docker/container" 26 "github.com/docker/docker/errdefs" 27 dockerspec "github.com/moby/docker-image-spec/specs-go/v1" 28 "github.com/opencontainers/go-digest" 29 "github.com/opencontainers/image-spec/identity" 30 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 31 "github.com/pkg/errors" 32 "golang.org/x/sync/errgroup" 33 ) 34 35 // Subset of ocispec.Image that only contains Labels 36 type configLabels struct { 37 // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. 38 Created *time.Time `json:"created,omitempty"` 39 40 Config struct { 41 Labels map[string]string `json:"Labels,omitempty"` 42 } `json:"config,omitempty"` 43 } 44 45 var acceptedImageFilterTags = map[string]bool{ 46 "dangling": true, 47 "label": true, 48 "label!": true, 49 "before": true, 50 "since": true, 51 "reference": true, 52 "until": true, 53 } 54 55 // byCreated is a temporary type used to sort a list of images by creation 56 // time. 57 type byCreated []*imagetypes.Summary 58 59 func (r byCreated) Len() int { return len(r) } 60 func (r byCreated) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 61 func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created } 62 63 // Images returns a filtered list of images. 64 // 65 // TODO(thaJeztah): verify behavior of `RepoDigests` and `RepoTags` for images without (untagged) or multiple tags; see https://github.com/moby/moby/issues/43861 66 // TODO(thaJeztah): verify "Size" vs "VirtualSize" in images; see https://github.com/moby/moby/issues/43862 67 func (i *ImageService) Images(ctx context.Context, opts imagetypes.ListOptions) ([]*imagetypes.Summary, error) { 68 if err := opts.Filters.Validate(acceptedImageFilterTags); err != nil { 69 return nil, err 70 } 71 72 filter, err := i.setupFilters(ctx, opts.Filters) 73 if err != nil { 74 return nil, err 75 } 76 77 imgs, err := i.images.List(ctx) 78 if err != nil { 79 return nil, err 80 } 81 82 // TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273 83 snapshotter := i.snapshotterService(i.snapshotter) 84 sizeCache := make(map[digest.Digest]int64) 85 snapshotSizeFn := func(d digest.Digest) (int64, error) { 86 if s, ok := sizeCache[d]; ok { 87 return s, nil 88 } 89 usage, err := snapshotter.Usage(ctx, d.String()) 90 if err != nil { 91 return 0, err 92 } 93 sizeCache[d] = usage.Size 94 return usage.Size, nil 95 } 96 97 uniqueImages := map[digest.Digest]images.Image{} 98 tagsByDigest := map[digest.Digest][]string{} 99 intermediateImages := map[digest.Digest]struct{}{} 100 101 hideIntermediate := !opts.All 102 if hideIntermediate { 103 for _, img := range imgs { 104 parent, ok := img.Labels[imageLabelClassicBuilderParent] 105 if ok && parent != "" { 106 dgst, err := digest.Parse(parent) 107 if err != nil { 108 log.G(ctx).WithFields(log.Fields{ 109 "error": err, 110 "value": parent, 111 }).Warnf("invalid %s label value", imageLabelClassicBuilderParent) 112 } 113 intermediateImages[dgst] = struct{}{} 114 } 115 } 116 } 117 118 // TODO: Allow platform override? 119 platformMatcher := matchAllWithPreference(cplatforms.Default()) 120 121 for _, img := range imgs { 122 isDangling := isDanglingImage(img) 123 124 if hideIntermediate && isDangling { 125 if _, ok := intermediateImages[img.Target.Digest]; ok { 126 continue 127 } 128 } 129 130 if !filter(img) { 131 continue 132 } 133 134 dgst := img.Target.Digest 135 uniqueImages[dgst] = img 136 137 if isDangling { 138 continue 139 } 140 141 ref, err := reference.ParseNormalizedNamed(img.Name) 142 if err != nil { 143 continue 144 } 145 tagsByDigest[dgst] = append(tagsByDigest[dgst], reference.FamiliarString(ref)) 146 } 147 148 resultsMut := sync.Mutex{} 149 eg, egCtx := errgroup.WithContext(ctx) 150 eg.SetLimit(runtime.NumCPU() * 2) 151 152 var ( 153 summaries = make([]*imagetypes.Summary, 0, len(imgs)) 154 root []*[]digest.Digest 155 layers map[digest.Digest]int 156 ) 157 if opts.SharedSize { 158 root = make([]*[]digest.Digest, 0, len(imgs)) 159 layers = make(map[digest.Digest]int) 160 } 161 162 for _, img := range uniqueImages { 163 img := img 164 eg.Go(func() error { 165 image, allChainsIDs, err := i.imageSummary(egCtx, img, platformMatcher, opts, tagsByDigest) 166 if err != nil { 167 return err 168 } 169 // No error, but image should be skipped. 170 if image == nil { 171 return nil 172 } 173 174 resultsMut.Lock() 175 summaries = append(summaries, image) 176 177 if opts.SharedSize { 178 root = append(root, &allChainsIDs) 179 for _, id := range allChainsIDs { 180 layers[id] = layers[id] + 1 181 } 182 } 183 resultsMut.Unlock() 184 return nil 185 }) 186 } 187 188 if err := eg.Wait(); err != nil { 189 return nil, err 190 } 191 192 if opts.SharedSize { 193 for n, chainIDs := range root { 194 sharedSize, err := computeSharedSize(*chainIDs, layers, snapshotSizeFn) 195 if err != nil { 196 return nil, err 197 } 198 summaries[n].SharedSize = sharedSize 199 } 200 } 201 202 sort.Sort(sort.Reverse(byCreated(summaries))) 203 204 return summaries, nil 205 } 206 207 // imageSummary returns a summary of the image, including the total size of the image and all its platforms. 208 // It also returns the chainIDs of all the layers of the image (including all its platforms). 209 // All return values will be nil if the image should be skipped. 210 func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platformMatcher platforms.MatchComparer, 211 opts imagetypes.ListOptions, tagsByDigest map[digest.Digest][]string, 212 ) (_ *imagetypes.Summary, allChainIDs []digest.Digest, _ error) { 213 214 // Total size of the image including all its platform 215 var totalSize int64 216 217 // ChainIDs of all the layers of the image (including all its platform) 218 var allChainsIDs []digest.Digest 219 220 // Count of containers using the image 221 var containersCount int64 222 223 // Single platform image manifest preferred by the platform matcher 224 var best *ImageManifest 225 var bestPlatform ocispec.Platform 226 227 err := i.walkImageManifests(ctx, img, func(img *ImageManifest) error { 228 if isPseudo, err := img.IsPseudoImage(ctx); isPseudo || err != nil { 229 return nil 230 } 231 232 available, err := img.CheckContentAvailable(ctx) 233 if err != nil { 234 log.G(ctx).WithFields(log.Fields{ 235 "error": err, 236 "manifest": img.Target(), 237 "image": img.Name(), 238 }).Warn("checking availability of platform specific manifest failed") 239 return nil 240 } 241 242 if !available { 243 return nil 244 } 245 246 conf, err := img.Config(ctx) 247 if err != nil { 248 return err 249 } 250 251 var dockerImage dockerspec.DockerOCIImage 252 if err := readConfig(ctx, i.content, conf, &dockerImage); err != nil { 253 return err 254 } 255 256 target := img.Target() 257 258 diffIDs, err := img.RootFS(ctx) 259 if err != nil { 260 return err 261 } 262 263 chainIDs := identity.ChainIDs(diffIDs) 264 265 ts, _, err := i.singlePlatformSize(ctx, img) 266 if err != nil { 267 return err 268 } 269 270 totalSize += ts 271 allChainsIDs = append(allChainsIDs, chainIDs...) 272 273 if opts.ContainerCount { 274 i.containers.ApplyAll(func(c *container.Container) { 275 if c.ImageManifest != nil && c.ImageManifest.Digest == target.Digest { 276 containersCount++ 277 } 278 }) 279 } 280 281 var platform ocispec.Platform 282 if target.Platform != nil { 283 platform = *target.Platform 284 } else { 285 platform = dockerImage.Platform 286 } 287 288 // Filter out platforms that don't match the requested platform. Do it 289 // after the size, container count and chainIDs are summed up to have 290 // the single combined entry still represent the whole multi-platform 291 // image. 292 if !platformMatcher.Match(platform) { 293 return nil 294 } 295 296 if best == nil || platformMatcher.Less(platform, bestPlatform) { 297 best = img 298 bestPlatform = platform 299 } 300 301 return nil 302 }) 303 if err != nil { 304 if errors.Is(err, errNotManifestOrIndex) { 305 log.G(ctx).WithFields(log.Fields{ 306 "error": err, 307 "image": img.Name, 308 }).Warn("unexpected image target (neither a manifest nor index)") 309 return nil, nil, nil 310 } 311 return nil, nil, err 312 } 313 314 if best == nil { 315 // TODO we should probably show *something* for images we've pulled 316 // but are 100% shallow or an empty manifest list/index 317 // ("tianon/scratch:index" is an empty example image index and 318 // "tianon/scratch:list" is an empty example manifest list) 319 return nil, nil, nil 320 } 321 322 image, err := i.singlePlatformImage(ctx, i.content, tagsByDigest[best.RealTarget.Digest], best) 323 if err != nil { 324 return nil, nil, err 325 } 326 image.Size = totalSize 327 328 if opts.ContainerCount { 329 image.Containers = containersCount 330 } 331 return image, allChainsIDs, nil 332 } 333 334 func (i *ImageService) singlePlatformSize(ctx context.Context, imgMfst *ImageManifest) (totalSize int64, contentSize int64, _ error) { 335 // TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273 336 snapshotter := i.snapshotterService(i.snapshotter) 337 338 diffIDs, err := imgMfst.RootFS(ctx) 339 if err != nil { 340 return -1, -1, errors.Wrapf(err, "failed to get rootfs of image %s", imgMfst.Name()) 341 } 342 343 imageSnapshotID := identity.ChainID(diffIDs).String() 344 unpackedUsage, err := calculateSnapshotTotalUsage(ctx, snapshotter, imageSnapshotID) 345 if err != nil { 346 if !cerrdefs.IsNotFound(err) { 347 log.G(ctx).WithError(err).WithFields(log.Fields{ 348 "image": imgMfst.Name(), 349 "snapshotID": imageSnapshotID, 350 }).Warn("failed to calculate unpacked size of image") 351 } 352 unpackedUsage = snapshots.Usage{Size: 0} 353 } 354 355 contentSize, err = imgMfst.Size(ctx) 356 if err != nil { 357 return -1, -1, err 358 } 359 360 // totalSize is the size of the image's packed layers and snapshots 361 // (unpacked layers) combined. 362 totalSize = contentSize + unpackedUsage.Size 363 return totalSize, contentSize, nil 364 } 365 366 func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore content.Store, repoTags []string, imageManifest *ImageManifest) (*imagetypes.Summary, error) { 367 var repoDigests []string 368 rawImg := imageManifest.Metadata() 369 target := rawImg.Target.Digest 370 371 logger := log.G(ctx).WithFields(log.Fields{ 372 "name": rawImg.Name, 373 "digest": target, 374 }) 375 376 ref, err := reference.ParseNamed(rawImg.Name) 377 if err != nil { 378 // If the image has unexpected name format (not a Named reference or a dangling image) 379 // add the offending name to RepoTags but also log an error to make it clear to the 380 // administrator that this is unexpected. 381 // TODO: Reconsider when containerd is more strict on image names, see: 382 // https://github.com/containerd/containerd/issues/7986 383 if !isDanglingImage(rawImg) { 384 logger.WithError(err).Error("failed to parse image name as reference") 385 repoTags = append(repoTags, rawImg.Name) 386 } 387 } else { 388 digested, err := reference.WithDigest(reference.TrimNamed(ref), target) 389 if err != nil { 390 logger.WithError(err).Error("failed to create digested reference") 391 } else { 392 repoDigests = append(repoDigests, reference.FamiliarString(digested)) 393 } 394 } 395 396 cfgDesc, err := imageManifest.Image.Config(ctx) 397 if err != nil { 398 return nil, err 399 } 400 var cfg configLabels 401 if err := readConfig(ctx, contentStore, cfgDesc, &cfg); err != nil { 402 return nil, err 403 } 404 405 totalSize, _, err := i.singlePlatformSize(ctx, imageManifest) 406 if err != nil { 407 return nil, errors.Wrapf(err, "failed to calculate size of image %s", imageManifest.Name()) 408 } 409 410 summary := &imagetypes.Summary{ 411 ParentID: rawImg.Labels[imageLabelClassicBuilderParent], 412 ID: target.String(), 413 RepoDigests: repoDigests, 414 RepoTags: repoTags, 415 Size: totalSize, 416 Labels: cfg.Config.Labels, 417 // -1 indicates that the value has not been set (avoids ambiguity 418 // between 0 (default) and "not set". We cannot use a pointer (nil) 419 // for this, as the JSON representation uses "omitempty", which would 420 // consider both "0" and "nil" to be "empty". 421 SharedSize: -1, 422 Containers: -1, 423 } 424 if cfg.Created != nil { 425 summary.Created = cfg.Created.Unix() 426 } 427 428 return summary, nil 429 } 430 431 type imageFilterFunc func(image images.Image) bool 432 433 // setupFilters constructs an imageFilterFunc from the given imageFilters. 434 // 435 // filterFunc is a function that checks whether given image matches the filters. 436 // TODO(thaJeztah): reimplement filters using containerd filters if possible: see https://github.com/moby/moby/issues/43845 437 func (i *ImageService) setupFilters(ctx context.Context, imageFilters filters.Args) (filterFunc imageFilterFunc, outErr error) { 438 var fltrs []imageFilterFunc 439 err := imageFilters.WalkValues("before", func(value string) error { 440 img, err := i.GetImage(ctx, value, backend.GetImageOpts{}) 441 if err != nil { 442 return err 443 } 444 if img != nil && img.Created != nil { 445 fltrs = append(fltrs, func(candidate images.Image) bool { 446 cand, err := i.GetImage(ctx, candidate.Name, backend.GetImageOpts{}) 447 if err != nil { 448 return false 449 } 450 return cand.Created != nil && cand.Created.Before(*img.Created) 451 }) 452 } 453 return nil 454 }) 455 if err != nil { 456 return nil, err 457 } 458 459 err = imageFilters.WalkValues("since", func(value string) error { 460 img, err := i.GetImage(ctx, value, backend.GetImageOpts{}) 461 if err != nil { 462 return err 463 } 464 if img != nil && img.Created != nil { 465 fltrs = append(fltrs, func(candidate images.Image) bool { 466 cand, err := i.GetImage(ctx, candidate.Name, backend.GetImageOpts{}) 467 if err != nil { 468 return false 469 } 470 return cand.Created != nil && cand.Created.After(*img.Created) 471 }) 472 } 473 return nil 474 }) 475 if err != nil { 476 return nil, err 477 } 478 479 err = imageFilters.WalkValues("until", func(value string) error { 480 ts, err := timetypes.GetTimestamp(value, time.Now()) 481 if err != nil { 482 return err 483 } 484 seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) 485 if err != nil { 486 return err 487 } 488 until := time.Unix(seconds, nanoseconds) 489 490 fltrs = append(fltrs, func(image images.Image) bool { 491 created := image.CreatedAt 492 return created.Before(until) 493 }) 494 return err 495 }) 496 if err != nil { 497 return nil, err 498 } 499 500 labelFn, err := setupLabelFilter(ctx, i.content, imageFilters) 501 if err != nil { 502 return nil, err 503 } 504 if labelFn != nil { 505 fltrs = append(fltrs, labelFn) 506 } 507 508 if imageFilters.Contains("dangling") { 509 danglingValue, err := imageFilters.GetBoolOrDefault("dangling", false) 510 if err != nil { 511 return nil, err 512 } 513 fltrs = append(fltrs, func(image images.Image) bool { 514 return danglingValue == isDanglingImage(image) 515 }) 516 } 517 518 if refs := imageFilters.Get("reference"); len(refs) != 0 { 519 fltrs = append(fltrs, func(image images.Image) bool { 520 ref, err := reference.ParseNormalizedNamed(image.Name) 521 if err != nil { 522 return false 523 } 524 for _, value := range refs { 525 found, err := reference.FamiliarMatch(value, ref) 526 if err != nil { 527 return false 528 } 529 if found { 530 return found 531 } 532 } 533 return false 534 }) 535 } 536 537 return func(image images.Image) bool { 538 for _, filter := range fltrs { 539 if !filter(image) { 540 return false 541 } 542 } 543 return true 544 }, nil 545 } 546 547 // setupLabelFilter parses filter args for "label" and "label!" and returns a 548 // filter func which will check if any image config from the given image has 549 // labels that match given predicates. 550 func setupLabelFilter(ctx context.Context, store content.Store, fltrs filters.Args) (func(image images.Image) bool, error) { 551 type labelCheck struct { 552 key string 553 value string 554 onlyExists bool 555 negate bool 556 } 557 558 var checks []labelCheck 559 for _, fltrName := range []string{"label", "label!"} { 560 for _, l := range fltrs.Get(fltrName) { 561 k, v, found := strings.Cut(l, "=") 562 err := labels.Validate(k, v) 563 if err != nil { 564 return nil, err 565 } 566 567 negate := strings.HasSuffix(fltrName, "!") 568 569 // If filter value is key!=value then flip the above. 570 if strings.HasSuffix(k, "!") { 571 k = strings.TrimSuffix(k, "!") 572 negate = !negate 573 } 574 575 checks = append(checks, labelCheck{ 576 key: k, 577 value: v, 578 onlyExists: !found, 579 negate: negate, 580 }) 581 } 582 } 583 584 if len(checks) == 0 { 585 return nil, nil 586 } 587 588 return func(image images.Image) bool { 589 // This is not an error, but a signal to Dispatch that it should stop 590 // processing more content (otherwise it will run for all children). 591 // It will be returned once a matching config is found. 592 errFoundConfig := errors.New("success, found matching config") 593 594 err := images.Dispatch(ctx, presentChildrenHandler(store, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { 595 if !images.IsConfigType(desc.MediaType) { 596 return nil, nil 597 } 598 var cfg configLabels 599 if err := readConfig(ctx, store, desc, &cfg); err != nil { 600 if errdefs.IsNotFound(err) { 601 return nil, nil 602 } 603 return nil, err 604 } 605 606 for _, check := range checks { 607 value, exists := cfg.Config.Labels[check.key] 608 609 if check.onlyExists { 610 // label! given without value, check if doesn't exist 611 if check.negate { 612 // Label exists, config doesn't match 613 if exists { 614 return nil, nil 615 } 616 } else { 617 // Label should exist 618 if !exists { 619 // Label doesn't exist, config doesn't match 620 return nil, nil 621 } 622 } 623 continue 624 } else if !exists { 625 // We are checking value and label doesn't exist. 626 return nil, nil 627 } 628 629 valueEquals := value == check.value 630 if valueEquals == check.negate { 631 return nil, nil 632 } 633 } 634 635 // This config matches the filter so we need to shop this image, stop dispatch. 636 return nil, errFoundConfig 637 })), nil, image.Target) 638 639 if err == errFoundConfig { 640 return true 641 } 642 if err != nil { 643 log.G(ctx).WithFields(log.Fields{ 644 "error": err, 645 "image": image.Name, 646 "checks": checks, 647 }).Error("failed to check image labels") 648 } 649 650 return false 651 }, nil 652 } 653 654 func computeSharedSize(chainIDs []digest.Digest, layers map[digest.Digest]int, sizeFn func(d digest.Digest) (int64, error)) (int64, error) { 655 var sharedSize int64 656 for _, chainID := range chainIDs { 657 if layers[chainID] == 1 { 658 continue 659 } 660 size, err := sizeFn(chainID) 661 if err != nil { 662 // Several images might share the same layer and neither of them 663 // might be unpacked (for example if it's a non-host platform). 664 if cerrdefs.IsNotFound(err) { 665 continue 666 } 667 return 0, err 668 } 669 sharedSize += size 670 } 671 return sharedSize, nil 672 } 673 674 // readConfig reads content pointed by the descriptor and unmarshals it into a specified output. 675 func readConfig(ctx context.Context, store content.Provider, desc ocispec.Descriptor, out interface{}) error { 676 data, err := content.ReadBlob(ctx, store, desc) 677 if err != nil { 678 err = errors.Wrapf(err, "failed to read config content") 679 if cerrdefs.IsNotFound(err) { 680 return errdefs.NotFound(err) 681 } 682 return err 683 } 684 685 err = json.Unmarshal(data, out) 686 if err != nil { 687 err = errors.Wrapf(err, "could not deserialize image config") 688 if cerrdefs.IsNotFound(err) { 689 return errdefs.NotFound(err) 690 } 691 return err 692 } 693 694 return nil 695 }