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