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