mirror of
https://github.com/moby/moby.git
synced 2026-07-13 02:52:20 +00:00
- Changed "match" to "provide", in an attempt to indicate that the image is
a multi-platform image that doesn't contain the given platform.
- Remove the "wanted" and instead make the requested platforms to be a part
of the sentence, but within braces.
Before this patch:
docker pull --quiet --platform=linux/riscv64 alpine:latest
docker image history --platform=linux/amd64 alpine
Error response from daemon: image with reference alpine:latest was found but does not match the specified platform: wanted linux/nosuch
With this patch:
docker pull --quiet --platform=linux/riscv64 alpine:latest
docker image history --platform=linux/amd64 alpine
Error response from daemon: image with reference alpine:latest was found but does not provide the specified platform (linux/amd64)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
440 lines
14 KiB
Go
440 lines
14 KiB
Go
package containerd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
containerdimages "github.com/containerd/containerd/images"
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/containerd/log"
|
|
"github.com/containerd/platforms"
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/docker/api/types/backend"
|
|
"github.com/docker/docker/daemon/images"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/image"
|
|
imagespec "github.com/moby/docker-image-spec/specs-go/v1"
|
|
"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 the specified platform"
|
|
if e.wanted.OS != "" {
|
|
msg += " (" + platforms.FormatAll(e.wanted) + ")"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// GetImage returns an image corresponding to the image referred to by refOrID.
|
|
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error) {
|
|
img, err := i.resolveImage(ctx, refOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pm := i.matchRequestedOrDefault(platforms.OnlyStrict, options.Platform)
|
|
|
|
imgV1, err := i.getImageV1(ctx, img, pm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return imgV1, nil
|
|
}
|
|
|
|
// getImageV1 gets the containerd image as a docker v1 image struct.
|
|
func (i *ImageService) getImageV1(ctx context.Context, img containerdimages.Image, platform platforms.MatchComparer) (*image.Image, error) {
|
|
im, err := i.getBestPresentImageManifest(ctx, img, platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ociImage imagespec.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)
|
|
}
|
|
|
|
return imgV1, nil
|
|
}
|
|
|
|
func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options backend.GetImageOpts) (*ocispec.Descriptor, error) {
|
|
img, err := i.resolveImage(ctx, refOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pm := i.matchRequestedOrDefault(platforms.Only, options.Platform)
|
|
|
|
im, err := i.getBestPresentImageManifest(ctx, img, pm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
desc := im.Target()
|
|
return &desc, 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 containerdimages.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) (containerdimages.Image, error) {
|
|
return i.resolveImage(ctx, refOrID)
|
|
}
|
|
|
|
func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (containerdimages.Image, error) {
|
|
parsed, err := reference.ParseAnyReference(refOrID)
|
|
if err != nil {
|
|
return containerdimages.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 containerdimages.Image{}, errors.Wrap(err, "failed to lookup digest")
|
|
}
|
|
if len(imgs) == 0 {
|
|
return containerdimages.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 containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
|
|
}
|
|
|
|
return imgs[0], nil
|
|
}
|
|
|
|
ref := reference.TagNameOnly(parsed.(reference.Named)).String()
|
|
img, err := i.images.Get(ctx, ref)
|
|
if err == nil {
|
|
return img, nil
|
|
} else {
|
|
// TODO(containerd): error translation can use common function
|
|
if !cerrdefs.IsNotFound(err) {
|
|
return containerdimages.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 containerdimages.Image{}, err
|
|
}
|
|
|
|
if len(imgs) == 0 {
|
|
return containerdimages.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 containerdimages.Image{}, errdefs.NotFound(errors.New("ambiguous reference"))
|
|
}
|
|
}
|
|
|
|
return imgs[0], nil
|
|
}
|
|
|
|
return containerdimages.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) ([]containerdimages.Image, error) {
|
|
nameFilter := "^" + regexp.QuoteMeta(ref.Name()) + ":" + reference.TagRegexp.String() + "$"
|
|
return i.images.List(ctx, "name~="+strconv.Quote(nameFilter))
|
|
}
|
|
|
|
func imageFamiliarName(img containerdimages.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
|
|
}
|
|
|
|
func convertError(err error) error {
|
|
// TODO: Convert containerd error to Docker error
|
|
return err
|
|
}
|
|
|
|
// 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) (*containerdimages.Image, []containerdimages.Image, error) {
|
|
parsed, err := reference.ParseAnyReference(refOrID)
|
|
if err != nil {
|
|
return nil, nil, errdefs.InvalidParameter(err)
|
|
}
|
|
var dgst digest.Digest
|
|
var img *containerdimages.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, convertError(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, convertError(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, convertError(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
|
|
}
|