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