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 }