github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_list.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"runtime"
     7  	"sort"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/containerd/containerd/content"
    13  	cerrdefs "github.com/containerd/containerd/errdefs"
    14  	"github.com/containerd/containerd/images"
    15  	"github.com/containerd/containerd/labels"
    16  	"github.com/containerd/containerd/platforms"
    17  	cplatforms "github.com/containerd/containerd/platforms"
    18  	"github.com/containerd/containerd/snapshots"
    19  	"github.com/containerd/log"
    20  	"github.com/distribution/reference"
    21  	"github.com/docker/docker/api/types/backend"
    22  	"github.com/docker/docker/api/types/filters"
    23  	imagetypes "github.com/docker/docker/api/types/image"
    24  	timetypes "github.com/docker/docker/api/types/time"
    25  	"github.com/docker/docker/container"
    26  	"github.com/docker/docker/errdefs"
    27  	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
    28  	"github.com/opencontainers/go-digest"
    29  	"github.com/opencontainers/image-spec/identity"
    30  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    31  	"github.com/pkg/errors"
    32  	"golang.org/x/sync/errgroup"
    33  )
    34  
    35  // Subset of ocispec.Image that only contains Labels
    36  type configLabels struct {
    37  	// Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6.
    38  	Created *time.Time `json:"created,omitempty"`
    39  
    40  	Config struct {
    41  		Labels map[string]string `json:"Labels,omitempty"`
    42  	} `json:"config,omitempty"`
    43  }
    44  
    45  var acceptedImageFilterTags = map[string]bool{
    46  	"dangling":  true,
    47  	"label":     true,
    48  	"label!":    true,
    49  	"before":    true,
    50  	"since":     true,
    51  	"reference": true,
    52  	"until":     true,
    53  }
    54  
    55  // byCreated is a temporary type used to sort a list of images by creation
    56  // time.
    57  type byCreated []*imagetypes.Summary
    58  
    59  func (r byCreated) Len() int           { return len(r) }
    60  func (r byCreated) Swap(i, j int)      { r[i], r[j] = r[j], r[i] }
    61  func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created }
    62  
    63  // Images returns a filtered list of images.
    64  //
    65  // TODO(thaJeztah): verify behavior of `RepoDigests` and `RepoTags` for images without (untagged) or multiple tags; see https://github.com/moby/moby/issues/43861
    66  // TODO(thaJeztah): verify "Size" vs "VirtualSize" in images; see https://github.com/moby/moby/issues/43862
    67  func (i *ImageService) Images(ctx context.Context, opts imagetypes.ListOptions) ([]*imagetypes.Summary, error) {
    68  	if err := opts.Filters.Validate(acceptedImageFilterTags); err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	filter, err := i.setupFilters(ctx, opts.Filters)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	imgs, err := i.images.List(ctx)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	// TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273
    83  	snapshotter := i.snapshotterService(i.snapshotter)
    84  	sizeCache := make(map[digest.Digest]int64)
    85  	snapshotSizeFn := func(d digest.Digest) (int64, error) {
    86  		if s, ok := sizeCache[d]; ok {
    87  			return s, nil
    88  		}
    89  		usage, err := snapshotter.Usage(ctx, d.String())
    90  		if err != nil {
    91  			return 0, err
    92  		}
    93  		sizeCache[d] = usage.Size
    94  		return usage.Size, nil
    95  	}
    96  
    97  	uniqueImages := map[digest.Digest]images.Image{}
    98  	tagsByDigest := map[digest.Digest][]string{}
    99  	intermediateImages := map[digest.Digest]struct{}{}
   100  
   101  	hideIntermediate := !opts.All
   102  	if hideIntermediate {
   103  		for _, img := range imgs {
   104  			parent, ok := img.Labels[imageLabelClassicBuilderParent]
   105  			if ok && parent != "" {
   106  				dgst, err := digest.Parse(parent)
   107  				if err != nil {
   108  					log.G(ctx).WithFields(log.Fields{
   109  						"error": err,
   110  						"value": parent,
   111  					}).Warnf("invalid %s label value", imageLabelClassicBuilderParent)
   112  				}
   113  				intermediateImages[dgst] = struct{}{}
   114  			}
   115  		}
   116  	}
   117  
   118  	// TODO: Allow platform override?
   119  	platformMatcher := matchAllWithPreference(cplatforms.Default())
   120  
   121  	for _, img := range imgs {
   122  		isDangling := isDanglingImage(img)
   123  
   124  		if hideIntermediate && isDangling {
   125  			if _, ok := intermediateImages[img.Target.Digest]; ok {
   126  				continue
   127  			}
   128  		}
   129  
   130  		if !filter(img) {
   131  			continue
   132  		}
   133  
   134  		dgst := img.Target.Digest
   135  		uniqueImages[dgst] = img
   136  
   137  		if isDangling {
   138  			continue
   139  		}
   140  
   141  		ref, err := reference.ParseNormalizedNamed(img.Name)
   142  		if err != nil {
   143  			continue
   144  		}
   145  		tagsByDigest[dgst] = append(tagsByDigest[dgst], reference.FamiliarString(ref))
   146  	}
   147  
   148  	resultsMut := sync.Mutex{}
   149  	eg, egCtx := errgroup.WithContext(ctx)
   150  	eg.SetLimit(runtime.NumCPU() * 2)
   151  
   152  	var (
   153  		summaries = make([]*imagetypes.Summary, 0, len(imgs))
   154  		root      []*[]digest.Digest
   155  		layers    map[digest.Digest]int
   156  	)
   157  	if opts.SharedSize {
   158  		root = make([]*[]digest.Digest, 0, len(imgs))
   159  		layers = make(map[digest.Digest]int)
   160  	}
   161  
   162  	for _, img := range uniqueImages {
   163  		img := img
   164  		eg.Go(func() error {
   165  			image, allChainsIDs, err := i.imageSummary(egCtx, img, platformMatcher, opts, tagsByDigest)
   166  			if err != nil {
   167  				return err
   168  			}
   169  			// No error, but image should be skipped.
   170  			if image == nil {
   171  				return nil
   172  			}
   173  
   174  			resultsMut.Lock()
   175  			summaries = append(summaries, image)
   176  
   177  			if opts.SharedSize {
   178  				root = append(root, &allChainsIDs)
   179  				for _, id := range allChainsIDs {
   180  					layers[id] = layers[id] + 1
   181  				}
   182  			}
   183  			resultsMut.Unlock()
   184  			return nil
   185  		})
   186  	}
   187  
   188  	if err := eg.Wait(); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	if opts.SharedSize {
   193  		for n, chainIDs := range root {
   194  			sharedSize, err := computeSharedSize(*chainIDs, layers, snapshotSizeFn)
   195  			if err != nil {
   196  				return nil, err
   197  			}
   198  			summaries[n].SharedSize = sharedSize
   199  		}
   200  	}
   201  
   202  	sort.Sort(sort.Reverse(byCreated(summaries)))
   203  
   204  	return summaries, nil
   205  }
   206  
   207  // imageSummary returns a summary of the image, including the total size of the image and all its platforms.
   208  // It also returns the chainIDs of all the layers of the image (including all its platforms).
   209  // All return values will be nil if the image should be skipped.
   210  func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platformMatcher platforms.MatchComparer,
   211  	opts imagetypes.ListOptions, tagsByDigest map[digest.Digest][]string,
   212  ) (_ *imagetypes.Summary, allChainIDs []digest.Digest, _ error) {
   213  
   214  	// Total size of the image including all its platform
   215  	var totalSize int64
   216  
   217  	// ChainIDs of all the layers of the image (including all its platform)
   218  	var allChainsIDs []digest.Digest
   219  
   220  	// Count of containers using the image
   221  	var containersCount int64
   222  
   223  	// Single platform image manifest preferred by the platform matcher
   224  	var best *ImageManifest
   225  	var bestPlatform ocispec.Platform
   226  
   227  	err := i.walkImageManifests(ctx, img, func(img *ImageManifest) error {
   228  		if isPseudo, err := img.IsPseudoImage(ctx); isPseudo || err != nil {
   229  			return nil
   230  		}
   231  
   232  		available, err := img.CheckContentAvailable(ctx)
   233  		if err != nil {
   234  			log.G(ctx).WithFields(log.Fields{
   235  				"error":    err,
   236  				"manifest": img.Target(),
   237  				"image":    img.Name(),
   238  			}).Warn("checking availability of platform specific manifest failed")
   239  			return nil
   240  		}
   241  
   242  		if !available {
   243  			return nil
   244  		}
   245  
   246  		conf, err := img.Config(ctx)
   247  		if err != nil {
   248  			return err
   249  		}
   250  
   251  		var dockerImage dockerspec.DockerOCIImage
   252  		if err := readConfig(ctx, i.content, conf, &dockerImage); err != nil {
   253  			return err
   254  		}
   255  
   256  		target := img.Target()
   257  
   258  		diffIDs, err := img.RootFS(ctx)
   259  		if err != nil {
   260  			return err
   261  		}
   262  
   263  		chainIDs := identity.ChainIDs(diffIDs)
   264  
   265  		ts, _, err := i.singlePlatformSize(ctx, img)
   266  		if err != nil {
   267  			return err
   268  		}
   269  
   270  		totalSize += ts
   271  		allChainsIDs = append(allChainsIDs, chainIDs...)
   272  
   273  		if opts.ContainerCount {
   274  			i.containers.ApplyAll(func(c *container.Container) {
   275  				if c.ImageManifest != nil && c.ImageManifest.Digest == target.Digest {
   276  					containersCount++
   277  				}
   278  			})
   279  		}
   280  
   281  		var platform ocispec.Platform
   282  		if target.Platform != nil {
   283  			platform = *target.Platform
   284  		} else {
   285  			platform = dockerImage.Platform
   286  		}
   287  
   288  		// Filter out platforms that don't match the requested platform.  Do it
   289  		// after the size, container count and chainIDs are summed up to have
   290  		// the single combined entry still represent the whole multi-platform
   291  		// image.
   292  		if !platformMatcher.Match(platform) {
   293  			return nil
   294  		}
   295  
   296  		if best == nil || platformMatcher.Less(platform, bestPlatform) {
   297  			best = img
   298  			bestPlatform = platform
   299  		}
   300  
   301  		return nil
   302  	})
   303  	if err != nil {
   304  		if errors.Is(err, errNotManifestOrIndex) {
   305  			log.G(ctx).WithFields(log.Fields{
   306  				"error": err,
   307  				"image": img.Name,
   308  			}).Warn("unexpected image target (neither a manifest nor index)")
   309  			return nil, nil, nil
   310  		}
   311  		return nil, nil, err
   312  	}
   313  
   314  	if best == nil {
   315  		// TODO we should probably show *something* for images we've pulled
   316  		// but are 100% shallow or an empty manifest list/index
   317  		// ("tianon/scratch:index" is an empty example image index and
   318  		// "tianon/scratch:list" is an empty example manifest list)
   319  		return nil, nil, nil
   320  	}
   321  
   322  	image, err := i.singlePlatformImage(ctx, i.content, tagsByDigest[best.RealTarget.Digest], best)
   323  	if err != nil {
   324  		return nil, nil, err
   325  	}
   326  	image.Size = totalSize
   327  
   328  	if opts.ContainerCount {
   329  		image.Containers = containersCount
   330  	}
   331  	return image, allChainsIDs, nil
   332  }
   333  
   334  func (i *ImageService) singlePlatformSize(ctx context.Context, imgMfst *ImageManifest) (totalSize int64, contentSize int64, _ error) {
   335  	// TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273
   336  	snapshotter := i.snapshotterService(i.snapshotter)
   337  
   338  	diffIDs, err := imgMfst.RootFS(ctx)
   339  	if err != nil {
   340  		return -1, -1, errors.Wrapf(err, "failed to get rootfs of image %s", imgMfst.Name())
   341  	}
   342  
   343  	imageSnapshotID := identity.ChainID(diffIDs).String()
   344  	unpackedUsage, err := calculateSnapshotTotalUsage(ctx, snapshotter, imageSnapshotID)
   345  	if err != nil {
   346  		if !cerrdefs.IsNotFound(err) {
   347  			log.G(ctx).WithError(err).WithFields(log.Fields{
   348  				"image":      imgMfst.Name(),
   349  				"snapshotID": imageSnapshotID,
   350  			}).Warn("failed to calculate unpacked size of image")
   351  		}
   352  		unpackedUsage = snapshots.Usage{Size: 0}
   353  	}
   354  
   355  	contentSize, err = imgMfst.Size(ctx)
   356  	if err != nil {
   357  		return -1, -1, err
   358  	}
   359  
   360  	// totalSize is the size of the image's packed layers and snapshots
   361  	// (unpacked layers) combined.
   362  	totalSize = contentSize + unpackedUsage.Size
   363  	return totalSize, contentSize, nil
   364  }
   365  
   366  func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore content.Store, repoTags []string, imageManifest *ImageManifest) (*imagetypes.Summary, error) {
   367  	var repoDigests []string
   368  	rawImg := imageManifest.Metadata()
   369  	target := rawImg.Target.Digest
   370  
   371  	logger := log.G(ctx).WithFields(log.Fields{
   372  		"name":   rawImg.Name,
   373  		"digest": target,
   374  	})
   375  
   376  	ref, err := reference.ParseNamed(rawImg.Name)
   377  	if err != nil {
   378  		// If the image has unexpected name format (not a Named reference or a dangling image)
   379  		// add the offending name to RepoTags but also log an error to make it clear to the
   380  		// administrator that this is unexpected.
   381  		// TODO: Reconsider when containerd is more strict on image names, see:
   382  		//       https://github.com/containerd/containerd/issues/7986
   383  		if !isDanglingImage(rawImg) {
   384  			logger.WithError(err).Error("failed to parse image name as reference")
   385  			repoTags = append(repoTags, rawImg.Name)
   386  		}
   387  	} else {
   388  		digested, err := reference.WithDigest(reference.TrimNamed(ref), target)
   389  		if err != nil {
   390  			logger.WithError(err).Error("failed to create digested reference")
   391  		} else {
   392  			repoDigests = append(repoDigests, reference.FamiliarString(digested))
   393  		}
   394  	}
   395  
   396  	cfgDesc, err := imageManifest.Image.Config(ctx)
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  	var cfg configLabels
   401  	if err := readConfig(ctx, contentStore, cfgDesc, &cfg); err != nil {
   402  		return nil, err
   403  	}
   404  
   405  	totalSize, _, err := i.singlePlatformSize(ctx, imageManifest)
   406  	if err != nil {
   407  		return nil, errors.Wrapf(err, "failed to calculate size of image %s", imageManifest.Name())
   408  	}
   409  
   410  	summary := &imagetypes.Summary{
   411  		ParentID:    rawImg.Labels[imageLabelClassicBuilderParent],
   412  		ID:          target.String(),
   413  		RepoDigests: repoDigests,
   414  		RepoTags:    repoTags,
   415  		Size:        totalSize,
   416  		Labels:      cfg.Config.Labels,
   417  		// -1 indicates that the value has not been set (avoids ambiguity
   418  		// between 0 (default) and "not set". We cannot use a pointer (nil)
   419  		// for this, as the JSON representation uses "omitempty", which would
   420  		// consider both "0" and "nil" to be "empty".
   421  		SharedSize: -1,
   422  		Containers: -1,
   423  	}
   424  	if cfg.Created != nil {
   425  		summary.Created = cfg.Created.Unix()
   426  	}
   427  
   428  	return summary, nil
   429  }
   430  
   431  type imageFilterFunc func(image images.Image) bool
   432  
   433  // setupFilters constructs an imageFilterFunc from the given imageFilters.
   434  //
   435  // filterFunc is a function that checks whether given image matches the filters.
   436  // TODO(thaJeztah): reimplement filters using containerd filters if possible: see https://github.com/moby/moby/issues/43845
   437  func (i *ImageService) setupFilters(ctx context.Context, imageFilters filters.Args) (filterFunc imageFilterFunc, outErr error) {
   438  	var fltrs []imageFilterFunc
   439  	err := imageFilters.WalkValues("before", func(value string) error {
   440  		img, err := i.GetImage(ctx, value, backend.GetImageOpts{})
   441  		if err != nil {
   442  			return err
   443  		}
   444  		if img != nil && img.Created != nil {
   445  			fltrs = append(fltrs, func(candidate images.Image) bool {
   446  				cand, err := i.GetImage(ctx, candidate.Name, backend.GetImageOpts{})
   447  				if err != nil {
   448  					return false
   449  				}
   450  				return cand.Created != nil && cand.Created.Before(*img.Created)
   451  			})
   452  		}
   453  		return nil
   454  	})
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  
   459  	err = imageFilters.WalkValues("since", func(value string) error {
   460  		img, err := i.GetImage(ctx, value, backend.GetImageOpts{})
   461  		if err != nil {
   462  			return err
   463  		}
   464  		if img != nil && img.Created != nil {
   465  			fltrs = append(fltrs, func(candidate images.Image) bool {
   466  				cand, err := i.GetImage(ctx, candidate.Name, backend.GetImageOpts{})
   467  				if err != nil {
   468  					return false
   469  				}
   470  				return cand.Created != nil && cand.Created.After(*img.Created)
   471  			})
   472  		}
   473  		return nil
   474  	})
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  
   479  	err = imageFilters.WalkValues("until", func(value string) error {
   480  		ts, err := timetypes.GetTimestamp(value, time.Now())
   481  		if err != nil {
   482  			return err
   483  		}
   484  		seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0)
   485  		if err != nil {
   486  			return err
   487  		}
   488  		until := time.Unix(seconds, nanoseconds)
   489  
   490  		fltrs = append(fltrs, func(image images.Image) bool {
   491  			created := image.CreatedAt
   492  			return created.Before(until)
   493  		})
   494  		return err
   495  	})
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  
   500  	labelFn, err := setupLabelFilter(ctx, i.content, imageFilters)
   501  	if err != nil {
   502  		return nil, err
   503  	}
   504  	if labelFn != nil {
   505  		fltrs = append(fltrs, labelFn)
   506  	}
   507  
   508  	if imageFilters.Contains("dangling") {
   509  		danglingValue, err := imageFilters.GetBoolOrDefault("dangling", false)
   510  		if err != nil {
   511  			return nil, err
   512  		}
   513  		fltrs = append(fltrs, func(image images.Image) bool {
   514  			return danglingValue == isDanglingImage(image)
   515  		})
   516  	}
   517  
   518  	if refs := imageFilters.Get("reference"); len(refs) != 0 {
   519  		fltrs = append(fltrs, func(image images.Image) bool {
   520  			ref, err := reference.ParseNormalizedNamed(image.Name)
   521  			if err != nil {
   522  				return false
   523  			}
   524  			for _, value := range refs {
   525  				found, err := reference.FamiliarMatch(value, ref)
   526  				if err != nil {
   527  					return false
   528  				}
   529  				if found {
   530  					return found
   531  				}
   532  			}
   533  			return false
   534  		})
   535  	}
   536  
   537  	return func(image images.Image) bool {
   538  		for _, filter := range fltrs {
   539  			if !filter(image) {
   540  				return false
   541  			}
   542  		}
   543  		return true
   544  	}, nil
   545  }
   546  
   547  // setupLabelFilter parses filter args for "label" and "label!" and returns a
   548  // filter func which will check if any image config from the given image has
   549  // labels that match given predicates.
   550  func setupLabelFilter(ctx context.Context, store content.Store, fltrs filters.Args) (func(image images.Image) bool, error) {
   551  	type labelCheck struct {
   552  		key        string
   553  		value      string
   554  		onlyExists bool
   555  		negate     bool
   556  	}
   557  
   558  	var checks []labelCheck
   559  	for _, fltrName := range []string{"label", "label!"} {
   560  		for _, l := range fltrs.Get(fltrName) {
   561  			k, v, found := strings.Cut(l, "=")
   562  			err := labels.Validate(k, v)
   563  			if err != nil {
   564  				return nil, err
   565  			}
   566  
   567  			negate := strings.HasSuffix(fltrName, "!")
   568  
   569  			// If filter value is key!=value then flip the above.
   570  			if strings.HasSuffix(k, "!") {
   571  				k = strings.TrimSuffix(k, "!")
   572  				negate = !negate
   573  			}
   574  
   575  			checks = append(checks, labelCheck{
   576  				key:        k,
   577  				value:      v,
   578  				onlyExists: !found,
   579  				negate:     negate,
   580  			})
   581  		}
   582  	}
   583  
   584  	if len(checks) == 0 {
   585  		return nil, nil
   586  	}
   587  
   588  	return func(image images.Image) bool {
   589  		// This is not an error, but a signal to Dispatch that it should stop
   590  		// processing more content (otherwise it will run for all children).
   591  		// It will be returned once a matching config is found.
   592  		errFoundConfig := errors.New("success, found matching config")
   593  
   594  		err := images.Dispatch(ctx, presentChildrenHandler(store, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
   595  			if !images.IsConfigType(desc.MediaType) {
   596  				return nil, nil
   597  			}
   598  			var cfg configLabels
   599  			if err := readConfig(ctx, store, desc, &cfg); err != nil {
   600  				if errdefs.IsNotFound(err) {
   601  					return nil, nil
   602  				}
   603  				return nil, err
   604  			}
   605  
   606  			for _, check := range checks {
   607  				value, exists := cfg.Config.Labels[check.key]
   608  
   609  				if check.onlyExists {
   610  					// label! given without value, check if doesn't exist
   611  					if check.negate {
   612  						// Label exists, config doesn't match
   613  						if exists {
   614  							return nil, nil
   615  						}
   616  					} else {
   617  						// Label should exist
   618  						if !exists {
   619  							// Label doesn't exist, config doesn't match
   620  							return nil, nil
   621  						}
   622  					}
   623  					continue
   624  				} else if !exists {
   625  					// We are checking value and label doesn't exist.
   626  					return nil, nil
   627  				}
   628  
   629  				valueEquals := value == check.value
   630  				if valueEquals == check.negate {
   631  					return nil, nil
   632  				}
   633  			}
   634  
   635  			// This config matches the filter so we need to shop this image, stop dispatch.
   636  			return nil, errFoundConfig
   637  		})), nil, image.Target)
   638  
   639  		if err == errFoundConfig {
   640  			return true
   641  		}
   642  		if err != nil {
   643  			log.G(ctx).WithFields(log.Fields{
   644  				"error":  err,
   645  				"image":  image.Name,
   646  				"checks": checks,
   647  			}).Error("failed to check image labels")
   648  		}
   649  
   650  		return false
   651  	}, nil
   652  }
   653  
   654  func computeSharedSize(chainIDs []digest.Digest, layers map[digest.Digest]int, sizeFn func(d digest.Digest) (int64, error)) (int64, error) {
   655  	var sharedSize int64
   656  	for _, chainID := range chainIDs {
   657  		if layers[chainID] == 1 {
   658  			continue
   659  		}
   660  		size, err := sizeFn(chainID)
   661  		if err != nil {
   662  			// Several images might share the same layer and neither of them
   663  			// might be unpacked (for example if it's a non-host platform).
   664  			if cerrdefs.IsNotFound(err) {
   665  				continue
   666  			}
   667  			return 0, err
   668  		}
   669  		sharedSize += size
   670  	}
   671  	return sharedSize, nil
   672  }
   673  
   674  // readConfig reads content pointed by the descriptor and unmarshals it into a specified output.
   675  func readConfig(ctx context.Context, store content.Provider, desc ocispec.Descriptor, out interface{}) error {
   676  	data, err := content.ReadBlob(ctx, store, desc)
   677  	if err != nil {
   678  		err = errors.Wrapf(err, "failed to read config content")
   679  		if cerrdefs.IsNotFound(err) {
   680  			return errdefs.NotFound(err)
   681  		}
   682  		return err
   683  	}
   684  
   685  	err = json.Unmarshal(data, out)
   686  	if err != nil {
   687  		err = errors.Wrapf(err, "could not deserialize image config")
   688  		if cerrdefs.IsNotFound(err) {
   689  			return errdefs.NotFound(err)
   690  		}
   691  		return err
   692  	}
   693  
   694  	return nil
   695  }