diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index bb9ea7df8a..59baa51b61 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "strings" + "time" "github.com/containerd/containerd" "github.com/containerd/containerd/content" @@ -14,6 +15,7 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + timetypes "github.com/docker/docker/api/types/time" "github.com/moby/buildkit/util/attestation" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/identity" @@ -29,6 +31,7 @@ var acceptedImageFilterTags = map[string]bool{ "before": true, "since": true, "reference": true, + "until": true, } // Images returns a filtered list of images. @@ -304,6 +307,27 @@ func (i *ImageService) setupFilters(ctx context.Context, imageFilters filters.Ar return nil, nil, err } + err = imageFilters.WalkValues("until", func(value string) error { + ts, err := timetypes.GetTimestamp(value, time.Now()) + if err != nil { + return err + } + seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) + if err != nil { + return err + } + until := time.Unix(seconds, nanoseconds) + + fltrs = append(fltrs, func(image images.Image) bool { + created := image.CreatedAt + return created.Before(until) + }) + return err + }) + if err != nil { + return nil, nil, err + } + labelFn, err := setupLabelFilter(i.client.ContentStore(), imageFilters) if err != nil { return nil, nil, err diff --git a/daemon/containerd/image_prune.go b/daemon/containerd/image_prune.go index d29397d4ab..e333e4c6bf 100644 --- a/daemon/containerd/image_prune.go +++ b/daemon/containerd/image_prune.go @@ -2,14 +2,151 @@ package containerd import ( "context" - "errors" + cerrdefs "github.com/containerd/containerd/errdefs" + containerdimages "github.com/containerd/containerd/images" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/errdefs" + "github.com/hashicorp/go-multierror" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) -// ImagesPrune removes unused images -func (i *ImageService) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*types.ImagesPruneReport, error) { - return nil, errdefs.NotImplemented(errors.New("not implemented")) +var imagesAcceptedFilters = map[string]bool{ + "dangling": true, + "label": true, + "label!": true, + "until": true, +} + +// errPruneRunning is returned when a prune request is received while +// one is in progress +var errPruneRunning = errdefs.Conflict(errors.New("a prune operation is already running")) + +// ImagesPrune removes unused images +func (i *ImageService) ImagesPrune(ctx context.Context, fltrs filters.Args) (*types.ImagesPruneReport, error) { + if !i.pruneRunning.CompareAndSwap(false, true) { + return nil, errPruneRunning + } + defer i.pruneRunning.Store(false) + + err := fltrs.Validate(imagesAcceptedFilters) + if err != nil { + return nil, err + } + + danglingOnly, err := fltrs.GetBoolOrDefault("dangling", false) + if err != nil { + return nil, err + } + // dangling=false will filter out dangling images like in image list. + // Remove it, because in this context dangling=false means that we're + // pruning NOT ONLY dangling (`docker image prune -a`) instead of NOT DANGLING. + // This will be handled by the danglingOnly parameter of pruneUnused. + for _, v := range fltrs.Get("dangling") { + fltrs.Del("dangling", v) + } + + _, filterFunc, err := i.setupFilters(ctx, fltrs) + if err != nil { + return nil, err + } + + return i.pruneUnused(ctx, filterFunc, danglingOnly) +} + +func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFunc, danglingOnly bool) (*types.ImagesPruneReport, error) { + report := types.ImagesPruneReport{} + is := i.client.ImageService() + store := i.client.ContentStore() + + allImages, err := i.client.ImageService().List(ctx) + if err != nil { + return nil, err + } + + imagesToPrune := map[string]containerdimages.Image{} + for _, img := range allImages { + if !danglingOnly || isDanglingImage(img) { + imagesToPrune[img.Name] = img + } + } + + // Apply filters + for name, img := range imagesToPrune { + filteredOut := !filterFunc(img) + logrus.WithField("image", name).WithField("filteredOut", filteredOut).Debug("filtering image") + if filteredOut { + delete(imagesToPrune, name) + } + } + + containers := i.containers.List() + + var errs error + // Exclude images used by existing containers + for _, ctr := range containers { + // Config.Image is the image reference passed by user. + // For example: container created by `docker run alpine` will have Image="alpine" + // Warning: This doesn't handle truncated ids: + // `docker run 124c7d2` will have Image="124c7d270790" + ref, err := reference.ParseNormalizedNamed(ctr.Config.Image) + logrus.WithFields(logrus.Fields{ + "ctr": ctr.ID, + "image": ref, + "nameParseErr": err, + }).Debug("filtering container's image") + + if err == nil { + name := reference.TagNameOnly(ref) + delete(imagesToPrune, name.String()) + } + } + + logrus.WithField("images", imagesToPrune).Debug("pruning") + + for _, img := range imagesToPrune { + blobs := []ocispec.Descriptor{} + + err = containerdimages.Walk(ctx, presentChildrenHandler(store, containerdimages.HandlerFunc( + func(_ context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + blobs = append(blobs, desc) + return nil, nil + })), + img.Target) + + if err != nil { + errs = multierror.Append(errs, err) + continue + } + err = is.Delete(ctx, img.Name, containerdimages.SynchronousDelete()) + if err != nil && !cerrdefs.IsNotFound(err) { + errs = multierror.Append(errs, err) + continue + } + + report.ImagesDeleted = append(report.ImagesDeleted, + types.ImageDeleteResponseItem{ + Untagged: img.Name, + }, + ) + + // Check which blobs have been deleted and sum their sizes + for _, blob := range blobs { + _, err := store.ReaderAt(ctx, blob) + + if cerrdefs.IsNotFound(err) { + report.ImagesDeleted = append(report.ImagesDeleted, + types.ImageDeleteResponseItem{ + Deleted: blob.Digest.String(), + }, + ) + report.SpaceReclaimed += uint64(blob.Size) + } + } + } + return &report, errs } diff --git a/daemon/containerd/service.go b/daemon/containerd/service.go index 9218945dd4..666978e1c3 100644 --- a/daemon/containerd/service.go +++ b/daemon/containerd/service.go @@ -3,6 +3,7 @@ package containerd import ( "context" "encoding/json" + "sync/atomic" "github.com/containerd/containerd" "github.com/containerd/containerd/content" @@ -29,6 +30,7 @@ type ImageService struct { registryHosts RegistryHostsProvider registryService RegistryConfigProvider eventsService *daemonevents.Events + pruneRunning atomic.Bool } type RegistryHostsProvider interface {