github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "sort" 8 "strconv" 9 "strings" 10 "sync/atomic" 11 "time" 12 13 cerrdefs "github.com/containerd/containerd/errdefs" 14 containerdimages "github.com/containerd/containerd/images" 15 "github.com/containerd/containerd/platforms" 16 "github.com/containerd/log" 17 "github.com/distribution/reference" 18 "github.com/docker/docker/api/types/backend" 19 "github.com/docker/docker/daemon/images" 20 "github.com/docker/docker/errdefs" 21 "github.com/docker/docker/image" 22 imagespec "github.com/moby/docker-image-spec/specs-go/v1" 23 "github.com/opencontainers/go-digest" 24 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 25 "github.com/pkg/errors" 26 "golang.org/x/sync/semaphore" 27 ) 28 29 var truncatedID = regexp.MustCompile(`^(sha256:)?([a-f0-9]{4,64})$`) 30 31 var errInconsistentData error = errors.New("consistency error: data changed during operation, retry") 32 33 // GetImage returns an image corresponding to the image referred to by refOrID. 34 func (i *ImageService) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error) { 35 desc, err := i.resolveImage(ctx, refOrID) 36 if err != nil { 37 return nil, err 38 } 39 40 platform := matchAllWithPreference(platforms.Default()) 41 if options.Platform != nil { 42 platform = platforms.OnlyStrict(*options.Platform) 43 } 44 45 presentImages, err := i.presentImages(ctx, desc, refOrID, platform) 46 if err != nil { 47 return nil, err 48 } 49 ociImage := presentImages[0] 50 51 img := dockerOciImageToDockerImagePartial(image.ID(desc.Target.Digest), ociImage) 52 53 parent, err := i.getImageLabelByDigest(ctx, desc.Target.Digest, imageLabelClassicBuilderParent) 54 if err != nil { 55 log.G(ctx).WithError(err).Warn("failed to determine Parent property") 56 } else { 57 img.Parent = image.ID(parent) 58 } 59 60 if options.Details { 61 lastUpdated := time.Unix(0, 0) 62 size, err := i.size(ctx, desc.Target, platform) 63 if err != nil { 64 return nil, err 65 } 66 67 tagged, err := i.images.List(ctx, "target.digest=="+desc.Target.Digest.String()) 68 if err != nil { 69 return nil, err 70 } 71 72 // Usually each image will result in 2 references (named and digested). 73 refs := make([]reference.Named, 0, len(tagged)*2) 74 for _, i := range tagged { 75 if i.UpdatedAt.After(lastUpdated) { 76 lastUpdated = i.UpdatedAt 77 } 78 if isDanglingImage(i) { 79 if len(tagged) > 1 { 80 // This is unexpected - dangling image should be deleted 81 // as soon as another image with the same target is created. 82 // Log a warning, but don't error out the whole operation. 83 log.G(ctx).WithField("refs", tagged).Warn("multiple images have the same target, but one of them is still dangling") 84 } 85 continue 86 } 87 88 name, err := reference.ParseNamed(i.Name) 89 if err != nil { 90 // This is inconsistent with `docker image ls` which will 91 // still include the malformed name in RepoTags. 92 log.G(ctx).WithField("name", name).WithError(err).Error("failed to parse image name as reference") 93 continue 94 } 95 refs = append(refs, name) 96 97 if _, ok := name.(reference.Digested); ok { 98 // Image name already contains a digest, so no need to create a digested reference. 99 continue 100 } 101 102 digested, err := reference.WithDigest(reference.TrimNamed(name), desc.Target.Digest) 103 if err != nil { 104 // This could only happen if digest is invalid, but considering that 105 // we get it from the Descriptor it's highly unlikely. 106 // Log error just in case. 107 log.G(ctx).WithError(err).Error("failed to create digested reference") 108 continue 109 } 110 refs = append(refs, digested) 111 } 112 113 img.Details = &image.Details{ 114 References: refs, 115 Size: size, 116 Metadata: nil, 117 Driver: i.snapshotter, 118 LastUpdated: lastUpdated, 119 } 120 } 121 122 return img, nil 123 } 124 125 // presentImages returns the images that are present in the content store, 126 // manifests without a config are ignored. 127 // The images are filtered and sorted by platform preference. 128 func (i *ImageService) presentImages(ctx context.Context, desc containerdimages.Image, refOrID string, platform platforms.MatchComparer) ([]imagespec.DockerOCIImage, error) { 129 var presentImages []imagespec.DockerOCIImage 130 err := i.walkImageManifests(ctx, desc, func(img *ImageManifest) error { 131 conf, err := img.Config(ctx) 132 if err != nil { 133 if cerrdefs.IsNotFound(err) { 134 log.G(ctx).WithFields(log.Fields{ 135 "manifestDescriptor": img.Target(), 136 }).Debug("manifest was present, but accessing its config failed, ignoring") 137 return nil 138 } 139 return errdefs.System(fmt.Errorf("failed to get config descriptor: %w", err)) 140 } 141 142 var ociimage imagespec.DockerOCIImage 143 if err := readConfig(ctx, i.content, conf, &ociimage); err != nil { 144 if errdefs.IsNotFound(err) { 145 log.G(ctx).WithFields(log.Fields{ 146 "manifestDescriptor": img.Target(), 147 "configDescriptor": conf, 148 }).Debug("manifest present, but its config is missing, ignoring") 149 return nil 150 } 151 return errdefs.System(fmt.Errorf("failed to read config of the manifest %v: %w", img.Target().Digest, err)) 152 } 153 154 if platform.Match(ociimage.Platform) { 155 presentImages = append(presentImages, ociimage) 156 } 157 158 return nil 159 }) 160 if err != nil { 161 return nil, err 162 } 163 if len(presentImages) == 0 { 164 ref, _ := reference.ParseAnyReference(refOrID) 165 return nil, images.ErrImageDoesNotExist{Ref: ref} 166 } 167 168 sort.SliceStable(presentImages, func(i, j int) bool { 169 return platform.Less(presentImages[i].Platform, presentImages[j].Platform) 170 }) 171 172 return presentImages, nil 173 } 174 175 func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options backend.GetImageOpts) (*ocispec.Descriptor, error) { 176 platform := matchAllWithPreference(platforms.Default()) 177 if options.Platform != nil { 178 platform = platforms.Only(*options.Platform) 179 } 180 181 cs := i.content 182 183 img, err := i.resolveImage(ctx, refOrID) 184 if err != nil { 185 return nil, err 186 } 187 188 desc := img.Target 189 if containerdimages.IsManifestType(desc.MediaType) { 190 plat := desc.Platform 191 if plat == nil { 192 config, err := img.Config(ctx, cs, platform) 193 if err != nil { 194 return nil, err 195 } 196 var configPlatform ocispec.Platform 197 if err := readConfig(ctx, cs, config, &configPlatform); err != nil { 198 return nil, err 199 } 200 201 plat = &configPlatform 202 } 203 204 if options.Platform != nil { 205 if plat == nil { 206 return nil, errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: nil", refOrID, platforms.Format(*options.Platform))) 207 } else if !platform.Match(*plat) { 208 return nil, errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s", refOrID, platforms.Format(*options.Platform), platforms.Format(*plat))) 209 } 210 } 211 212 return &desc, nil 213 } 214 215 if containerdimages.IsIndexType(desc.MediaType) { 216 childManifests, err := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)(ctx, desc) 217 if err != nil { 218 if cerrdefs.IsNotFound(err) { 219 return nil, errdefs.NotFound(err) 220 } 221 return nil, errdefs.System(err) 222 } 223 224 // len(childManifests) == 1 since we requested 1 and if none 225 // were found LimitManifests would have thrown an error 226 if !containerdimages.IsManifestType(childManifests[0].MediaType) { 227 return nil, errdefs.NotFound(fmt.Errorf("manifest has incorrect mediatype: %s", childManifests[0].MediaType)) 228 } 229 230 return &childManifests[0], nil 231 } 232 233 return nil, errdefs.NotFound(errors.New("failed to find manifest")) 234 } 235 236 // size returns the total size of the image's packed resources. 237 func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform platforms.MatchComparer) (int64, error) { 238 var size int64 239 240 cs := i.content 241 handler := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1) 242 243 var wh containerdimages.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 244 children, err := handler(ctx, desc) 245 if err != nil { 246 if !cerrdefs.IsNotFound(err) { 247 return nil, err 248 } 249 } 250 251 atomic.AddInt64(&size, desc.Size) 252 253 return children, nil 254 } 255 256 l := semaphore.NewWeighted(3) 257 if err := containerdimages.Dispatch(ctx, wh, l, desc); err != nil { 258 return 0, err 259 } 260 261 return size, nil 262 } 263 264 // resolveDescriptor searches for a descriptor based on the given 265 // reference or identifier. Returns the descriptor of 266 // the image, which could be a manifest list, manifest, or config. 267 func (i *ImageService) resolveDescriptor(ctx context.Context, refOrID string) (ocispec.Descriptor, error) { 268 img, err := i.resolveImage(ctx, refOrID) 269 if err != nil { 270 return ocispec.Descriptor{}, err 271 } 272 273 return img.Target, nil 274 } 275 276 func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (containerdimages.Image, error) { 277 parsed, err := reference.ParseAnyReference(refOrID) 278 if err != nil { 279 return containerdimages.Image{}, errdefs.InvalidParameter(err) 280 } 281 282 digested, ok := parsed.(reference.Digested) 283 if ok { 284 imgs, err := i.images.List(ctx, "target.digest=="+digested.Digest().String()) 285 if err != nil { 286 return containerdimages.Image{}, errors.Wrap(err, "failed to lookup digest") 287 } 288 if len(imgs) == 0 { 289 return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} 290 } 291 292 // If reference is both Named and Digested, make sure we don't match 293 // images with a different repository even if digest matches. 294 // For example, busybox@sha256:abcdef..., shouldn't match asdf@sha256:abcdef... 295 if parsedNamed, ok := parsed.(reference.Named); ok { 296 for _, img := range imgs { 297 imgNamed, err := reference.ParseNormalizedNamed(img.Name) 298 if err != nil { 299 log.G(ctx).WithError(err).WithField("image", img.Name).Warn("image with invalid name encountered") 300 continue 301 } 302 303 if parsedNamed.Name() == imgNamed.Name() { 304 return img, nil 305 } 306 } 307 return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} 308 } 309 310 return imgs[0], nil 311 } 312 313 ref := reference.TagNameOnly(parsed.(reference.Named)).String() 314 img, err := i.images.Get(ctx, ref) 315 if err == nil { 316 return img, nil 317 } else { 318 // TODO(containerd): error translation can use common function 319 if !cerrdefs.IsNotFound(err) { 320 return containerdimages.Image{}, err 321 } 322 } 323 324 // If the identifier could be a short ID, attempt to match 325 if truncatedID.MatchString(refOrID) { 326 idWithoutAlgo := strings.TrimPrefix(refOrID, "sha256:") 327 filters := []string{ 328 fmt.Sprintf("name==%q", ref), // Or it could just look like one. 329 "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))), 330 } 331 imgs, err := i.images.List(ctx, filters...) 332 if err != nil { 333 return containerdimages.Image{}, err 334 } 335 336 if len(imgs) == 0 { 337 return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} 338 } 339 if len(imgs) > 1 { 340 digests := map[digest.Digest]struct{}{} 341 for _, img := range imgs { 342 if img.Name == ref { 343 return img, nil 344 } 345 digests[img.Target.Digest] = struct{}{} 346 } 347 348 if len(digests) > 1 { 349 return containerdimages.Image{}, errdefs.NotFound(errors.New("ambiguous reference")) 350 } 351 } 352 353 return imgs[0], nil 354 } 355 356 return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} 357 } 358 359 // getAllImagesWithRepository returns a slice of images which name is a reference 360 // pointing to the same repository as the given reference. 361 func (i *ImageService) getAllImagesWithRepository(ctx context.Context, ref reference.Named) ([]containerdimages.Image, error) { 362 nameFilter := "^" + regexp.QuoteMeta(ref.Name()) + ":" + reference.TagRegexp.String() + "$" 363 return i.images.List(ctx, "name~="+strconv.Quote(nameFilter)) 364 } 365 366 func imageFamiliarName(img containerdimages.Image) string { 367 if isDanglingImage(img) { 368 return img.Target.Digest.String() 369 } 370 371 if ref, err := reference.ParseNamed(img.Name); err == nil { 372 return reference.FamiliarString(ref) 373 } 374 return img.Name 375 } 376 377 // getImageLabelByDigest will return the value of the label for images 378 // targeting the specified digest. 379 // If images have different values, an errdefs.Conflict error will be returned. 380 func (i *ImageService) getImageLabelByDigest(ctx context.Context, target digest.Digest, labelKey string) (string, error) { 381 imgs, err := i.images.List(ctx, "target.digest=="+target.String()+",labels."+labelKey) 382 if err != nil { 383 return "", errdefs.System(err) 384 } 385 386 var value string 387 for _, img := range imgs { 388 if v, ok := img.Labels[labelKey]; ok { 389 if value != "" && value != v { 390 return value, errdefs.Conflict(fmt.Errorf("conflicting label value %q and %q", value, v)) 391 } 392 value = v 393 } 394 } 395 396 return value, nil 397 } 398 399 func convertError(err error) error { 400 // TODO: Convert containerd error to Docker error 401 return err 402 } 403 404 // resolveAllReferences resolves the reference name or ID to an image and returns all the images with 405 // the same target. 406 // 407 // Returns: 408 // 409 // 1: *(github.com/containerd/containerd/images).Image 410 // 411 // An image match from the image store with the provided refOrID 412 // 413 // 2: [](github.com/containerd/containerd/images).Image 414 // 415 // List of all images with the same target that matches the refOrID. If the first argument is 416 // non-nil, the image list will all have the same target as the matched image. If the first 417 // argument is nil but the list is non-empty, this value is a list of all the images with a 418 // target that matches the digest provided in the refOrID, but none are an image name match 419 // to refOrID. 420 // 421 // 3: error 422 // 423 // An error looking up refOrID or no images found with matching name or target. Note that the first 424 // argument may be nil with a nil error if the second argument is non-empty. 425 func (i *ImageService) resolveAllReferences(ctx context.Context, refOrID string) (*containerdimages.Image, []containerdimages.Image, error) { 426 parsed, err := reference.ParseAnyReference(refOrID) 427 if err != nil { 428 return nil, nil, errdefs.InvalidParameter(err) 429 } 430 var dgst digest.Digest 431 var img *containerdimages.Image 432 433 if truncatedID.MatchString(refOrID) { 434 if d, ok := parsed.(reference.Digested); ok { 435 if cimg, err := i.images.Get(ctx, d.String()); err == nil { 436 img = &cimg 437 dgst = d.Digest() 438 if cimg.Target.Digest != dgst { 439 // Ambiguous image reference, use reference name 440 log.G(ctx).WithField("image", refOrID).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest") 441 dgst = cimg.Target.Digest 442 } 443 } else if !cerrdefs.IsNotFound(err) { 444 return nil, nil, convertError(err) 445 } else { 446 dgst = d.Digest() 447 } 448 } else { 449 idWithoutAlgo := strings.TrimPrefix(refOrID, "sha256:") 450 name := reference.TagNameOnly(parsed.(reference.Named)).String() 451 filters := []string{ 452 fmt.Sprintf("name==%q", name), // Or it could just look like one. 453 "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))), 454 } 455 imgs, err := i.images.List(ctx, filters...) 456 if err != nil { 457 return nil, nil, convertError(err) 458 } 459 460 if len(imgs) == 0 { 461 return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} 462 } 463 464 for _, limg := range imgs { 465 if limg.Name == name { 466 copyImg := limg 467 img = ©Img 468 } 469 if dgst != "" { 470 if limg.Target.Digest != dgst { 471 return nil, nil, errdefs.NotFound(errors.New("ambiguous reference")) 472 } 473 } else { 474 dgst = limg.Target.Digest 475 } 476 } 477 478 // Return immediately if target digest matches already included 479 if img == nil || len(imgs) > 1 { 480 return img, imgs, nil 481 } 482 } 483 } else { 484 named, ok := parsed.(reference.Named) 485 if !ok { 486 return nil, nil, errdefs.InvalidParameter(errors.New("invalid name reference")) 487 } 488 489 digested, ok := parsed.(reference.Digested) 490 if ok { 491 dgst = digested.Digest() 492 } 493 494 name := reference.TagNameOnly(named).String() 495 496 cimg, err := i.images.Get(ctx, name) 497 if err != nil { 498 if !cerrdefs.IsNotFound(err) { 499 return nil, nil, convertError(err) 500 } 501 // If digest is given, continue looking up for matching targets. 502 // There will be no exact match found but the caller may attempt 503 // to match across images with the matching target. 504 if dgst == "" { 505 return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} 506 } 507 } else { 508 img = &cimg 509 if dgst != "" && img.Target.Digest != dgst { 510 // Ambiguous image reference, use reference name 511 log.G(ctx).WithField("image", name).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest") 512 } 513 dgst = img.Target.Digest 514 } 515 } 516 517 // Lookup up all associated images and check for consistency with first reference 518 // Ideally operations dependent on multiple values will rely on the garbage collector, 519 // this logic will just check for consistency and throw an error 520 imgs, err := i.images.List(ctx, "target.digest=="+dgst.String()) 521 if err != nil { 522 return nil, nil, errors.Wrap(err, "failed to lookup digest") 523 } 524 if len(imgs) == 0 { 525 if img == nil { 526 return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} 527 } 528 err = errInconsistentData 529 } else if img != nil { 530 // Check to ensure the original img is in the list still 531 err = errInconsistentData 532 for _, rimg := range imgs { 533 if rimg.Name == img.Name { 534 err = nil 535 break 536 } 537 } 538 } 539 if errors.Is(err, errInconsistentData) { 540 if retries, ok := ctx.Value(errInconsistentData).(int); !ok || retries < 3 { 541 log.G(ctx).WithFields(log.Fields{"retry": retries, "ref": refOrID}).Info("image changed during lookup, retrying") 542 return i.resolveAllReferences(context.WithValue(ctx, errInconsistentData, retries+1), refOrID) 543 } 544 return nil, nil, err 545 } 546 547 return img, imgs, nil 548 }