package images // import "github.com/docker/docker/daemon/images" import ( "context" "fmt" "strconv" "sync/atomic" "time" "github.com/containerd/log" "github.com/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" imagetypes "github.com/docker/docker/api/types/image" timetypes "github.com/docker/docker/api/types/time" "github.com/docker/docker/errdefs" "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) 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, pruneFilters filters.Args) (*types.ImagesPruneReport, error) { if !atomic.CompareAndSwapInt32(&i.pruneRunning, 0, 1) { return nil, errPruneRunning } defer atomic.StoreInt32(&i.pruneRunning, 0) // make sure that only accepted filters have been received err := pruneFilters.Validate(imagesAcceptedFilters) if err != nil { return nil, err } rep := &types.ImagesPruneReport{} danglingOnly, err := pruneFilters.GetBoolOrDefault("dangling", true) if err != nil { return nil, err } until, err := getUntilFromPruneFilters(pruneFilters) if err != nil { return nil, err } var allImages map[image.ID]*image.Image if danglingOnly { allImages = i.imageStore.Heads() } else { allImages = i.imageStore.Map() } // Filter intermediary images and get their unique size allLayers := i.layerStore.Map() topImages := map[image.ID]*image.Image{} for id, img := range allImages { select { case <-ctx.Done(): return nil, ctx.Err() default: dgst := digest.Digest(id) if len(i.referenceStore.References(dgst)) == 0 && len(i.imageStore.Children(id)) != 0 { continue } if !until.IsZero() && (img.Created == nil || img.Created.After(until)) { continue } if img.Config != nil && !matchLabels(pruneFilters, img.Config.Labels) { continue } topImages[id] = img } } canceled := false deleteImagesLoop: for id := range topImages { select { case <-ctx.Done(): // we still want to calculate freed size and return the data canceled = true break deleteImagesLoop default: } deletedImages := []imagetypes.DeleteResponse{} refs := i.referenceStore.References(id.Digest()) if len(refs) > 0 { shouldDelete := !danglingOnly if !shouldDelete { hasTag := false for _, ref := range refs { if _, ok := ref.(reference.NamedTagged); ok { hasTag = true break } } // Only delete if it has no references which is a valid NamedTagged. shouldDelete = !hasTag } if shouldDelete { for _, ref := range refs { imgDel, err := i.ImageDelete(ctx, ref.String(), false, true) if imageDeleteFailed(ref.String(), err) { continue } deletedImages = append(deletedImages, imgDel...) } } } else { hex := id.Digest().Encoded() imgDel, err := i.ImageDelete(ctx, hex, false, true) if imageDeleteFailed(hex, err) { continue } deletedImages = append(deletedImages, imgDel...) } rep.ImagesDeleted = append(rep.ImagesDeleted, deletedImages...) } // Compute how much space was freed for _, d := range rep.ImagesDeleted { if d.Deleted != "" { chid := layer.ChainID(d.Deleted) if l, ok := allLayers[chid]; ok { rep.SpaceReclaimed += uint64(l.DiffSize()) } } } if canceled { log.G(ctx).Debugf("ImagesPrune operation cancelled: %#v", *rep) } i.eventsService.Log(events.ActionPrune, events.ImageEventType, events.Actor{ Attributes: map[string]string{ "reclaimed": strconv.FormatUint(rep.SpaceReclaimed, 10), }, }) return rep, nil } func imageDeleteFailed(ref string, err error) bool { switch { case err == nil: return false case errdefs.IsConflict(err), errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): return true default: log.G(context.TODO()).Warnf("failed to prune image %s: %v", ref, err) return true } } func matchLabels(pruneFilters filters.Args, labels map[string]string) bool { if !pruneFilters.MatchKVList("label", labels) { return false } // By default MatchKVList will return true if field (like 'label!') does not exist // So we have to add additional Contains("label!") check if pruneFilters.Contains("label!") { if pruneFilters.MatchKVList("label!", labels) { return false } } return true } func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) { until := time.Time{} if !pruneFilters.Contains("until") { return until, nil } untilFilters := pruneFilters.Get("until") if len(untilFilters) > 1 { return until, fmt.Errorf("more than one until filter specified") } ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now()) if err != nil { return until, err } seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) if err != nil { return until, err } until = time.Unix(seconds, nanoseconds) return until, nil }