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