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