github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_prune.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "strings" 6 7 cerrdefs "github.com/containerd/containerd/errdefs" 8 containerdimages "github.com/containerd/containerd/images" 9 "github.com/containerd/log" 10 "github.com/distribution/reference" 11 "github.com/docker/docker/api/types" 12 "github.com/docker/docker/api/types/filters" 13 "github.com/docker/docker/api/types/image" 14 "github.com/docker/docker/errdefs" 15 "github.com/docker/docker/internal/compatcontext" 16 "github.com/hashicorp/go-multierror" 17 "github.com/opencontainers/go-digest" 18 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 19 "github.com/pkg/errors" 20 ) 21 22 var imagesAcceptedFilters = map[string]bool{ 23 "dangling": true, 24 "label": true, 25 "label!": true, 26 "until": true, 27 } 28 29 // errPruneRunning is returned when a prune request is received while 30 // one is in progress 31 var errPruneRunning = errdefs.Conflict(errors.New("a prune operation is already running")) 32 33 // ImagesPrune removes unused images 34 func (i *ImageService) ImagesPrune(ctx context.Context, fltrs filters.Args) (*types.ImagesPruneReport, error) { 35 if !i.pruneRunning.CompareAndSwap(false, true) { 36 return nil, errPruneRunning 37 } 38 defer i.pruneRunning.Store(false) 39 40 err := fltrs.Validate(imagesAcceptedFilters) 41 if err != nil { 42 return nil, err 43 } 44 45 danglingOnly, err := fltrs.GetBoolOrDefault("dangling", true) 46 if err != nil { 47 return nil, err 48 } 49 // dangling=false will filter out dangling images like in image list. 50 // Remove it, because in this context dangling=false means that we're 51 // pruning NOT ONLY dangling (`docker image prune -a`) instead of NOT DANGLING. 52 // This will be handled by the danglingOnly parameter of pruneUnused. 53 for _, v := range fltrs.Get("dangling") { 54 fltrs.Del("dangling", v) 55 } 56 57 filterFunc, err := i.setupFilters(ctx, fltrs) 58 if err != nil { 59 return nil, err 60 } 61 62 return i.pruneUnused(ctx, filterFunc, danglingOnly) 63 } 64 65 func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFunc, danglingOnly bool) (*types.ImagesPruneReport, error) { 66 report := types.ImagesPruneReport{} 67 68 allImages, err := i.images.List(ctx) 69 if err != nil { 70 return nil, err 71 } 72 73 // How many images make reference to a particular target digest. 74 digestRefCount := map[digest.Digest]int{} 75 // Images considered for pruning. 76 imagesToPrune := map[string]containerdimages.Image{} 77 for _, img := range allImages { 78 digestRefCount[img.Target.Digest] += 1 79 80 if !danglingOnly || isDanglingImage(img) { 81 canBePruned := filterFunc(img) 82 log.G(ctx).WithFields(log.Fields{ 83 "image": img.Name, 84 "canBePruned": canBePruned, 85 }).Debug("considering image for pruning") 86 87 if canBePruned { 88 imagesToPrune[img.Name] = img 89 } 90 91 } 92 } 93 94 // Image specified by digests that are used by containers. 95 usedDigests := map[digest.Digest]struct{}{} 96 97 // Exclude images used by existing containers 98 for _, ctr := range i.containers.List() { 99 // If the original image was deleted, make sure we don't delete the dangling image 100 delete(imagesToPrune, danglingImageName(ctr.ImageID.Digest())) 101 102 // Config.Image is the image reference passed by user. 103 // Config.ImageID is the resolved content digest based on the user's Config.Image. 104 // For example: container created by: 105 // `docker run alpine` will have Config.Image="alpine" 106 // `docker run 82d1e9d` will have Config.Image="82d1e9d" 107 // but both will have ImageID="sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1" 108 imageDgst := ctr.ImageID.Digest() 109 110 // If user didn't specify an explicit image, mark the digest as used. 111 normalizedImageID := "sha256:" + strings.TrimPrefix(ctr.Config.Image, "sha256:") 112 if strings.HasPrefix(imageDgst.String(), normalizedImageID) { 113 usedDigests[imageDgst] = struct{}{} 114 continue 115 } 116 117 ref, err := reference.ParseNormalizedNamed(ctr.Config.Image) 118 log.G(ctx).WithFields(log.Fields{ 119 "ctr": ctr.ID, 120 "image": ref, 121 "nameParseErr": err, 122 }).Debug("filtering container's image") 123 124 if err == nil { 125 // If user provided a specific image name, exclude that image. 126 name := reference.TagNameOnly(ref) 127 delete(imagesToPrune, name.String()) 128 } 129 } 130 131 // Create dangling images for images that will be deleted but are still in use. 132 for _, img := range imagesToPrune { 133 dgst := img.Target.Digest 134 135 digestRefCount[dgst] -= 1 136 if digestRefCount[dgst] == 0 { 137 if _, isUsed := usedDigests[dgst]; isUsed { 138 if err := i.ensureDanglingImage(ctx, img); err != nil { 139 return &report, errors.Wrapf(err, "failed to create ensure dangling image for %s", img.Name) 140 } 141 } 142 } 143 } 144 145 possiblyDeletedConfigs := map[digest.Digest]struct{}{} 146 var errs error 147 148 // Workaround for https://github.com/moby/buildkit/issues/3797 149 defer func() { 150 if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil { 151 errs = multierror.Append(errs, err) 152 } 153 }() 154 155 for _, img := range imagesToPrune { 156 log.G(ctx).WithField("image", img).Debug("pruning image") 157 158 blobs := []ocispec.Descriptor{} 159 160 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, desc ocispec.Descriptor) error { 161 blobs = append(blobs, desc) 162 if containerdimages.IsConfigType(desc.MediaType) { 163 possiblyDeletedConfigs[desc.Digest] = struct{}{} 164 } 165 return nil 166 }) 167 if err != nil { 168 errs = multierror.Append(errs, err) 169 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 170 return &report, errs 171 } 172 continue 173 } 174 err = i.images.Delete(ctx, img.Name, containerdimages.SynchronousDelete()) 175 if err != nil && !cerrdefs.IsNotFound(err) { 176 errs = multierror.Append(errs, err) 177 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 178 return &report, errs 179 } 180 continue 181 } 182 183 report.ImagesDeleted = append(report.ImagesDeleted, 184 image.DeleteResponse{ 185 Untagged: imageFamiliarName(img), 186 }, 187 ) 188 189 // Check which blobs have been deleted and sum their sizes 190 for _, blob := range blobs { 191 _, err := i.content.ReaderAt(ctx, blob) 192 193 if cerrdefs.IsNotFound(err) { 194 report.ImagesDeleted = append(report.ImagesDeleted, 195 image.DeleteResponse{ 196 Deleted: blob.Digest.String(), 197 }, 198 ) 199 report.SpaceReclaimed += uint64(blob.Size) 200 } 201 } 202 } 203 204 return &report, errs 205 } 206 207 // unleaseSnapshotsFromDeletedConfigs removes gc.ref.snapshot content label from configs that are not 208 // referenced by any of the existing images. 209 // This is a temporary solution to the rootfs snapshot not being deleted when there's a buildkit history 210 // item referencing an image config. 211 func (i *ImageService) unleaseSnapshotsFromDeletedConfigs(ctx context.Context, possiblyDeletedConfigs map[digest.Digest]struct{}) error { 212 all, err := i.images.List(ctx) 213 if err != nil { 214 return errors.Wrap(err, "failed to list images during snapshot lease removal") 215 } 216 217 var errs error 218 for _, img := range all { 219 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, desc ocispec.Descriptor) error { 220 if containerdimages.IsConfigType(desc.MediaType) { 221 delete(possiblyDeletedConfigs, desc.Digest) 222 } 223 return nil 224 }) 225 if err != nil { 226 errs = multierror.Append(errs, err) 227 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 228 return errs 229 } 230 continue 231 } 232 } 233 234 // At this point, all configs that are used by any image has been removed from the slice 235 for cfgDigest := range possiblyDeletedConfigs { 236 info, err := i.content.Info(ctx, cfgDigest) 237 if err != nil { 238 if cerrdefs.IsNotFound(err) { 239 log.G(ctx).WithField("config", cfgDigest).Debug("config already gone") 240 } else { 241 errs = multierror.Append(errs, err) 242 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 243 return errs 244 } 245 } 246 continue 247 } 248 249 label := "containerd.io/gc.ref.snapshot." + i.StorageDriver() 250 251 delete(info.Labels, label) 252 _, err = i.content.Update(ctx, info, "labels."+label) 253 if err != nil { 254 errs = multierror.Append(errs, errors.Wrapf(err, "failed to remove gc.ref.snapshot label from %s", cfgDigest)) 255 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 256 return errs 257 } 258 } 259 } 260 261 return errs 262 }