| package containerd | |
| import ( | |
| "context" | |
| "fmt" | |
| "regexp" | |
| "strconv" | |
| "strings" | |
| c8dimages "github.com/containerd/containerd/v2/core/images" | |
| cerrdefs "github.com/containerd/errdefs" | |
| "github.com/containerd/log" | |
| "github.com/containerd/platforms" | |
| "github.com/distribution/reference" | |
| dockerspec "github.com/moby/docker-image-spec/specs-go/v1" | |
| "github.com/moby/moby/v2/daemon/images" | |
| "github.com/moby/moby/v2/daemon/internal/image" | |
| "github.com/moby/moby/v2/daemon/server/imagebackend" | |
| "github.com/moby/moby/v2/errdefs" | |
| "github.com/opencontainers/go-digest" | |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | |
| "github.com/pkg/errors" | |
| ) | |
| var errInconsistentData error = errors.New("consistency error: data changed during operation, retry") | |
| type errPlatformNotFound struct { | |
| wanted ocispec.Platform | |
| imageRef string | |
| } | |
| func (e *errPlatformNotFound) NotFound() {} | |
| func (e *errPlatformNotFound) Error() string { | |
| msg := "image with reference " + e.imageRef + " was found but does not provide " | |
| if e.wanted.OS != "" { | |
| msg += "the specified platform (" + platforms.FormatAll(e.wanted) + ")" | |
| } else { | |
| msg += "any platform" | |
| } | |
| return msg | |
| } | |
| // GetImage returns an image corresponding to the image referred to by refOrID. | |
| func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagebackend.GetImageOpts) (*image.Image, error) { | |
| img, err := i.resolveImage(ctx, refOrID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| pm := i.matchRequestedOrDefault(platforms.OnlyStrict, options.Platform) | |
| im, err := i.getBestPresentImageManifest(ctx, img, pm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var ociImage dockerspec.DockerOCIImage | |
| err = im.ReadConfig(ctx, &ociImage) | |
| if err != nil { | |
| return nil, err | |
| } | |
| imgV1 := dockerOciImageToDockerImagePartial(image.ID(img.Target.Digest), ociImage) | |
| parent, err := i.getImageLabelByDigest(ctx, img.Target.Digest, imageLabelClassicBuilderParent) | |
| if err != nil { | |
| log.G(ctx).WithError(err).Warn("failed to determine Parent property") | |
| } else { | |
| imgV1.Parent = image.ID(parent) | |
| } | |
| target := im.Target() | |
| imgV1.Details = &image.Details{ | |
| ManifestDescriptor: &target, | |
| } | |
| return imgV1, nil | |
| } | |
| // getBestPresentImageManifest returns a platform-specific image manifest that best matches the provided platform matcher. | |
| // Only locally available platform images are considered. | |
| // If no image manifest matches the platform, an error is returned. | |
| func (i *ImageService) getBestPresentImageManifest(ctx context.Context, img c8dimages.Image, pm platforms.MatchComparer) (*ImageManifest, error) { | |
| var best *ImageManifest | |
| var bestPlatform ocispec.Platform | |
| err := i.walkImageManifests(ctx, img, func(im *ImageManifest) error { | |
| imPlatform, err := im.ImagePlatform(ctx) | |
| if err != nil { | |
| return err | |
| } | |
| if pm.Match(imPlatform) { | |
| if best == nil || pm.Less(imPlatform, bestPlatform) { | |
| best = im | |
| bestPlatform = imPlatform | |
| } | |
| } | |
| return nil | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if best == nil { | |
| err := &errPlatformNotFound{imageRef: imageFamiliarName(img)} | |
| if p, ok := pm.(platformMatcherWithRequestedPlatform); ok && p.Requested != nil { | |
| err.wanted = *p.Requested | |
| } | |
| return nil, err | |
| } | |
| return best, nil | |
| } | |
| // resolveDescriptor searches for a descriptor based on the given | |
| // reference or identifier. Returns the descriptor of | |
| // the image, which could be a manifest list, manifest, or config. | |
| func (i *ImageService) resolveDescriptor(ctx context.Context, refOrID string) (ocispec.Descriptor, error) { | |
| img, err := i.resolveImage(ctx, refOrID) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| return img.Target, nil | |
| } | |
| // ResolveImage looks up an image by reference or identifier in the image store. | |
| func (i *ImageService) ResolveImage(ctx context.Context, refOrID string) (c8dimages.Image, error) { | |
| return i.resolveImage(ctx, refOrID) | |
| } | |
| func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (c8dimages.Image, error) { | |
| parsed, err := reference.ParseAnyReference(refOrID) | |
| if err != nil { | |
| return c8dimages.Image{}, errdefs.InvalidParameter(err) | |
| } | |
| digested, ok := parsed.(reference.Digested) | |
| if ok { | |
| imgs, err := i.images.List(ctx, "target.digest=="+digested.Digest().String()) | |
| if err != nil { | |
| return c8dimages.Image{}, errors.Wrap(err, "failed to lookup digest") | |
| } | |
| if len(imgs) == 0 { | |
| return c8dimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| // If reference is both Named and Digested, make sure we don't match | |
| // images with a different repository even if digest matches. | |
| // For example, busybox@sha256:abcdef..., shouldn't match asdf@sha256:abcdef... | |
| if parsedNamed, ok := parsed.(reference.Named); ok { | |
| for _, img := range imgs { | |
| imgNamed, err := reference.ParseNormalizedNamed(img.Name) | |
| if err != nil { | |
| log.G(ctx).WithError(err).WithField("image", img.Name).Warn("image with invalid name encountered") | |
| continue | |
| } | |
| if parsedNamed.Name() == imgNamed.Name() { | |
| return img, nil | |
| } | |
| } | |
| return c8dimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| return imgs[0], nil | |
| } | |
| // Try resolve by name:tag | |
| ref := reference.TagNameOnly(parsed.(reference.Named)).String() | |
| if img, err := i.images.Get(ctx, ref); err == nil { | |
| return img, nil | |
| } else if !cerrdefs.IsNotFound(err) { | |
| return c8dimages.Image{}, err | |
| } | |
| // If the identifier could be a short ID, attempt to match. | |
| if idWithoutAlgo := checkTruncatedID(refOrID); idWithoutAlgo != "" { // Valid ID. | |
| filters := []string{ | |
| fmt.Sprintf("name==%q", ref), // Or it could just look like one. | |
| "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))), | |
| } | |
| imgs, err := i.images.List(ctx, filters...) | |
| if err != nil { | |
| return c8dimages.Image{}, err | |
| } | |
| if len(imgs) == 0 { | |
| return c8dimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| if len(imgs) > 1 { | |
| digests := map[digest.Digest]struct{}{} | |
| for _, img := range imgs { | |
| if img.Name == ref { | |
| return img, nil | |
| } | |
| digests[img.Target.Digest] = struct{}{} | |
| } | |
| if len(digests) > 1 { | |
| return c8dimages.Image{}, errdefs.NotFound(errors.New("ambiguous reference")) | |
| } | |
| } | |
| return imgs[0], nil | |
| } | |
| return c8dimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| // getAllImagesWithRepository returns a slice of images which name is a reference | |
| // pointing to the same repository as the given reference. | |
| func (i *ImageService) getAllImagesWithRepository(ctx context.Context, ref reference.Named) ([]c8dimages.Image, error) { | |
| nameFilter := "^" + regexp.QuoteMeta(ref.Name()) + ":" + reference.TagRegexp.String() + "$" | |
| return i.images.List(ctx, "name~="+strconv.Quote(nameFilter)) | |
| } | |
| func imageFamiliarName(img c8dimages.Image) string { | |
| if isDanglingImage(img) { | |
| return img.Target.Digest.String() | |
| } | |
| if ref, err := reference.ParseNamed(img.Name); err == nil { | |
| return reference.FamiliarString(ref) | |
| } | |
| return img.Name | |
| } | |
| // getImageLabelByDigest will return the value of the label for images | |
| // targeting the specified digest. | |
| // If images have different values, an errdefs.Conflict error will be returned. | |
| func (i *ImageService) getImageLabelByDigest(ctx context.Context, target digest.Digest, labelKey string) (string, error) { | |
| imgs, err := i.images.List(ctx, "target.digest=="+target.String()+",labels."+labelKey) | |
| if err != nil { | |
| return "", errdefs.System(err) | |
| } | |
| var value string | |
| for _, img := range imgs { | |
| if v, ok := img.Labels[labelKey]; ok { | |
| if value != "" && value != v { | |
| return value, errdefs.Conflict(fmt.Errorf("conflicting label value %q and %q", value, v)) | |
| } | |
| value = v | |
| } | |
| } | |
| return value, nil | |
| } | |
| // resolveAllReferences resolves the reference name or ID to an image and returns all the images with | |
| // the same target. | |
| // | |
| // Returns: | |
| // | |
| // 1: *(github.com/containerd/containerd/images).Image | |
| // | |
| // An image match from the image store with the provided refOrID | |
| // | |
| // 2: [](github.com/containerd/containerd/images).Image | |
| // | |
| // List of all images with the same target that matches the refOrID. If the first argument is | |
| // non-nil, the image list will all have the same target as the matched image. If the first | |
| // argument is nil but the list is non-empty, this value is a list of all the images with a | |
| // target that matches the digest provided in the refOrID, but none are an image name match | |
| // to refOrID. | |
| // | |
| // 3: error | |
| // | |
| // An error looking up refOrID or no images found with matching name or target. Note that the first | |
| // argument may be nil with a nil error if the second argument is non-empty. | |
| func (i *ImageService) resolveAllReferences(ctx context.Context, refOrID string) (*c8dimages.Image, []c8dimages.Image, error) { | |
| parsed, err := reference.ParseAnyReference(refOrID) | |
| if err != nil { | |
| return nil, nil, errdefs.InvalidParameter(err) | |
| } | |
| var dgst digest.Digest | |
| var img *c8dimages.Image | |
| if idWithoutAlgo := checkTruncatedID(refOrID); idWithoutAlgo != "" { // Valid ID. | |
| if d, ok := parsed.(reference.Digested); ok { | |
| if cimg, err := i.images.Get(ctx, d.String()); err == nil { | |
| img = &cimg | |
| dgst = d.Digest() | |
| if cimg.Target.Digest != dgst { | |
| // Ambiguous image reference, use reference name | |
| log.G(ctx).WithField("image", refOrID).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest") | |
| dgst = cimg.Target.Digest | |
| } | |
| } else if !cerrdefs.IsNotFound(err) { | |
| return nil, nil, err | |
| } else { | |
| dgst = d.Digest() | |
| } | |
| } else { | |
| name := reference.TagNameOnly(parsed.(reference.Named)).String() | |
| filters := []string{ | |
| fmt.Sprintf("name==%q", name), // Or it could just look like one. | |
| "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))), | |
| } | |
| imgs, err := i.images.List(ctx, filters...) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| if len(imgs) == 0 { | |
| return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| for _, limg := range imgs { | |
| if limg.Name == name { | |
| copyImg := limg | |
| img = ©Img | |
| } | |
| if dgst != "" { | |
| if limg.Target.Digest != dgst { | |
| return nil, nil, errdefs.NotFound(errors.New("ambiguous reference")) | |
| } | |
| } else { | |
| dgst = limg.Target.Digest | |
| } | |
| } | |
| // Return immediately if target digest matches already included | |
| if img == nil || len(imgs) > 1 { | |
| return img, imgs, nil | |
| } | |
| } | |
| } else { | |
| named, ok := parsed.(reference.Named) | |
| if !ok { | |
| return nil, nil, errdefs.InvalidParameter(errors.New("invalid name reference")) | |
| } | |
| digested, ok := parsed.(reference.Digested) | |
| if ok { | |
| dgst = digested.Digest() | |
| } | |
| name := reference.TagNameOnly(named).String() | |
| cimg, err := i.images.Get(ctx, name) | |
| if err != nil { | |
| if !cerrdefs.IsNotFound(err) { | |
| return nil, nil, err | |
| } | |
| // If digest is given, continue looking up for matching targets. | |
| // There will be no exact match found but the caller may attempt | |
| // to match across images with the matching target. | |
| if dgst == "" { | |
| return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| } else { | |
| img = &cimg | |
| if dgst != "" && img.Target.Digest != dgst { | |
| // Ambiguous image reference, use reference name | |
| log.G(ctx).WithField("image", name).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest") | |
| } | |
| dgst = img.Target.Digest | |
| } | |
| } | |
| // Lookup up all associated images and check for consistency with first reference | |
| // Ideally operations dependent on multiple values will rely on the garbage collector, | |
| // this logic will just check for consistency and throw an error | |
| imgs, err := i.images.List(ctx, "target.digest=="+dgst.String()) | |
| if err != nil { | |
| return nil, nil, errors.Wrap(err, "failed to lookup digest") | |
| } | |
| if len(imgs) == 0 { | |
| if img == nil { | |
| return nil, nil, images.ErrImageDoesNotExist{Ref: parsed} | |
| } | |
| err = errInconsistentData | |
| } else if img != nil { | |
| // Check to ensure the original img is in the list still | |
| err = errInconsistentData | |
| for _, rimg := range imgs { | |
| if rimg.Name == img.Name { | |
| err = nil | |
| break | |
| } | |
| } | |
| } | |
| if errors.Is(err, errInconsistentData) { | |
| if retries, ok := ctx.Value(errInconsistentData).(int); !ok || retries < 3 { | |
| log.G(ctx).WithFields(log.Fields{"retry": retries, "ref": refOrID}).Info("image changed during lookup, retrying") | |
| return i.resolveAllReferences(context.WithValue(ctx, errInconsistentData, retries+1), refOrID) | |
| } | |
| return nil, nil, err | |
| } | |
| return img, imgs, nil | |
| } | |
| // checkTruncatedID checks id for validity. If id is invalid, an empty string | |
| // is returned; otherwise, the ID without the optional "sha256:" prefix is | |
| // returned. The validity check is equivalent to | |
| // regexp.MustCompile(`^(sha256:)?([a-f0-9]{4,64})$`).MatchString(id). | |
| func checkTruncatedID(id string) string { | |
| id = strings.TrimPrefix(id, "sha256:") | |
| if l := len(id); l < 4 || l > 64 { | |
| return "" | |
| } | |
| for _, c := range id { | |
| if (c < '0' || c > '9') && (c < 'a' || c > 'f') { | |
| return "" | |
| } | |
| } | |
| return id | |
| } |