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