github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_delete.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 cerrdefs "github.com/containerd/containerd/errdefs" 10 "github.com/containerd/containerd/images" 11 containerdimages "github.com/containerd/containerd/images" 12 "github.com/containerd/log" 13 "github.com/distribution/reference" 14 "github.com/docker/docker/api/types/events" 15 imagetypes "github.com/docker/docker/api/types/image" 16 "github.com/docker/docker/container" 17 dimages "github.com/docker/docker/daemon/images" 18 "github.com/docker/docker/image" 19 "github.com/docker/docker/internal/compatcontext" 20 "github.com/docker/docker/pkg/stringid" 21 "github.com/opencontainers/go-digest" 22 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 23 ) 24 25 // ImageDelete deletes the image referenced by the given imageRef from this 26 // daemon. The given imageRef can be an image ID, ID prefix, or a repository 27 // reference (with an optional tag or digest, defaulting to the tag name 28 // "latest"). There is differing behavior depending on whether the given 29 // imageRef is a repository reference or not. 30 // 31 // If the given imageRef is a repository reference then that repository 32 // reference will be removed. However, if there exists any containers which 33 // were created using the same image reference then the repository reference 34 // cannot be removed unless either there are other repository references to the 35 // same image or force is true. Following removal of the repository reference, 36 // the referenced image itself will attempt to be deleted as described below 37 // but quietly, meaning any image delete conflicts will cause the image to not 38 // be deleted and the conflict will not be reported. 39 // 40 // There may be conflicts preventing deletion of an image and these conflicts 41 // are divided into two categories grouped by their severity: 42 // 43 // Hard Conflict: 44 // - any running container using the image. 45 // 46 // Soft Conflict: 47 // - any stopped container using the image. 48 // - any repository tag or digest references to the image. 49 // 50 // The image cannot be removed if there are any hard conflicts and can be 51 // removed if there are soft conflicts only if force is true. 52 // 53 // If prune is true, ancestor images will each attempt to be deleted quietly, 54 // meaning any delete conflicts will cause the image to not be deleted and the 55 // conflict will not be reported. 56 // 57 // TODO(thaJeztah): image delete should send prometheus counters; see https://github.com/moby/moby/issues/45268 58 func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force, prune bool) (response []imagetypes.DeleteResponse, retErr error) { 59 start := time.Now() 60 defer func() { 61 if retErr == nil { 62 dimages.ImageActions.WithValues("delete").UpdateSince(start) 63 } 64 }() 65 66 var c conflictType 67 if !force { 68 c |= conflictSoft 69 } 70 71 img, all, err := i.resolveAllReferences(ctx, imageRef) 72 if err != nil { 73 return nil, err 74 } 75 76 var imgID image.ID 77 if img == nil { 78 if len(all) == 0 { 79 parsed, _ := reference.ParseAnyReference(imageRef) 80 return nil, dimages.ErrImageDoesNotExist{Ref: parsed} 81 } 82 imgID = image.ID(all[0].Target.Digest) 83 var named reference.Named 84 if !isImageIDPrefix(imgID.String(), imageRef) { 85 if nn, err := reference.ParseNormalizedNamed(imageRef); err == nil { 86 named = nn 87 } 88 } 89 sameRef, err := i.getSameReferences(ctx, named, all) 90 if err != nil { 91 return nil, err 92 } 93 94 if len(sameRef) == 0 && named != nil { 95 return nil, dimages.ErrImageDoesNotExist{Ref: named} 96 } 97 98 if len(sameRef) == len(all) && !force { 99 c &= ^conflictActiveReference 100 } 101 if named != nil && len(sameRef) > 0 && len(sameRef) != len(all) { 102 var records []imagetypes.DeleteResponse 103 for _, ref := range sameRef { 104 // TODO: Add with target 105 err := i.images.Delete(ctx, ref.Name) 106 if err != nil { 107 return nil, err 108 } 109 if nn, err := reference.ParseNormalizedNamed(ref.Name); err == nil { 110 familiarRef := reference.FamiliarString(nn) 111 i.logImageEvent(ref, familiarRef, events.ActionUnTag) 112 records = append(records, imagetypes.DeleteResponse{Untagged: familiarRef}) 113 } 114 } 115 return records, nil 116 } 117 } else { 118 imgID = image.ID(img.Target.Digest) 119 explicitDanglingRef := strings.HasPrefix(imageRef, imageNameDanglingPrefix) && isDanglingImage(*img) 120 if isImageIDPrefix(imgID.String(), imageRef) || explicitDanglingRef { 121 return i.deleteAll(ctx, imgID, all, c, prune) 122 } 123 parsedRef, err := reference.ParseNormalizedNamed(img.Name) 124 if err != nil { 125 return nil, err 126 } 127 128 sameRef, err := i.getSameReferences(ctx, parsedRef, all) 129 if err != nil { 130 return nil, err 131 } 132 if len(sameRef) != len(all) { 133 var records []imagetypes.DeleteResponse 134 for _, ref := range sameRef { 135 // TODO: Add with target 136 err := i.images.Delete(ctx, ref.Name) 137 if err != nil { 138 return nil, err 139 } 140 if nn, err := reference.ParseNormalizedNamed(ref.Name); err == nil { 141 familiarRef := reference.FamiliarString(nn) 142 i.logImageEvent(ref, familiarRef, events.ActionUnTag) 143 records = append(records, imagetypes.DeleteResponse{Untagged: familiarRef}) 144 } 145 } 146 return records, nil 147 } else if len(all) > 1 && !force { 148 // Since only a single used reference, remove all active 149 // TODO: Consider keeping the conflict and changing active 150 // reference calculation in image checker. 151 c &= ^conflictActiveReference 152 } 153 154 using := func(c *container.Container) bool { 155 return c.ImageID == imgID 156 } 157 // TODO: Should this also check parentage here? 158 ctr := i.containers.First(using) 159 if ctr != nil { 160 familiarRef := reference.FamiliarString(parsedRef) 161 if !force { 162 // If we removed the repository reference then 163 // this image would remain "dangling" and since 164 // we really want to avoid that the client must 165 // explicitly force its removal. 166 err := &imageDeleteConflict{ 167 reference: familiarRef, 168 used: true, 169 message: fmt.Sprintf("container %s is using its referenced image %s", 170 stringid.TruncateID(ctr.ID), 171 stringid.TruncateID(imgID.String())), 172 } 173 return nil, err 174 } 175 176 // Delete all images 177 err := i.softImageDelete(ctx, *img, all) 178 if err != nil { 179 return nil, err 180 } 181 182 i.logImageEvent(*img, familiarRef, events.ActionUnTag) 183 records := []imagetypes.DeleteResponse{{Untagged: familiarRef}} 184 return records, nil 185 } 186 } 187 188 return i.deleteAll(ctx, imgID, all, c, prune) 189 } 190 191 // deleteAll deletes the image from the daemon, and if prune is true, 192 // also deletes dangling parents if there is no conflict in doing so. 193 // Parent images are removed quietly, and if there is any issue/conflict 194 // it is logged but does not halt execution/an error is not returned. 195 func (i *ImageService) deleteAll(ctx context.Context, imgID image.ID, all []images.Image, c conflictType, prune bool) (records []imagetypes.DeleteResponse, err error) { 196 // Workaround for: https://github.com/moby/buildkit/issues/3797 197 possiblyDeletedConfigs := map[digest.Digest]struct{}{} 198 if len(all) > 0 && i.content != nil { 199 handled := map[digest.Digest]struct{}{} 200 for _, img := range all { 201 if _, ok := handled[img.Target.Digest]; ok { 202 continue 203 } else { 204 handled[img.Target.Digest] = struct{}{} 205 } 206 err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, d ocispec.Descriptor) error { 207 if images.IsConfigType(d.MediaType) { 208 possiblyDeletedConfigs[d.Digest] = struct{}{} 209 } 210 return nil 211 }) 212 if err != nil { 213 return nil, err 214 } 215 } 216 } 217 defer func() { 218 if len(possiblyDeletedConfigs) > 0 { 219 if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil { 220 log.G(ctx).WithError(err).Warn("failed to unlease snapshots") 221 } 222 } 223 }() 224 225 var parents []containerdimages.Image 226 if prune { 227 // TODO(dmcgowan): Consider using GC labels to walk for deletion 228 parents, err = i.parents(ctx, imgID) 229 if err != nil { 230 log.G(ctx).WithError(err).Warn("failed to get image parents") 231 } 232 } 233 234 for _, imageRef := range all { 235 if err := i.imageDeleteHelper(ctx, imageRef, all, &records, c); err != nil { 236 return records, err 237 } 238 } 239 i.LogImageEvent(imgID.String(), imgID.String(), events.ActionDelete) 240 records = append(records, imagetypes.DeleteResponse{Deleted: imgID.String()}) 241 242 for _, parent := range parents { 243 if !isDanglingImage(parent) { 244 break 245 } 246 err = i.imageDeleteHelper(ctx, parent, all, &records, conflictSoft) 247 if err != nil { 248 log.G(ctx).WithError(err).Warn("failed to remove image parent") 249 break 250 } 251 parentID := parent.Target.Digest.String() 252 i.LogImageEvent(parentID, parentID, events.ActionDelete) 253 records = append(records, imagetypes.DeleteResponse{Deleted: parentID}) 254 } 255 256 return records, nil 257 } 258 259 // isImageIDPrefix returns whether the given 260 // possiblePrefix is a prefix of the given imageID. 261 func isImageIDPrefix(imageID, possiblePrefix string) bool { 262 if strings.HasPrefix(imageID, possiblePrefix) { 263 return true 264 } 265 if i := strings.IndexRune(imageID, ':'); i >= 0 { 266 return strings.HasPrefix(imageID[i+1:], possiblePrefix) 267 } 268 return false 269 } 270 271 // getSameReferences returns the set of images which are the same as: 272 // - the provided img if non-nil 273 // - OR the first named image found in the provided image set 274 // - OR the full set of provided images if no named references in the set 275 // 276 // References are considered the same if: 277 // - Both contain the same name and tag 278 // - Both contain the same name, one is untagged and no other differing tags in set 279 // - One is dangling 280 // 281 // Note: All imgs should have the same target, only the image name will be considered 282 // for determining whether images are the same. 283 func (i *ImageService) getSameReferences(ctx context.Context, named reference.Named, imgs []images.Image) ([]images.Image, error) { 284 var ( 285 tag string 286 sameRef []images.Image 287 digestRefs = []images.Image{} 288 allTags bool 289 ) 290 if named != nil { 291 if tagged, ok := named.(reference.Tagged); ok { 292 tag = tagged.Tag() 293 } else if _, ok := named.(reference.Digested); ok { 294 // If digest is explicitly provided, match all tags 295 allTags = true 296 } 297 } 298 for _, ref := range imgs { 299 if !isDanglingImage(ref) { 300 if repoRef, err := reference.ParseNamed(ref.Name); err == nil { 301 if named == nil { 302 named = repoRef 303 if tagged, ok := named.(reference.Tagged); ok { 304 tag = tagged.Tag() 305 } 306 } else if named.Name() != repoRef.Name() { 307 continue 308 } else if !allTags { 309 if tagged, ok := repoRef.(reference.Tagged); ok { 310 if tag == "" { 311 tag = tagged.Tag() 312 } else if tag != tagged.Tag() { 313 // Same repo, different tag, do not include digest refs 314 digestRefs = nil 315 continue 316 } 317 } else { 318 if digestRefs != nil { 319 digestRefs = append(digestRefs, ref) 320 } 321 // Add digest refs at end if no other tags in the same name 322 continue 323 } 324 } 325 } else { 326 // Ignore names which do not parse 327 log.G(ctx).WithError(err).WithField("image", ref.Name).Info("failed to parse image name, ignoring") 328 } 329 } 330 sameRef = append(sameRef, ref) 331 } 332 if digestRefs != nil { 333 sameRef = append(sameRef, digestRefs...) 334 } 335 return sameRef, nil 336 } 337 338 type conflictType int 339 340 const ( 341 conflictRunningContainer conflictType = 1 << iota 342 conflictActiveReference 343 conflictStoppedContainer 344 conflictHard = conflictRunningContainer 345 conflictSoft = conflictActiveReference | conflictStoppedContainer 346 ) 347 348 // imageDeleteHelper attempts to delete the given image from this daemon. 349 // If the image has any hard delete conflicts (running containers using 350 // the image) then it cannot be deleted. If the image has any soft delete 351 // conflicts (any tags/digests referencing the image or any stopped container 352 // using the image) then it can only be deleted if force is true. Any deleted 353 // images and untagged references are appended to the given records. If any 354 // error or conflict is encountered, it will be returned immediately without 355 // deleting the image. 356 func (i *ImageService) imageDeleteHelper(ctx context.Context, img images.Image, all []images.Image, records *[]imagetypes.DeleteResponse, extra conflictType) error { 357 // First, determine if this image has any conflicts. Ignore soft conflicts 358 // if force is true. 359 c := conflictHard | extra 360 361 imgID := image.ID(img.Target.Digest) 362 363 err := i.checkImageDeleteConflict(ctx, imgID, all, c) 364 if err != nil { 365 return err 366 } 367 368 untaggedRef, err := reference.ParseAnyReference(img.Name) 369 if err != nil { 370 return err 371 } 372 373 if !isDanglingImage(img) && len(all) == 1 && extra&conflictActiveReference != 0 { 374 children, err := i.Children(ctx, imgID) 375 if err != nil { 376 return err 377 } 378 if len(children) > 0 { 379 img := images.Image{ 380 Name: danglingImageName(img.Target.Digest), 381 Target: img.Target, 382 CreatedAt: time.Now(), 383 Labels: img.Labels, 384 } 385 if _, err = i.images.Create(ctx, img); err != nil && !cerrdefs.IsAlreadyExists(err) { 386 return fmt.Errorf("failed to create dangling image: %w", err) 387 } 388 } 389 } 390 391 // TODO: Add target option 392 err = i.images.Delete(ctx, img.Name, images.SynchronousDelete()) 393 if err != nil { 394 return err 395 } 396 397 if !isDanglingImage(img) { 398 i.logImageEvent(img, reference.FamiliarString(untaggedRef), events.ActionUnTag) 399 *records = append(*records, imagetypes.DeleteResponse{Untagged: reference.FamiliarString(untaggedRef)}) 400 } 401 402 return nil 403 } 404 405 // ImageDeleteConflict holds a soft or hard conflict and associated 406 // error. A hard conflict represents a running container using the 407 // image, while a soft conflict is any tags/digests referencing the 408 // given image or any stopped container using the image. 409 // Implements the error interface. 410 type imageDeleteConflict struct { 411 hard bool 412 used bool 413 reference string 414 message string 415 } 416 417 func (idc *imageDeleteConflict) Error() string { 418 var forceMsg string 419 if idc.hard { 420 forceMsg = "cannot be forced" 421 } else { 422 forceMsg = "must be forced" 423 } 424 return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", idc.reference, forceMsg, idc.message) 425 } 426 427 func (imageDeleteConflict) Conflict() {} 428 429 // checkImageDeleteConflict returns a conflict representing 430 // any issue preventing deletion of the given image ID, and 431 // nil if there are none. It takes a bitmask representing a 432 // filter for which conflict types the caller cares about, 433 // and will only check for these conflict types. 434 func (i *ImageService) checkImageDeleteConflict(ctx context.Context, imgID image.ID, all []images.Image, mask conflictType) error { 435 if mask&conflictRunningContainer != 0 { 436 running := func(c *container.Container) bool { 437 return c.ImageID == imgID && c.IsRunning() 438 } 439 if ctr := i.containers.First(running); ctr != nil { 440 return &imageDeleteConflict{ 441 reference: stringid.TruncateID(imgID.String()), 442 hard: true, 443 used: true, 444 message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(ctr.ID)), 445 } 446 } 447 } 448 449 if mask&conflictStoppedContainer != 0 { 450 stopped := func(c *container.Container) bool { 451 return !c.IsRunning() && c.ImageID == imgID 452 } 453 if ctr := i.containers.First(stopped); ctr != nil { 454 return &imageDeleteConflict{ 455 reference: stringid.TruncateID(imgID.String()), 456 used: true, 457 message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(ctr.ID)), 458 } 459 } 460 } 461 462 if mask&conflictActiveReference != 0 { 463 // TODO: Count unexpired references... 464 if len(all) > 1 { 465 return &imageDeleteConflict{ 466 reference: stringid.TruncateID(imgID.String()), 467 message: "image is referenced in multiple repositories", 468 } 469 } 470 } 471 472 return nil 473 }