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