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