github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/images/image_prune.go (about)

     1  package images // import "github.com/docker/docker/daemon/images"
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/docker/distribution/reference"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/events"
    13  	"github.com/docker/docker/api/types/filters"
    14  	timetypes "github.com/docker/docker/api/types/time"
    15  	"github.com/docker/docker/errdefs"
    16  	"github.com/docker/docker/image"
    17  	"github.com/docker/docker/layer"
    18  	"github.com/opencontainers/go-digest"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  var imagesAcceptedFilters = map[string]bool{
    24  	"dangling": true,
    25  	"label":    true,
    26  	"label!":   true,
    27  	"until":    true,
    28  }
    29  
    30  // errPruneRunning is returned when a prune request is received while
    31  // one is in progress
    32  var errPruneRunning = errdefs.Conflict(errors.New("a prune operation is already running"))
    33  
    34  // ImagesPrune removes unused images
    35  func (i *ImageService) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*types.ImagesPruneReport, error) {
    36  	if !atomic.CompareAndSwapInt32(&i.pruneRunning, 0, 1) {
    37  		return nil, errPruneRunning
    38  	}
    39  	defer atomic.StoreInt32(&i.pruneRunning, 0)
    40  
    41  	// make sure that only accepted filters have been received
    42  	err := pruneFilters.Validate(imagesAcceptedFilters)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	rep := &types.ImagesPruneReport{}
    48  
    49  	danglingOnly := true
    50  	if pruneFilters.Contains("dangling") {
    51  		if pruneFilters.ExactMatch("dangling", "false") || pruneFilters.ExactMatch("dangling", "0") {
    52  			danglingOnly = false
    53  		} else if !pruneFilters.ExactMatch("dangling", "true") && !pruneFilters.ExactMatch("dangling", "1") {
    54  			return nil, invalidFilter{"dangling", pruneFilters.Get("dangling")}
    55  		}
    56  	}
    57  
    58  	until, err := getUntilFromPruneFilters(pruneFilters)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	var allImages map[image.ID]*image.Image
    64  	if danglingOnly {
    65  		allImages = i.imageStore.Heads()
    66  	} else {
    67  		allImages = i.imageStore.Map()
    68  	}
    69  
    70  	// Filter intermediary images and get their unique size
    71  	allLayers := i.layerStore.Map()
    72  	topImages := map[image.ID]*image.Image{}
    73  	for id, img := range allImages {
    74  		select {
    75  		case <-ctx.Done():
    76  			return nil, ctx.Err()
    77  		default:
    78  			dgst := digest.Digest(id)
    79  			if len(i.referenceStore.References(dgst)) == 0 && len(i.imageStore.Children(id)) != 0 {
    80  				continue
    81  			}
    82  			if !until.IsZero() && img.Created.After(until) {
    83  				continue
    84  			}
    85  			if img.Config != nil && !matchLabels(pruneFilters, img.Config.Labels) {
    86  				continue
    87  			}
    88  			topImages[id] = img
    89  		}
    90  	}
    91  
    92  	canceled := false
    93  deleteImagesLoop:
    94  	for id := range topImages {
    95  		select {
    96  		case <-ctx.Done():
    97  			// we still want to calculate freed size and return the data
    98  			canceled = true
    99  			break deleteImagesLoop
   100  		default:
   101  		}
   102  
   103  		deletedImages := []types.ImageDeleteResponseItem{}
   104  		refs := i.referenceStore.References(id.Digest())
   105  		if len(refs) > 0 {
   106  			shouldDelete := !danglingOnly
   107  			if !shouldDelete {
   108  				hasTag := false
   109  				for _, ref := range refs {
   110  					if _, ok := ref.(reference.NamedTagged); ok {
   111  						hasTag = true
   112  						break
   113  					}
   114  				}
   115  
   116  				// Only delete if it's untagged (i.e. repo:<none>)
   117  				shouldDelete = !hasTag
   118  			}
   119  
   120  			if shouldDelete {
   121  				for _, ref := range refs {
   122  					imgDel, err := i.ImageDelete(ctx, ref.String(), false, true)
   123  					if imageDeleteFailed(ref.String(), err) {
   124  						continue
   125  					}
   126  					deletedImages = append(deletedImages, imgDel...)
   127  				}
   128  			}
   129  		} else {
   130  			hex := id.Digest().Encoded()
   131  			imgDel, err := i.ImageDelete(ctx, hex, false, true)
   132  			if imageDeleteFailed(hex, err) {
   133  				continue
   134  			}
   135  			deletedImages = append(deletedImages, imgDel...)
   136  		}
   137  
   138  		rep.ImagesDeleted = append(rep.ImagesDeleted, deletedImages...)
   139  	}
   140  
   141  	// Compute how much space was freed
   142  	for _, d := range rep.ImagesDeleted {
   143  		if d.Deleted != "" {
   144  			chid := layer.ChainID(d.Deleted)
   145  			if l, ok := allLayers[chid]; ok {
   146  				rep.SpaceReclaimed += uint64(l.DiffSize())
   147  			}
   148  		}
   149  	}
   150  
   151  	if canceled {
   152  		logrus.Debugf("ImagesPrune operation cancelled: %#v", *rep)
   153  	}
   154  	i.eventsService.Log("prune", events.ImageEventType, events.Actor{
   155  		Attributes: map[string]string{
   156  			"reclaimed": strconv.FormatUint(rep.SpaceReclaimed, 10),
   157  		},
   158  	})
   159  	return rep, nil
   160  }
   161  
   162  func imageDeleteFailed(ref string, err error) bool {
   163  	switch {
   164  	case err == nil:
   165  		return false
   166  	case errdefs.IsConflict(err), errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
   167  		return true
   168  	default:
   169  		logrus.Warnf("failed to prune image %s: %v", ref, err)
   170  		return true
   171  	}
   172  }
   173  
   174  func matchLabels(pruneFilters filters.Args, labels map[string]string) bool {
   175  	if !pruneFilters.MatchKVList("label", labels) {
   176  		return false
   177  	}
   178  	// By default MatchKVList will return true if field (like 'label!') does not exist
   179  	// So we have to add additional Contains("label!") check
   180  	if pruneFilters.Contains("label!") {
   181  		if pruneFilters.MatchKVList("label!", labels) {
   182  			return false
   183  		}
   184  	}
   185  	return true
   186  }
   187  
   188  func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) {
   189  	until := time.Time{}
   190  	if !pruneFilters.Contains("until") {
   191  		return until, nil
   192  	}
   193  	untilFilters := pruneFilters.Get("until")
   194  	if len(untilFilters) > 1 {
   195  		return until, fmt.Errorf("more than one until filter specified")
   196  	}
   197  	ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now())
   198  	if err != nil {
   199  		return until, err
   200  	}
   201  	seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0)
   202  	if err != nil {
   203  		return until, err
   204  	}
   205  	until = time.Unix(seconds, nanoseconds)
   206  	return until, nil
   207  }