github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/daemon/containerd/image_list.go (about)

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