github.com/moby/docker@v26.1.3+incompatible/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  	cerrdefs "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/containerd/log"
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/docker/api/types/backend"
    17  	"github.com/docker/docker/errdefs"
    18  	"github.com/docker/docker/image"
    19  	"github.com/docker/docker/layer"
    20  	"github.com/opencontainers/go-digest"
    21  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  // ErrImageDoesNotExist is error returned when no image can be found for a reference.
    26  type ErrImageDoesNotExist struct {
    27  	Ref reference.Reference
    28  }
    29  
    30  func (e ErrImageDoesNotExist) Error() string {
    31  	ref := e.Ref
    32  	if named, ok := ref.(reference.Named); ok {
    33  		ref = reference.TagNameOnly(named)
    34  	}
    35  	return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref))
    36  }
    37  
    38  // NotFound implements the NotFound interface
    39  func (e ErrImageDoesNotExist) NotFound() {}
    40  
    41  type manifestList struct {
    42  	Manifests []ocispec.Descriptor `json:"manifests"`
    43  }
    44  
    45  type manifest struct {
    46  	Config ocispec.Descriptor `json:"config"`
    47  }
    48  
    49  func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform, setupInit func(string) error) error {
    50  	// Only makes sense when containerd image store is used
    51  	panic("not implemented")
    52  }
    53  
    54  func (i *ImageService) manifestMatchesPlatform(ctx context.Context, img *image.Image, platform ocispec.Platform) (bool, error) {
    55  	ls, err := i.leases.ListResources(ctx, leases.Lease{ID: imageKey(img.ID().String())})
    56  	if err != nil {
    57  		if cerrdefs.IsNotFound(err) {
    58  			return false, nil
    59  		}
    60  		log.G(ctx).WithFields(log.Fields{
    61  			"error":           err,
    62  			"image":           img.ID,
    63  			"desiredPlatform": platforms.Format(platform),
    64  		}).Error("Error looking up image leases")
    65  		return false, err
    66  	}
    67  
    68  	// Note we are comparing against manifest lists here, which we expect to always have a CPU variant set (where applicable).
    69  	// So there is no need for the fallback matcher here.
    70  	comparer := platforms.Only(platform)
    71  
    72  	var (
    73  		ml manifestList
    74  		m  manifest
    75  	)
    76  
    77  	makeRdr := func(ra content.ReaderAt) io.Reader {
    78  		return io.LimitReader(io.NewSectionReader(ra, 0, ra.Size()), 1e6)
    79  	}
    80  
    81  	for _, r := range ls {
    82  		logger := log.G(ctx).WithFields(log.Fields{
    83  			"image":           img.ID,
    84  			"desiredPlatform": platforms.Format(platform),
    85  			"resourceID":      r.ID,
    86  			"resourceType":    r.Type,
    87  		})
    88  		logger.Debug("Checking lease resource for platform match")
    89  		if r.Type != "content" {
    90  			continue
    91  		}
    92  
    93  		ra, err := i.content.ReaderAt(ctx, ocispec.Descriptor{Digest: digest.Digest(r.ID)})
    94  		if err != nil {
    95  			if cerrdefs.IsNotFound(err) {
    96  				continue
    97  			}
    98  			logger.WithError(err).Error("Error looking up referenced manifest list for image")
    99  			continue
   100  		}
   101  
   102  		data, err := io.ReadAll(makeRdr(ra))
   103  		ra.Close()
   104  
   105  		if err != nil {
   106  			logger.WithError(err).Error("Error reading manifest list for image")
   107  			continue
   108  		}
   109  
   110  		ml.Manifests = nil
   111  
   112  		if err := json.Unmarshal(data, &ml); err != nil {
   113  			logger.WithError(err).Error("Error unmarshalling content")
   114  			continue
   115  		}
   116  
   117  		for _, md := range ml.Manifests {
   118  			switch md.MediaType {
   119  			case ocispec.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
   120  			default:
   121  				continue
   122  			}
   123  
   124  			p := ocispec.Platform{
   125  				Architecture: md.Platform.Architecture,
   126  				OS:           md.Platform.OS,
   127  				Variant:      md.Platform.Variant,
   128  			}
   129  			if !comparer.Match(p) {
   130  				logger.WithField("otherPlatform", platforms.Format(p)).Debug("Manifest is not a match")
   131  				continue
   132  			}
   133  
   134  			// 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.
   135  
   136  			ra, err := i.content.ReaderAt(ctx, ocispec.Descriptor{Digest: md.Digest})
   137  			if err != nil {
   138  				logger.WithField("otherDigest", md.Digest).WithError(err).Error("Could not get reader for manifest")
   139  				continue
   140  			}
   141  
   142  			data, err := io.ReadAll(makeRdr(ra))
   143  			ra.Close()
   144  			if err != nil {
   145  				logger.WithError(err).Error("Error reading manifest for image")
   146  				continue
   147  			}
   148  
   149  			if err := json.Unmarshal(data, &m); err != nil {
   150  				logger.WithError(err).Error("Error desserializing manifest")
   151  				continue
   152  			}
   153  
   154  			if m.Config.Digest == img.ID().Digest() {
   155  				logger.WithField("manifestDigest", md.Digest).Debug("Found matching manifest for image")
   156  				return true, nil
   157  			}
   158  
   159  			logger.WithField("otherDigest", md.Digest).Debug("Skipping non-matching manifest")
   160  		}
   161  	}
   162  
   163  	return false, nil
   164  }
   165  
   166  // GetImage returns an image corresponding to the image referred to by refOrID.
   167  func (i *ImageService) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error) {
   168  	img, err := i.getImage(ctx, refOrID, options)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	if options.Details {
   173  		var size int64
   174  		var layerMetadata map[string]string
   175  		layerID := img.RootFS.ChainID()
   176  		if layerID != "" {
   177  			l, err := i.layerStore.Get(layerID)
   178  			if err != nil {
   179  				return nil, err
   180  			}
   181  			defer layer.ReleaseAndLog(i.layerStore, l)
   182  			size = l.Size()
   183  			layerMetadata, err = l.Metadata()
   184  			if err != nil {
   185  				return nil, err
   186  			}
   187  		}
   188  
   189  		lastUpdated, err := i.imageStore.GetLastUpdated(img.ID())
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		img.Details = &image.Details{
   194  			References:  i.referenceStore.References(img.ID().Digest()),
   195  			Size:        size,
   196  			Metadata:    layerMetadata,
   197  			Driver:      i.layerStore.DriverName(),
   198  			LastUpdated: lastUpdated,
   199  		}
   200  	}
   201  	return img, nil
   202  }
   203  
   204  func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options backend.GetImageOpts) (*ocispec.Descriptor, error) {
   205  	panic("not implemented")
   206  }
   207  
   208  func (i *ImageService) getImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (retImg *image.Image, retErr error) {
   209  	defer func() {
   210  		if retErr != nil || retImg == nil || options.Platform == nil {
   211  			return
   212  		}
   213  
   214  		imgPlat := ocispec.Platform{
   215  			OS:           retImg.OS,
   216  			Architecture: retImg.Architecture,
   217  			Variant:      retImg.Variant,
   218  		}
   219  		p := *options.Platform
   220  		// Note that `platforms.Only` will fuzzy match this for us
   221  		// For example: an armv6 image will run just fine on an armv7 CPU, without emulation or anything.
   222  		if OnlyPlatformWithFallback(p).Match(imgPlat) {
   223  			return
   224  		}
   225  		// In some cases the image config can actually be wrong (e.g. classic `docker build` may not handle `--platform` correctly)
   226  		// 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.
   227  		var matches bool
   228  		matches, retErr = i.manifestMatchesPlatform(ctx, retImg, p)
   229  		if matches || retErr != nil {
   230  			return
   231  		}
   232  
   233  		// This allows us to tell clients that we don't have the image they asked for
   234  		// Where this gets hairy is the image store does not currently support multi-arch images, e.g.:
   235  		//   An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform
   236  		//   The image store does not store the manifest list and image tags are assigned to architecture specific images.
   237  		//   So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images.
   238  		//   This may be confusing.
   239  		//   The alternative to this is to return an errdefs.Conflict error with a helpful message, but clients will not be
   240  		//   able to automatically tell what causes the conflict.
   241  		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)))
   242  	}()
   243  	ref, err := reference.ParseAnyReference(refOrID)
   244  	if err != nil {
   245  		return nil, errdefs.InvalidParameter(err)
   246  	}
   247  	namedRef, ok := ref.(reference.Named)
   248  	if !ok {
   249  		digested, ok := ref.(reference.Digested)
   250  		if !ok {
   251  			return nil, ErrImageDoesNotExist{Ref: ref}
   252  		}
   253  		if img, err := i.imageStore.Get(image.ID(digested.Digest())); err == nil {
   254  			return img, nil
   255  		}
   256  		return nil, ErrImageDoesNotExist{Ref: ref}
   257  	}
   258  
   259  	if dgst, err := i.referenceStore.Get(namedRef); err == nil {
   260  		// Search the image stores to get the operating system, defaulting to host OS.
   261  		if img, err := i.imageStore.Get(image.ID(dgst)); err == nil {
   262  			return img, nil
   263  		}
   264  	}
   265  
   266  	// Search based on ID
   267  	if id, err := i.imageStore.Search(refOrID); err == nil {
   268  		img, err := i.imageStore.Get(id)
   269  		if err != nil {
   270  			return nil, ErrImageDoesNotExist{Ref: ref}
   271  		}
   272  		return img, nil
   273  	}
   274  
   275  	return nil, ErrImageDoesNotExist{Ref: ref}
   276  }
   277  
   278  // OnlyPlatformWithFallback uses `platforms.Only` with a fallback to handle the case where the platform
   279  // being matched does not have a CPU variant.
   280  //
   281  // The reason for this is that CPU variant is not even if the official image config spec as of this writing.
   282  // See: https://github.com/opencontainers/image-spec/pull/809
   283  // Since Docker tends to compare platforms from the image config, we need to handle this case.
   284  func OnlyPlatformWithFallback(p ocispec.Platform) platforms.Matcher {
   285  	return &onlyFallbackMatcher{only: platforms.Only(p), p: platforms.Normalize(p)}
   286  }
   287  
   288  type onlyFallbackMatcher struct {
   289  	only platforms.Matcher
   290  	p    ocispec.Platform
   291  }
   292  
   293  func (m *onlyFallbackMatcher) Match(other ocispec.Platform) bool {
   294  	if m.only.Match(other) {
   295  		// It matches, no reason to fallback
   296  		return true
   297  	}
   298  	if other.Variant != "" {
   299  		// If there is a variant then this fallback does not apply, and there is no match
   300  		return false
   301  	}
   302  	otherN := platforms.Normalize(other)
   303  	otherN.Variant = "" // normalization adds a default variant... which is the whole problem with `platforms.Only`
   304  
   305  	return m.p.OS == otherN.OS &&
   306  		m.p.Architecture == otherN.Architecture
   307  }