github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/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  	"github.com/docker/docker/errdefs"
    16  	"github.com/docker/docker/image"
    17  	digest "github.com/opencontainers/go-digest"
    18  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  // ErrImageDoesNotExist is error returned when no image can be found for a reference.
    24  type ErrImageDoesNotExist struct {
    25  	ref reference.Reference
    26  }
    27  
    28  func (e ErrImageDoesNotExist) Error() string {
    29  	ref := e.ref
    30  	if named, ok := ref.(reference.Named); ok {
    31  		ref = reference.TagNameOnly(named)
    32  	}
    33  	return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref))
    34  }
    35  
    36  // NotFound implements the NotFound interface
    37  func (e ErrImageDoesNotExist) NotFound() {}
    38  
    39  type manifestList struct {
    40  	Manifests []specs.Descriptor `json:"manifests"`
    41  }
    42  
    43  type manifest struct {
    44  	Config specs.Descriptor `json:"config"`
    45  }
    46  
    47  func (i *ImageService) manifestMatchesPlatform(img *image.Image, platform specs.Platform) bool {
    48  	ctx := context.TODO()
    49  	logger := logrus.WithField("image", img.ID).WithField("desiredPlatform", platforms.Format(platform))
    50  
    51  	ls, leaseErr := i.leases.ListResources(context.TODO(), leases.Lease{ID: imageKey(img.ID().Digest())})
    52  	if leaseErr != nil {
    53  		logger.WithError(leaseErr).Error("Error looking up image leases")
    54  		return false
    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
   141  			}
   142  
   143  			logger.WithField("otherDigest", md.Digest).Debug("Skipping non-matching manifest")
   144  		}
   145  	}
   146  
   147  	return false
   148  }
   149  
   150  // GetImage returns an image corresponding to the image referred to by refOrID.
   151  func (i *ImageService) GetImage(refOrID string, platform *specs.Platform) (retImg *image.Image, retErr error) {
   152  	defer func() {
   153  		if retErr != nil || retImg == nil || 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 := *platform
   163  		// Note that `platforms.Only` will fuzzy match this for us
   164  		// For example: an armv6 image will run just fine an 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 coresponds to this imaage to check if at least the manifest list says it is the correct image.
   170  		if i.manifestMatchesPlatform(retImg, p) {
   171  			return
   172  		}
   173  
   174  		// This allows us to tell clients that we don't have the image they asked for
   175  		// Where this gets hairy is the image store does not currently support multi-arch images, e.g.:
   176  		//   An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform
   177  		//   The image store does not store the manifest list and image tags are assigned to architecture specific images.
   178  		//   So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images.
   179  		//   This may be confusing.
   180  		//   The alternative to this is to return a errdefs.Conflict error with a helpful message, but clients will not be
   181  		//   able to automatically tell what causes the conflict.
   182  		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)))
   183  	}()
   184  	ref, err := reference.ParseAnyReference(refOrID)
   185  	if err != nil {
   186  		return nil, errdefs.InvalidParameter(err)
   187  	}
   188  	namedRef, ok := ref.(reference.Named)
   189  	if !ok {
   190  		digested, ok := ref.(reference.Digested)
   191  		if !ok {
   192  			return nil, ErrImageDoesNotExist{ref}
   193  		}
   194  		id := image.IDFromDigest(digested.Digest())
   195  		if img, err := i.imageStore.Get(id); err == nil {
   196  			return img, nil
   197  		}
   198  		return nil, ErrImageDoesNotExist{ref}
   199  	}
   200  
   201  	if digest, err := i.referenceStore.Get(namedRef); err == nil {
   202  		// Search the image stores to get the operating system, defaulting to host OS.
   203  		id := image.IDFromDigest(digest)
   204  		if img, err := i.imageStore.Get(id); err == nil {
   205  			return img, nil
   206  		}
   207  	}
   208  
   209  	// Search based on ID
   210  	if id, err := i.imageStore.Search(refOrID); err == nil {
   211  		img, err := i.imageStore.Get(id)
   212  		if err != nil {
   213  			return nil, ErrImageDoesNotExist{ref}
   214  		}
   215  		return img, nil
   216  	}
   217  
   218  	return nil, ErrImageDoesNotExist{ref}
   219  }
   220  
   221  // OnlyPlatformWithFallback uses `platforms.Only` with a fallback to handle the case where the platform
   222  // being matched does not have a CPU variant.
   223  //
   224  // The reason for this is that CPU variant is not even if the official image config spec as of this writing.
   225  // See: https://github.com/opencontainers/image-spec/pull/809
   226  // Since Docker tends to compare platforms from the image config, we need to handle this case.
   227  func OnlyPlatformWithFallback(p specs.Platform) platforms.Matcher {
   228  	return &onlyFallbackMatcher{only: platforms.Only(p), p: platforms.Normalize(p)}
   229  }
   230  
   231  type onlyFallbackMatcher struct {
   232  	only platforms.Matcher
   233  	p    specs.Platform
   234  }
   235  
   236  func (m *onlyFallbackMatcher) Match(other specs.Platform) bool {
   237  	if m.only.Match(other) {
   238  		// It matches, no reason to fallback
   239  		return true
   240  	}
   241  	if other.Variant != "" {
   242  		// If there is a variant then this fallback does not apply, and there is no match
   243  		return false
   244  	}
   245  	otherN := platforms.Normalize(other)
   246  	otherN.Variant = "" // normalization adds a default variant... which is the whole problem with `platforms.Only`
   247  
   248  	return m.p.OS == otherN.OS &&
   249  		m.p.Architecture == otherN.Architecture
   250  }