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