github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/images/image_delete.go (about) 1 package images // import "github.com/Prakhar-Agarwal-byte/moby/daemon/images" 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/distribution/reference" 10 "github.com/Prakhar-Agarwal-byte/moby/api/types/events" 11 imagetypes "github.com/Prakhar-Agarwal-byte/moby/api/types/image" 12 "github.com/Prakhar-Agarwal-byte/moby/container" 13 "github.com/Prakhar-Agarwal-byte/moby/errdefs" 14 "github.com/Prakhar-Agarwal-byte/moby/image" 15 "github.com/Prakhar-Agarwal-byte/moby/pkg/stringid" 16 "github.com/pkg/errors" 17 ) 18 19 type conflictType int 20 21 const ( 22 conflictDependentChild conflictType = 1 << iota 23 conflictRunningContainer 24 conflictActiveReference 25 conflictStoppedContainer 26 conflictHard = conflictDependentChild | conflictRunningContainer 27 conflictSoft = conflictActiveReference | conflictStoppedContainer 28 ) 29 30 // ImageDelete deletes the image referenced by the given imageRef from this 31 // daemon. The given imageRef can be an image ID, ID prefix, or a repository 32 // reference (with an optional tag or digest, defaulting to the tag name 33 // "latest"). There is differing behavior depending on whether the given 34 // imageRef is a repository reference or not. 35 // 36 // If the given imageRef is a repository reference then that repository 37 // reference will be removed. However, if there exists any containers which 38 // were created using the same image reference then the repository reference 39 // cannot be removed unless either there are other repository references to the 40 // same image or force is true. Following removal of the repository reference, 41 // the referenced image itself will attempt to be deleted as described below 42 // but quietly, meaning any image delete conflicts will cause the image to not 43 // be deleted and the conflict will not be reported. 44 // 45 // There may be conflicts preventing deletion of an image and these conflicts 46 // are divided into two categories grouped by their severity: 47 // 48 // Hard Conflict: 49 // - a pull or build using the image. 50 // - any descendant image. 51 // - any running container using the image. 52 // 53 // Soft Conflict: 54 // - any stopped container using the image. 55 // - any repository tag or digest references to the image. 56 // 57 // The image cannot be removed if there are any hard conflicts and can be 58 // removed if there are soft conflicts only if force is true. 59 // 60 // If prune is true, ancestor images will each attempt to be deleted quietly, 61 // meaning any delete conflicts will cause the image to not be deleted and the 62 // conflict will not be reported. 63 func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]imagetypes.DeleteResponse, error) { 64 start := time.Now() 65 records := []imagetypes.DeleteResponse{} 66 67 img, err := i.GetImage(ctx, imageRef, imagetypes.GetImageOpts{}) 68 if err != nil { 69 return nil, err 70 } 71 72 imgID := img.ID() 73 repoRefs := i.referenceStore.References(imgID.Digest()) 74 75 using := func(c *container.Container) bool { 76 return c.ImageID == imgID 77 } 78 79 var removedRepositoryRef bool 80 if !isImageIDPrefix(imgID.String(), imageRef) { 81 // A repository reference was given and should be removed 82 // first. We can only remove this reference if either force is 83 // true, there are multiple repository references to this 84 // image, or there are no containers using the given reference. 85 if !force && isSingleReference(repoRefs) { 86 if ctr := i.containers.First(using); ctr != nil { 87 // If we removed the repository reference then 88 // this image would remain "dangling" and since 89 // we really want to avoid that the client must 90 // explicitly force its removal. 91 err := errors.Errorf("conflict: unable to remove repository reference %q (must force) - container %s is using its referenced image %s", imageRef, stringid.TruncateID(ctr.ID), stringid.TruncateID(imgID.String())) 92 return nil, errdefs.Conflict(err) 93 } 94 } 95 96 parsedRef, err := reference.ParseNormalizedNamed(imageRef) 97 if err != nil { 98 return nil, err 99 } 100 101 parsedRef, err = i.removeImageRef(parsedRef) 102 if err != nil { 103 return nil, err 104 } 105 106 untaggedRecord := imagetypes.DeleteResponse{Untagged: reference.FamiliarString(parsedRef)} 107 108 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 109 records = append(records, untaggedRecord) 110 111 repoRefs = i.referenceStore.References(imgID.Digest()) 112 113 // If a tag reference was removed and the only remaining 114 // references to the same repository are digest references, 115 // then clean up those digest references. 116 if _, isCanonical := parsedRef.(reference.Canonical); !isCanonical { 117 foundRepoTagRef := false 118 for _, repoRef := range repoRefs { 119 if _, repoRefIsCanonical := repoRef.(reference.Canonical); !repoRefIsCanonical && parsedRef.Name() == repoRef.Name() { 120 foundRepoTagRef = true 121 break 122 } 123 } 124 if !foundRepoTagRef { 125 // Remove canonical references from same repository 126 var remainingRefs []reference.Named 127 for _, repoRef := range repoRefs { 128 if _, repoRefIsCanonical := repoRef.(reference.Canonical); repoRefIsCanonical && parsedRef.Name() == repoRef.Name() { 129 if _, err := i.removeImageRef(repoRef); err != nil { 130 return records, err 131 } 132 records = append(records, imagetypes.DeleteResponse{Untagged: reference.FamiliarString(repoRef)}) 133 } else { 134 remainingRefs = append(remainingRefs, repoRef) 135 } 136 } 137 repoRefs = remainingRefs 138 } 139 } 140 141 // If it has remaining references then the untag finished the remove 142 if len(repoRefs) > 0 { 143 return records, nil 144 } 145 146 removedRepositoryRef = true 147 } else { 148 // If an ID reference was given AND there is at most one tag 149 // reference to the image AND all references are within one 150 // repository, then remove all references. 151 if isSingleReference(repoRefs) { 152 c := conflictHard 153 if !force { 154 c |= conflictSoft &^ conflictActiveReference 155 } 156 if conflict := i.checkImageDeleteConflict(imgID, c); conflict != nil { 157 return nil, conflict 158 } 159 160 for _, repoRef := range repoRefs { 161 parsedRef, err := i.removeImageRef(repoRef) 162 if err != nil { 163 return nil, err 164 } 165 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 166 records = append(records, imagetypes.DeleteResponse{Untagged: reference.FamiliarString(parsedRef)}) 167 } 168 } 169 } 170 171 if err := i.imageDeleteHelper(imgID, &records, force, prune, removedRepositoryRef); err != nil { 172 return nil, err 173 } 174 175 imageActions.WithValues("delete").UpdateSince(start) 176 177 return records, nil 178 } 179 180 // isSingleReference returns true when all references are from one repository 181 // and there is at most one tag. Returns false for empty input. 182 func isSingleReference(repoRefs []reference.Named) bool { 183 if len(repoRefs) <= 1 { 184 return len(repoRefs) == 1 185 } 186 var singleRef reference.Named 187 canonicalRefs := map[string]struct{}{} 188 for _, repoRef := range repoRefs { 189 if _, isCanonical := repoRef.(reference.Canonical); isCanonical { 190 canonicalRefs[repoRef.Name()] = struct{}{} 191 } else if singleRef == nil { 192 singleRef = repoRef 193 } else { 194 return false 195 } 196 } 197 if singleRef == nil { 198 // Just use first canonical ref 199 singleRef = repoRefs[0] 200 } 201 _, ok := canonicalRefs[singleRef.Name()] 202 return len(canonicalRefs) == 1 && ok 203 } 204 205 // isImageIDPrefix returns whether the given possiblePrefix is a prefix of the 206 // given imageID. 207 func isImageIDPrefix(imageID, possiblePrefix string) bool { 208 if strings.HasPrefix(imageID, possiblePrefix) { 209 return true 210 } 211 212 if i := strings.IndexRune(imageID, ':'); i >= 0 { 213 return strings.HasPrefix(imageID[i+1:], possiblePrefix) 214 } 215 216 return false 217 } 218 219 // removeImageRef attempts to parse and remove the given image reference from 220 // this daemon's store of repository tag/digest references. The given 221 // repositoryRef must not be an image ID but a repository name followed by an 222 // optional tag or digest reference. If tag or digest is omitted, the default 223 // tag is used. Returns the resolved image reference and an error. 224 func (i *ImageService) removeImageRef(ref reference.Named) (reference.Named, error) { 225 ref = reference.TagNameOnly(ref) 226 227 // Ignore the boolean value returned, as far as we're concerned, this 228 // is an idempotent operation and it's okay if the reference didn't 229 // exist in the first place. 230 _, err := i.referenceStore.Delete(ref) 231 232 return ref, err 233 } 234 235 // removeAllReferencesToImageID attempts to remove every reference to the given 236 // imgID from this daemon's store of repository tag/digest references. Returns 237 // on the first encountered error. Removed references are logged to this 238 // daemon's event service. An "Untagged" types.ImageDeleteResponseItem is added to the 239 // given list of records. 240 func (i *ImageService) removeAllReferencesToImageID(imgID image.ID, records *[]imagetypes.DeleteResponse) error { 241 for _, imageRef := range i.referenceStore.References(imgID.Digest()) { 242 parsedRef, err := i.removeImageRef(imageRef) 243 if err != nil { 244 return err 245 } 246 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag) 247 *records = append(*records, imagetypes.DeleteResponse{ 248 Untagged: reference.FamiliarString(parsedRef), 249 }) 250 } 251 252 return nil 253 } 254 255 // ImageDeleteConflict holds a soft or hard conflict and an associated error. 256 // Implements the error interface. 257 type imageDeleteConflict struct { 258 hard bool 259 used bool 260 imgID image.ID 261 message string 262 } 263 264 func (idc *imageDeleteConflict) Error() string { 265 var forceMsg string 266 if idc.hard { 267 forceMsg = "cannot be forced" 268 } else { 269 forceMsg = "must be forced" 270 } 271 272 return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID.String()), forceMsg, idc.message) 273 } 274 275 func (idc *imageDeleteConflict) Conflict() {} 276 277 // imageDeleteHelper attempts to delete the given image from this daemon. If 278 // the image has any hard delete conflicts (child images or running containers 279 // using the image) then it cannot be deleted. If the image has any soft delete 280 // conflicts (any tags/digests referencing the image or any stopped container 281 // using the image) then it can only be deleted if force is true. If the delete 282 // succeeds and prune is true, the parent images are also deleted if they do 283 // not have any soft or hard delete conflicts themselves. Any deleted images 284 // and untagged references are appended to the given records. If any error or 285 // conflict is encountered, it will be returned immediately without deleting 286 // the image. If quiet is true, any encountered conflicts will be ignored and 287 // the function will return nil immediately without deleting the image. 288 func (i *ImageService) imageDeleteHelper(imgID image.ID, records *[]imagetypes.DeleteResponse, force, prune, quiet bool) error { 289 // First, determine if this image has any conflicts. Ignore soft conflicts 290 // if force is true. 291 c := conflictHard 292 if !force { 293 c |= conflictSoft 294 } 295 if conflict := i.checkImageDeleteConflict(imgID, c); conflict != nil { 296 if quiet && (!i.imageIsDangling(imgID) || conflict.used) { 297 // Ignore conflicts UNLESS the image is "dangling" or not being used in 298 // which case we want the user to know. 299 return nil 300 } 301 302 // There was a conflict and it's either a hard conflict OR we are not 303 // forcing deletion on soft conflicts. 304 return conflict 305 } 306 307 parent, err := i.imageStore.GetParent(imgID) 308 if err != nil { 309 // There may be no parent 310 parent = "" 311 } 312 313 // Delete all repository tag/digest references to this image. 314 if err := i.removeAllReferencesToImageID(imgID, records); err != nil { 315 return err 316 } 317 318 removedLayers, err := i.imageStore.Delete(imgID) 319 if err != nil { 320 return err 321 } 322 323 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionDelete) 324 *records = append(*records, imagetypes.DeleteResponse{Deleted: imgID.String()}) 325 for _, removedLayer := range removedLayers { 326 *records = append(*records, imagetypes.DeleteResponse{Deleted: removedLayer.ChainID.String()}) 327 } 328 329 if !prune || parent == "" { 330 return nil 331 } 332 333 // We need to prune the parent image. This means delete it if there are 334 // no tags/digests referencing it and there are no containers using it ( 335 // either running or stopped). 336 // Do not force prunings, but do so quietly (stopping on any encountered 337 // conflicts). 338 return i.imageDeleteHelper(parent, records, false, true, true) 339 } 340 341 // checkImageDeleteConflict determines whether there are any conflicts 342 // preventing deletion of the given image from this daemon. A hard conflict is 343 // any image which has the given image as a parent or any running container 344 // using the image. A soft conflict is any tags/digest referencing the given 345 // image or any stopped container using the image. If ignoreSoftConflicts is 346 // true, this function will not check for soft conflict conditions. 347 func (i *ImageService) checkImageDeleteConflict(imgID image.ID, mask conflictType) *imageDeleteConflict { 348 // Check if the image has any descendant images. 349 if mask&conflictDependentChild != 0 && len(i.imageStore.Children(imgID)) > 0 { 350 return &imageDeleteConflict{ 351 hard: true, 352 imgID: imgID, 353 message: "image has dependent child images", 354 } 355 } 356 357 if mask&conflictRunningContainer != 0 { 358 // Check if any running container is using the image. 359 running := func(c *container.Container) bool { 360 return c.ImageID == imgID && c.IsRunning() 361 } 362 if ctr := i.containers.First(running); ctr != nil { 363 return &imageDeleteConflict{ 364 imgID: imgID, 365 hard: true, 366 used: true, 367 message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(ctr.ID)), 368 } 369 } 370 } 371 372 // Check if any repository tags/digest reference this image. 373 if mask&conflictActiveReference != 0 && len(i.referenceStore.References(imgID.Digest())) > 0 { 374 return &imageDeleteConflict{ 375 imgID: imgID, 376 message: "image is referenced in multiple repositories", 377 } 378 } 379 380 if mask&conflictStoppedContainer != 0 { 381 // Check if any stopped containers reference this image. 382 stopped := func(c *container.Container) bool { 383 return !c.IsRunning() && c.ImageID == imgID 384 } 385 if ctr := i.containers.First(stopped); ctr != nil { 386 return &imageDeleteConflict{ 387 imgID: imgID, 388 used: true, 389 message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(ctr.ID)), 390 } 391 } 392 } 393 394 return nil 395 } 396 397 // imageIsDangling returns whether the given image is "dangling" which means 398 // that there are no repository references to the given image and it has no 399 // child images. 400 func (i *ImageService) imageIsDangling(imgID image.ID) bool { 401 return !(len(i.referenceStore.References(imgID.Digest())) > 0 || len(i.imageStore.Children(imgID)) > 0) 402 }