github.com/rawahars/moby@v24.0.4+incompatible/daemon/containerd/image_list.go (about)

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