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