github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/daemon/images/image.go (about)

     1  package images // import "github.com/docker/docker/daemon/images"
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  
    10  	"github.com/containerd/containerd/content"
    11  	c8derrdefs "github.com/containerd/containerd/errdefs"
    12  	"github.com/containerd/containerd/images"
    13  	"github.com/containerd/containerd/leases"
    14  	"github.com/containerd/containerd/platforms"
    15  	"github.com/docker/distribution/reference"
    16  	"github.com/docker/docker/errdefs"
    17  	"github.com/docker/docker/image"
    18  	digest "github.com/opencontainers/go-digest"
    19  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    20  	"github.com/pkg/errors"
    21  	"github.com/sirupsen/logrus"
    22  )
    23  
    24  // ErrImageDoesNotExist is error returned when no image can be found for a reference.
    25  type ErrImageDoesNotExist struct {
    26  	ref reference.Reference
    27  }
    28  
    29  func (e ErrImageDoesNotExist) Error() string {
    30  	ref := e.ref
    31  	if named, ok := ref.(reference.Named); ok {
    32  		ref = reference.TagNameOnly(named)
    33  	}
    34  	return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref))
    35  }
    36  
    37  // NotFound implements the NotFound interface
    38  func (e ErrImageDoesNotExist) NotFound() {}
    39  
    40  type manifestList struct {
    41  	Manifests []specs.Descriptor `json:"manifests"`
    42  }
    43  
    44  type manifest struct {
    45  	Config specs.Descriptor `json:"config"`
    46  }
    47  
    48  func (i *ImageService) manifestMatchesPlatform(img *image.Image, platform specs.Platform) bool {
    49  	ctx := context.TODO()
    50  	logger := logrus.WithField("image", img.ID).WithField("desiredPlatform", platforms.Format(platform))
    51  
    52  	ls, leaseErr := i.leases.ListResources(context.TODO(), leases.Lease{ID: imageKey(img.ID().Digest())})
    53  	if leaseErr != nil {
    54  		logger.WithError(leaseErr).Error("Error looking up image leases")
    55  		return false
    56  	}
    57  
    58  	// Note we are comparing against manifest lists here, which we expect to always have a CPU variant set (where applicable).
    59  	// So there is no need for the fallback matcher here.
    60  	comparer := platforms.Only(platform)
    61  
    62  	var (
    63  		ml manifestList
    64  		m  manifest
    65  	)
    66  
    67  	makeRdr := func(ra content.ReaderAt) io.Reader {
    68  		return io.LimitReader(io.NewSectionReader(ra, 0, ra.Size()), 1e6)
    69  	}
    70  
    71  	for _, r := range ls {
    72  		logger := logger.WithField("resourceID", r.ID).WithField("resourceType", r.Type)
    73  		logger.Debug("Checking lease resource for platform match")
    74  		if r.Type != "content" {
    75  			continue
    76  		}
    77  
    78  		ra, err := i.content.ReaderAt(ctx, specs.Descriptor{Digest: digest.Digest(r.ID)})
    79  		if err != nil {
    80  			if c8derrdefs.IsNotFound(err) {
    81  				continue
    82  			}
    83  			logger.WithError(err).Error("Error looking up referenced manifest list for image")
    84  			continue
    85  		}
    86  
    87  		data, err := ioutil.ReadAll(makeRdr(ra))
    88  		ra.Close()
    89  
    90  		if err != nil {
    91  			logger.WithError(err).Error("Error reading manifest list for image")
    92  			continue
    93  		}
    94  
    95  		ml.Manifests = nil
    96  
    97  		if err := json.Unmarshal(data, &ml); err != nil {
    98  			logger.WithError(err).Error("Error unmarshalling content")
    99  			continue
   100  		}
   101  
   102  		for _, md := range ml.Manifests {
   103  			switch md.MediaType {
   104  			case specs.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
   105  			default:
   106  				continue
   107  			}
   108  
   109  			p := specs.Platform{
   110  				Architecture: md.Platform.Architecture,
   111  				OS:           md.Platform.OS,
   112  				Variant:      md.Platform.Variant,
   113  			}
   114  			if !comparer.Match(p) {
   115  				logger.WithField("otherPlatform", platforms.Format(p)).Debug("Manifest is not a match")
   116  				continue
   117  			}
   118  
   119  			// Here we have a platform match for the referenced manifest, let's make sure the manifest is actually for the image config we are using.
   120  
   121  			ra, err := i.content.ReaderAt(ctx, specs.Descriptor{Digest: md.Digest})
   122  			if err != nil {
   123  				logger.WithField("otherDigest", md.Digest).WithError(err).Error("Could not get reader for manifest")
   124  				continue
   125  			}
   126  
   127  			data, err := ioutil.ReadAll(makeRdr(ra))
   128  			ra.Close()
   129  			if err != nil {
   130  				logger.WithError(err).Error("Error reading manifest for image")
   131  				continue
   132  			}
   133  
   134  			if err := json.Unmarshal(data, &m); err != nil {
   135  				logger.WithError(err).Error("Error desserializing manifest")
   136  				continue
   137  			}
   138  
   139  			if m.Config.Digest == img.ID().Digest() {
   140  				logger.WithField("manifestDigest", md.Digest).Debug("Found matching manifest for image")
   141  				return true
   142  			}
   143  
   144  			logger.WithField("otherDigest", md.Digest).Debug("Skipping non-matching manifest")
   145  		}
   146  	}
   147  
   148  	return false
   149  }
   150  
   151  // GetImage returns an image corresponding to the image referred to by refOrID.
   152  func (i *ImageService) GetImage(refOrID string, platform *specs.Platform) (retImg *image.Image, retErr error) {
   153  	defer func() {
   154  		if retErr != nil || retImg == nil || platform == nil {
   155  			return
   156  		}
   157  
   158  		imgPlat := specs.Platform{
   159  			OS:           retImg.OS,
   160  			Architecture: retImg.Architecture,
   161  			Variant:      retImg.Variant,
   162  		}
   163  		p := *platform
   164  		// Note that `platforms.Only` will fuzzy match this for us
   165  		// For example: an armv6 image will run just fine an an armv7 CPU, without emulation or anything.
   166  		if OnlyPlatformWithFallback(p).Match(imgPlat) {
   167  			return
   168  		}
   169  		// In some cases the image config can actually be wrong (e.g. classic `docker build` may not handle `--platform` correctly)
   170  		// So we'll look up the manifest list that coresponds to this imaage to check if at least the manifest list says it is the correct image.
   171  		if i.manifestMatchesPlatform(retImg, p) {
   172  			return
   173  		}
   174  
   175  		// This allows us to tell clients that we don't have the image they asked for
   176  		// Where this gets hairy is the image store does not currently support multi-arch images, e.g.:
   177  		//   An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform
   178  		//   The image store does not store the manifest list and image tags are assigned to architecture specific images.
   179  		//   So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images.
   180  		//   This may be confusing.
   181  		//   The alternative to this is to return a errdefs.Conflict error with a helpful message, but clients will not be
   182  		//   able to automatically tell what causes the conflict.
   183  		retErr = errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s", refOrID, platforms.Format(p), platforms.Format(imgPlat)))
   184  	}()
   185  	ref, err := reference.ParseAnyReference(refOrID)
   186  	if err != nil {
   187  		return nil, errdefs.InvalidParameter(err)
   188  	}
   189  	namedRef, ok := ref.(reference.Named)
   190  	if !ok {
   191  		digested, ok := ref.(reference.Digested)
   192  		if !ok {
   193  			return nil, ErrImageDoesNotExist{ref}
   194  		}
   195  		id := image.IDFromDigest(digested.Digest())
   196  		if img, err := i.imageStore.Get(id); err == nil {
   197  			return img, nil
   198  		}
   199  		return nil, ErrImageDoesNotExist{ref}
   200  	}
   201  
   202  	if digest, err := i.referenceStore.Get(namedRef); err == nil {
   203  		// Search the image stores to get the operating system, defaulting to host OS.
   204  		id := image.IDFromDigest(digest)
   205  		if img, err := i.imageStore.Get(id); err == nil {
   206  			return img, nil
   207  		}
   208  	}
   209  
   210  	// Search based on ID
   211  	if id, err := i.imageStore.Search(refOrID); err == nil {
   212  		img, err := i.imageStore.Get(id)
   213  		if err != nil {
   214  			return nil, ErrImageDoesNotExist{ref}
   215  		}
   216  		return img, nil
   217  	}
   218  
   219  	return nil, ErrImageDoesNotExist{ref}
   220  }
   221  
   222  // OnlyPlatformWithFallback uses `platforms.Only` with a fallback to handle the case where the platform
   223  //  being matched does not have a CPU variant.
   224  //
   225  // The reason for this is that CPU variant is not even if the official image config spec as of this writing.
   226  // See: https://github.com/opencontainers/image-spec/pull/809
   227  // Since Docker tends to compare platforms from the image config, we need to handle this case.
   228  func OnlyPlatformWithFallback(p specs.Platform) platforms.Matcher {
   229  	return &onlyFallbackMatcher{only: platforms.Only(p), p: platforms.Normalize(p)}
   230  }
   231  
   232  type onlyFallbackMatcher struct {
   233  	only platforms.Matcher
   234  	p    specs.Platform
   235  }
   236  
   237  func (m *onlyFallbackMatcher) Match(other specs.Platform) bool {
   238  	if m.only.Match(other) {
   239  		// It matches, no reason to fallback
   240  		return true
   241  	}
   242  	if other.Variant != "" {
   243  		// If there is a variant then this fallback does not apply, and there is no match
   244  		return false
   245  	}
   246  	otherN := platforms.Normalize(other)
   247  	otherN.Variant = "" // normalization adds a default variant... which is the whole problem with `platforms.Only`
   248  
   249  	return m.p.OS == otherN.OS &&
   250  		m.p.Architecture == otherN.Architecture
   251  }