github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/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/Prakhar-Agarwal-byte/moby/api/types" 12 "github.com/Prakhar-Agarwal-byte/moby/api/types/filters" 13 "github.com/Prakhar-Agarwal-byte/moby/api/types/image" 14 "github.com/Prakhar-Agarwal-byte/moby/errdefs" 15 "github.com/Prakhar-Agarwal-byte/moby/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", false) 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 is := i.client.ImageService() 68 store := i.client.ContentStore() 69 70 allImages, err := i.client.ImageService().List(ctx) 71 if err != nil { 72 return nil, err 73 } 74 75 // How many images make reference to a particular target digest. 76 digestRefCount := map[digest.Digest]int{} 77 // Images considered for pruning. 78 imagesToPrune := map[string]containerdimages.Image{} 79 for _, img := range allImages { 80 digestRefCount[img.Target.Digest] += 1 81 82 if !danglingOnly || isDanglingImage(img) { 83 canBePruned := filterFunc(img) 84 log.G(ctx).WithFields(log.Fields{ 85 "image": img.Name, 86 "canBePruned": canBePruned, 87 }).Debug("considering image for pruning") 88 89 if canBePruned { 90 imagesToPrune[img.Name] = img 91 } 92 93 } 94 } 95 96 // Image specified by digests that are used by containers. 97 usedDigests := map[digest.Digest]struct{}{} 98 99 // Exclude images used by existing containers 100 for _, ctr := range i.containers.List() { 101 // If the original image was deleted, make sure we don't delete the dangling image 102 delete(imagesToPrune, danglingImageName(ctr.ImageID.Digest())) 103 104 // Config.Image is the image reference passed by user. 105 // Config.ImageID is the resolved content digest based on the user's Config.Image. 106 // For example: container created by: 107 // `docker run alpine` will have Config.Image="alpine" 108 // `docker run 82d1e9d` will have Config.Image="82d1e9d" 109 // but both will have ImageID="sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1" 110 imageDgst := ctr.ImageID.Digest() 111 112 // If user didn't specify an explicit image, mark the digest as used. 113 normalizedImageID := "sha256:" + strings.TrimPrefix(ctr.Config.Image, "sha256:") 114 if strings.HasPrefix(imageDgst.String(), normalizedImageID) { 115 usedDigests[imageDgst] = struct{}{} 116 continue 117 } 118 119 ref, err := reference.ParseNormalizedNamed(ctr.Config.Image) 120 log.G(ctx).WithFields(log.Fields{ 121 "ctr": ctr.ID, 122 "image": ref, 123 "nameParseErr": err, 124 }).Debug("filtering container's image") 125 126 if err == nil { 127 // If user provided a specific image name, exclude that image. 128 name := reference.TagNameOnly(ref) 129 delete(imagesToPrune, name.String()) 130 } 131 } 132 133 // Create dangling images for images that will be deleted but are still in use. 134 for _, img := range imagesToPrune { 135 dgst := img.Target.Digest 136 137 digestRefCount[dgst] -= 1 138 if digestRefCount[dgst] == 0 { 139 if _, isUsed := usedDigests[dgst]; isUsed { 140 if err := i.ensureDanglingImage(ctx, img); err != nil { 141 return &report, errors.Wrapf(err, "failed to create ensure dangling image for %s", img.Name) 142 } 143 } 144 } 145 } 146 147 possiblyDeletedConfigs := map[digest.Digest]struct{}{} 148 var errs error 149 150 // Workaround for https://github.com/moby/buildkit/issues/3797 151 defer func() { 152 if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil { 153 errs = multierror.Append(errs, err) 154 } 155 }() 156 157 for _, img := range imagesToPrune { 158 log.G(ctx).WithField("image", img).Debug("pruning image") 159 160 blobs := []ocispec.Descriptor{} 161 162 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, desc ocispec.Descriptor) error { 163 blobs = append(blobs, desc) 164 if containerdimages.IsConfigType(desc.MediaType) { 165 possiblyDeletedConfigs[desc.Digest] = struct{}{} 166 } 167 return nil 168 }) 169 if err != nil { 170 errs = multierror.Append(errs, err) 171 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 172 return &report, errs 173 } 174 continue 175 } 176 err = is.Delete(ctx, img.Name, containerdimages.SynchronousDelete()) 177 if err != nil && !cerrdefs.IsNotFound(err) { 178 errs = multierror.Append(errs, err) 179 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 180 return &report, errs 181 } 182 continue 183 } 184 185 report.ImagesDeleted = append(report.ImagesDeleted, 186 image.DeleteResponse{ 187 Untagged: img.Name, 188 }, 189 ) 190 191 // Check which blobs have been deleted and sum their sizes 192 for _, blob := range blobs { 193 _, err := store.ReaderAt(ctx, blob) 194 195 if cerrdefs.IsNotFound(err) { 196 report.ImagesDeleted = append(report.ImagesDeleted, 197 image.DeleteResponse{ 198 Deleted: blob.Digest.String(), 199 }, 200 ) 201 report.SpaceReclaimed += uint64(blob.Size) 202 } 203 } 204 } 205 206 return &report, errs 207 } 208 209 // unleaseSnapshotsFromDeletedConfigs removes gc.ref.snapshot content label from configs that are not 210 // referenced by any of the existing images. 211 // This is a temporary solution to the rootfs snapshot not being deleted when there's a buildkit history 212 // item referencing an image config. 213 func (i *ImageService) unleaseSnapshotsFromDeletedConfigs(ctx context.Context, possiblyDeletedConfigs map[digest.Digest]struct{}) error { 214 is := i.client.ImageService() 215 store := i.client.ContentStore() 216 217 all, err := is.List(ctx) 218 if err != nil { 219 return errors.Wrap(err, "failed to list images during snapshot lease removal") 220 } 221 222 var errs error 223 for _, img := range all { 224 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, desc ocispec.Descriptor) error { 225 if containerdimages.IsConfigType(desc.MediaType) { 226 delete(possiblyDeletedConfigs, desc.Digest) 227 } 228 return nil 229 }) 230 if err != nil { 231 errs = multierror.Append(errs, err) 232 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 233 return errs 234 } 235 continue 236 } 237 } 238 239 // At this point, all configs that are used by any image has been removed from the slice 240 for cfgDigest := range possiblyDeletedConfigs { 241 info, err := store.Info(ctx, cfgDigest) 242 if err != nil { 243 if cerrdefs.IsNotFound(err) { 244 log.G(ctx).WithField("config", cfgDigest).Debug("config already gone") 245 } else { 246 errs = multierror.Append(errs, err) 247 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 248 return errs 249 } 250 } 251 continue 252 } 253 254 label := "containerd.io/gc.ref.snapshot." + i.StorageDriver() 255 256 delete(info.Labels, label) 257 _, err = store.Update(ctx, info, "labels."+label) 258 if err != nil { 259 errs = multierror.Append(errs, errors.Wrapf(err, "failed to remove gc.ref.snapshot label from %s", cfgDigest)) 260 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 261 return errs 262 } 263 } 264 } 265 266 return errs 267 }