mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
This package was aliased as "imagespec" in some places, and "dockerspec" in other places, which made it easy to confuse. Change all uses of this package to be aliased as "dockerspec" and configure an "importas" linting check to enforce it. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
412 lines
13 KiB
Go
412 lines
13 KiB
Go
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
|
|
}
|