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 = &copyImg
   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  }