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