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