github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/containerd/image_delete.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/containerd/containerd/images" 10 "github.com/containerd/log" 11 "github.com/distribution/reference" 12 "github.com/Prakhar-Agarwal-byte/moby/api/types/events" 13 imagetypes "github.com/Prakhar-Agarwal-byte/moby/api/types/image" 14 "github.com/Prakhar-Agarwal-byte/moby/container" 15 "github.com/Prakhar-Agarwal-byte/moby/image" 16 "github.com/Prakhar-Agarwal-byte/moby/internal/compatcontext" 17 "github.com/Prakhar-Agarwal-byte/moby/pkg/stringid" 18 "github.com/opencontainers/go-digest" 19 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 20 ) 21 22 // ImageDelete deletes the image referenced by the given imageRef from this 23 // daemon. The given imageRef can be an image ID, ID prefix, or a repository 24 // reference (with an optional tag or digest, defaulting to the tag name 25 // "latest"). There is differing behavior depending on whether the given 26 // imageRef is a repository reference or not. 27 // 28 // If the given imageRef is a repository reference then that repository 29 // reference will be removed. However, if there exists any containers which 30 // were created using the same image reference then the repository reference 31 // cannot be removed unless either there are other repository references to the 32 // same image or force is true. Following removal of the repository reference, 33 // the referenced image itself will attempt to be deleted as described below 34 // but quietly, meaning any image delete conflicts will cause the image to not 35 // be deleted and the conflict will not be reported. 36 // 37 // There may be conflicts preventing deletion of an image and these conflicts 38 // are divided into two categories grouped by their severity: 39 // 40 // Hard Conflict: 41 // - any running container using the image. 42 // 43 // Soft Conflict: 44 // - any stopped container using the image. 45 // - any repository tag or digest references to the image. 46 // 47 // The image cannot be removed if there are any hard conflicts and can be 48 // removed if there are soft conflicts only if force is true. 49 // 50 // If prune is true, ancestor images will each attempt to be deleted quietly, 51 // meaning any delete conflicts will cause the image to not be deleted and the 52 // conflict will not be reported. 53 // 54 // TODO(thaJeztah): image delete should send prometheus counters; see https://github.com/moby/moby/issues/45268 55 func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]imagetypes.DeleteResponse, error) { 56 parsedRef, err := reference.ParseNormalizedNamed(imageRef) 57 if err != nil { 58 return nil, err 59 } 60 61 img, err := i.resolveImage(ctx, imageRef) 62 if err != nil { 63 return nil, err 64 } 65 66 imgID := image.ID(img.Target.Digest) 67 68 explicitDanglingRef := strings.HasPrefix(imageRef, imageNameDanglingPrefix) && isDanglingImage(img) 69 if isImageIDPrefix(imgID.String(), imageRef) || explicitDanglingRef { 70 return i.deleteAll(ctx, img, force, prune) 71 } 72 73 singleRef, err := i.isSingleReference(ctx, img) 74 if err != nil { 75 return nil, err 76 } 77 if !singleRef { 78 err := i.client.ImageService().Delete(ctx, img.Name) 79 if err != nil { 80 return nil, err 81 } 82 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 83 records := []imagetypes.DeleteResponse{{Untagged: reference.FamiliarString(reference.TagNameOnly(parsedRef))}} 84 return records, nil 85 } 86 87 using := func(c *container.Container) bool { 88 return c.ImageID == imgID 89 } 90 ctr := i.containers.First(using) 91 if ctr != nil { 92 if !force { 93 // If we removed the repository reference then 94 // this image would remain "dangling" and since 95 // we really want to avoid that the client must 96 // explicitly force its removal. 97 refString := reference.FamiliarString(reference.TagNameOnly(parsedRef)) 98 err := &imageDeleteConflict{ 99 reference: refString, 100 used: true, 101 message: fmt.Sprintf("container %s is using its referenced image %s", 102 stringid.TruncateID(ctr.ID), 103 stringid.TruncateID(imgID.String())), 104 } 105 return nil, err 106 } 107 108 err := i.softImageDelete(ctx, img) 109 if err != nil { 110 return nil, err 111 } 112 113 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 114 records := []imagetypes.DeleteResponse{{Untagged: reference.FamiliarString(reference.TagNameOnly(parsedRef))}} 115 return records, nil 116 } 117 118 return i.deleteAll(ctx, img, force, prune) 119 } 120 121 // deleteAll deletes the image from the daemon, and if prune is true, 122 // also deletes dangling parents if there is no conflict in doing so. 123 // Parent images are removed quietly, and if there is any issue/conflict 124 // it is logged but does not halt execution/an error is not returned. 125 func (i *ImageService) deleteAll(ctx context.Context, img images.Image, force, prune bool) ([]imagetypes.DeleteResponse, error) { 126 var records []imagetypes.DeleteResponse 127 128 // Workaround for: https://github.com/moby/buildkit/issues/3797 129 possiblyDeletedConfigs := map[digest.Digest]struct{}{} 130 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, d ocispec.Descriptor) error { 131 if images.IsConfigType(d.MediaType) { 132 possiblyDeletedConfigs[d.Digest] = struct{}{} 133 } 134 return nil 135 }) 136 if err != nil { 137 return nil, err 138 } 139 defer func() { 140 if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil { 141 log.G(ctx).WithError(err).Warn("failed to unlease snapshots") 142 } 143 }() 144 145 imgID := img.Target.Digest.String() 146 147 var parents []imageWithRootfs 148 if prune { 149 parents, err = i.parents(ctx, image.ID(imgID)) 150 if err != nil { 151 log.G(ctx).WithError(err).Warn("failed to get image parents") 152 } 153 sortParentsByAffinity(parents) 154 } 155 156 imageRefs, err := i.client.ImageService().List(ctx, "target.digest=="+imgID) 157 if err != nil { 158 return nil, err 159 } 160 for _, imageRef := range imageRefs { 161 if err := i.imageDeleteHelper(ctx, imageRef, &records, force); err != nil { 162 return records, err 163 } 164 } 165 i.LogImageEvent(imgID, imgID, events.ActionDelete) 166 records = append(records, imagetypes.DeleteResponse{Deleted: imgID}) 167 168 for _, parent := range parents { 169 if !isDanglingImage(parent.img) { 170 break 171 } 172 err = i.imageDeleteHelper(ctx, parent.img, &records, false) 173 if err != nil { 174 log.G(ctx).WithError(err).Warn("failed to remove image parent") 175 break 176 } 177 parentID := parent.img.Target.Digest.String() 178 i.LogImageEvent(parentID, parentID, events.ActionDelete) 179 records = append(records, imagetypes.DeleteResponse{Deleted: parentID}) 180 } 181 182 return records, nil 183 } 184 185 // isImageIDPrefix returns whether the given 186 // possiblePrefix is a prefix of the given imageID. 187 func isImageIDPrefix(imageID, possiblePrefix string) bool { 188 if strings.HasPrefix(imageID, possiblePrefix) { 189 return true 190 } 191 if i := strings.IndexRune(imageID, ':'); i >= 0 { 192 return strings.HasPrefix(imageID[i+1:], possiblePrefix) 193 } 194 return false 195 } 196 197 func sortParentsByAffinity(parents []imageWithRootfs) { 198 sort.Slice(parents, func(i, j int) bool { 199 lenRootfsI := len(parents[i].rootfs.DiffIDs) 200 lenRootfsJ := len(parents[j].rootfs.DiffIDs) 201 if lenRootfsI == lenRootfsJ { 202 return isDanglingImage(parents[i].img) 203 } 204 return lenRootfsI > lenRootfsJ 205 }) 206 } 207 208 // isSingleReference returns true if there are no other images in the 209 // daemon targeting the same content as `img` that are not dangling. 210 func (i *ImageService) isSingleReference(ctx context.Context, img images.Image) (bool, error) { 211 refs, err := i.client.ImageService().List(ctx, "target.digest=="+img.Target.Digest.String()) 212 if err != nil { 213 return false, err 214 } 215 for _, ref := range refs { 216 if !isDanglingImage(ref) && ref.Name != img.Name { 217 return false, nil 218 } 219 } 220 return true, nil 221 } 222 223 type conflictType int 224 225 const ( 226 conflictRunningContainer conflictType = 1 << iota 227 conflictActiveReference 228 conflictStoppedContainer 229 conflictHard = conflictRunningContainer 230 conflictSoft = conflictActiveReference | conflictStoppedContainer 231 ) 232 233 // imageDeleteHelper attempts to delete the given image from this daemon. 234 // If the image has any hard delete conflicts (running containers using 235 // the image) then it cannot be deleted. If the image has any soft delete 236 // conflicts (any tags/digests referencing the image or any stopped container 237 // using the image) then it can only be deleted if force is true. Any deleted 238 // images and untagged references are appended to the given records. If any 239 // error or conflict is encountered, it will be returned immediately without 240 // deleting the image. 241 func (i *ImageService) imageDeleteHelper(ctx context.Context, img images.Image, records *[]imagetypes.DeleteResponse, force bool) error { 242 // First, determine if this image has any conflicts. Ignore soft conflicts 243 // if force is true. 244 c := conflictHard 245 if !force { 246 c |= conflictSoft 247 } 248 249 imgID := image.ID(img.Target.Digest) 250 251 err := i.checkImageDeleteConflict(ctx, imgID, c) 252 if err != nil { 253 return err 254 } 255 256 untaggedRef, err := reference.ParseAnyReference(img.Name) 257 if err != nil { 258 return err 259 } 260 err = i.client.ImageService().Delete(ctx, img.Name, images.SynchronousDelete()) 261 if err != nil { 262 return err 263 } 264 265 if !isDanglingImage(img) { 266 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 267 *records = append(*records, imagetypes.DeleteResponse{Untagged: reference.FamiliarString(untaggedRef)}) 268 } 269 270 return nil 271 } 272 273 // ImageDeleteConflict holds a soft or hard conflict and associated 274 // error. A hard conflict represents a running container using the 275 // image, while a soft conflict is any tags/digests referencing the 276 // given image or any stopped container using the image. 277 // Implements the error interface. 278 type imageDeleteConflict struct { 279 hard bool 280 used bool 281 reference string 282 message string 283 } 284 285 func (idc *imageDeleteConflict) Error() string { 286 var forceMsg string 287 if idc.hard { 288 forceMsg = "cannot be forced" 289 } else { 290 forceMsg = "must be forced" 291 } 292 return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", idc.reference, forceMsg, idc.message) 293 } 294 295 func (imageDeleteConflict) Conflict() {} 296 297 // checkImageDeleteConflict returns a conflict representing 298 // any issue preventing deletion of the given image ID, and 299 // nil if there are none. It takes a bitmask representing a 300 // filter for which conflict types the caller cares about, 301 // and will only check for these conflict types. 302 func (i *ImageService) checkImageDeleteConflict(ctx context.Context, imgID image.ID, mask conflictType) error { 303 if mask&conflictRunningContainer != 0 { 304 running := func(c *container.Container) bool { 305 return c.ImageID == imgID && c.IsRunning() 306 } 307 if ctr := i.containers.First(running); ctr != nil { 308 return &imageDeleteConflict{ 309 reference: stringid.TruncateID(imgID.String()), 310 hard: true, 311 used: true, 312 message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(ctr.ID)), 313 } 314 } 315 } 316 317 if mask&conflictStoppedContainer != 0 { 318 stopped := func(c *container.Container) bool { 319 return !c.IsRunning() && c.ImageID == imgID 320 } 321 if ctr := i.containers.First(stopped); ctr != nil { 322 return &imageDeleteConflict{ 323 reference: stringid.TruncateID(imgID.String()), 324 used: true, 325 message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(ctr.ID)), 326 } 327 } 328 } 329 330 if mask&conflictActiveReference != 0 { 331 refs, err := i.client.ImageService().List(ctx, "target.digest=="+imgID.String()) 332 if err != nil { 333 return err 334 } 335 if len(refs) > 1 { 336 return &imageDeleteConflict{ 337 reference: stringid.TruncateID(imgID.String()), 338 message: "image is referenced in multiple repositories", 339 } 340 } 341 } 342 343 return nil 344 }