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