Update cleanup logic to use resolve all images
Ensure that when removing an image, an image is checked consistently against the images with the same target digest. Add unit testing around delete. Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
parent
529d19bad8
commit
87c87bccb5
10 changed files with 449 additions and 146 deletions
|
@ -12,8 +12,7 @@ import (
|
|||
// walkPresentChildren is a simple wrapper for containerdimages.Walk with presentChildrenHandler.
|
||||
// This is only a convenient helper to reduce boilerplate.
|
||||
func (i *ImageService) walkPresentChildren(ctx context.Context, target ocispec.Descriptor, f func(context.Context, ocispec.Descriptor) error) error {
|
||||
store := i.client.ContentStore()
|
||||
return containerdimages.Walk(ctx, presentChildrenHandler(store, containerdimages.HandlerFunc(
|
||||
return containerdimages.Walk(ctx, presentChildrenHandler(i.content, containerdimages.HandlerFunc(
|
||||
func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
return nil, f(ctx, desc)
|
||||
})), target)
|
||||
|
|
|
@ -43,8 +43,6 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options ima
|
|||
platform = cplatforms.OnlyStrict(*options.Platform)
|
||||
}
|
||||
|
||||
cs := i.client.ContentStore()
|
||||
|
||||
var presentImages []imagespec.DockerOCIImage
|
||||
err = i.walkImageManifests(ctx, desc, func(img *ImageManifest) error {
|
||||
conf, err := img.Config(ctx)
|
||||
|
@ -59,7 +57,7 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options ima
|
|||
}
|
||||
|
||||
var ociimage imagespec.DockerOCIImage
|
||||
if err := readConfig(ctx, cs, conf, &ociimage); err != nil {
|
||||
if err := readConfig(ctx, i.content, conf, &ociimage); err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"manifestDescriptor": img.Target(),
|
||||
|
@ -101,7 +99,7 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options ima
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tagged, err := i.client.ImageService().List(ctx, "target.digest=="+desc.Target.Digest.String())
|
||||
tagged, err := i.images.List(ctx, "target.digest=="+desc.Target.Digest.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -266,11 +264,9 @@ func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (contai
|
|||
return containerdimages.Image{}, errdefs.InvalidParameter(err)
|
||||
}
|
||||
|
||||
is := i.client.ImageService()
|
||||
|
||||
digested, ok := parsed.(reference.Digested)
|
||||
if ok {
|
||||
imgs, err := is.List(ctx, "target.digest=="+digested.Digest().String())
|
||||
imgs, err := i.images.List(ctx, "target.digest=="+digested.Digest().String())
|
||||
if err != nil {
|
||||
return containerdimages.Image{}, errors.Wrap(err, "failed to lookup digest")
|
||||
}
|
||||
|
@ -300,7 +296,7 @@ func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (contai
|
|||
}
|
||||
|
||||
ref := reference.TagNameOnly(parsed.(reference.Named)).String()
|
||||
img, err := is.Get(ctx, ref)
|
||||
img, err := i.images.Get(ctx, ref)
|
||||
if err == nil {
|
||||
return img, nil
|
||||
} else {
|
||||
|
@ -317,7 +313,7 @@ func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (contai
|
|||
fmt.Sprintf("name==%q", ref), // Or it could just look like one.
|
||||
"target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))),
|
||||
}
|
||||
imgs, err := is.List(ctx, filters...)
|
||||
imgs, err := i.images.List(ctx, filters...)
|
||||
if err != nil {
|
||||
return containerdimages.Image{}, err
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
// Children returns a slice of image IDs that are children of the `id` image
|
||||
func (i *ImageService) Children(ctx context.Context, id image.ID) ([]image.ID, error) {
|
||||
imgs, err := i.client.ImageService().List(ctx, "labels."+imageLabelClassicBuilderParent+"=="+string(id))
|
||||
imgs, err := i.images.List(ctx, "labels."+imageLabelClassicBuilderParent+"=="+string(id))
|
||||
if err != nil {
|
||||
return []image.ID{}, errdefs.System(errors.Wrap(err, "failed to list all images"))
|
||||
}
|
||||
|
@ -88,16 +88,14 @@ func (i *ImageService) parents(ctx context.Context, id image.ID) ([]imageWithRoo
|
|||
return nil, errors.Wrap(err, "failed to get child image")
|
||||
}
|
||||
|
||||
cs := i.client.ContentStore()
|
||||
|
||||
allPlatforms, err := containerdimages.Platforms(ctx, cs, target)
|
||||
allPlatforms, err := containerdimages.Platforms(ctx, i.content, target)
|
||||
if err != nil {
|
||||
return nil, errdefs.System(errors.Wrap(err, "failed to list platforms supported by image"))
|
||||
}
|
||||
|
||||
var childRootFS []ocispec.RootFS
|
||||
for _, platform := range allPlatforms {
|
||||
rootfs, err := platformRootfs(ctx, cs, target, platform)
|
||||
rootfs, err := platformRootfs(ctx, i.content, target, platform)
|
||||
if err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
continue
|
||||
|
@ -108,7 +106,7 @@ func (i *ImageService) parents(ctx context.Context, id image.ID) ([]imageWithRoo
|
|||
childRootFS = append(childRootFS, rootfs)
|
||||
}
|
||||
|
||||
imgs, err := i.client.ImageService().List(ctx)
|
||||
imgs, err := i.images.List(ctx)
|
||||
if err != nil {
|
||||
return nil, errdefs.System(errors.Wrap(err, "failed to list all images"))
|
||||
}
|
||||
|
@ -117,7 +115,7 @@ func (i *ImageService) parents(ctx context.Context, id image.ID) ([]imageWithRoo
|
|||
for _, img := range imgs {
|
||||
nextImage:
|
||||
for _, platform := range allPlatforms {
|
||||
rootfs, err := platformRootfs(ctx, cs, img.Target, platform)
|
||||
rootfs, err := platformRootfs(ctx, i.content, img.Target, platform)
|
||||
if err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
continue
|
||||
|
@ -158,7 +156,7 @@ func (i *ImageService) getParentsByBuilderLabel(ctx context.Context, img contain
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return i.client.ImageService().List(ctx, "target.digest=="+dgst.String())
|
||||
return i.images.List(ctx, "target.digest=="+dgst.String())
|
||||
}
|
||||
|
||||
type imageWithRootfs struct {
|
||||
|
|
|
@ -53,123 +53,161 @@ import (
|
|||
//
|
||||
// TODO(thaJeztah): image delete should send prometheus counters; see https://github.com/moby/moby/issues/45268
|
||||
func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]imagetypes.DeleteResponse, error) {
|
||||
parsedRef, err := reference.ParseNormalizedNamed(imageRef)
|
||||
var c conflictType
|
||||
if !force {
|
||||
c |= conflictSoft
|
||||
}
|
||||
|
||||
img, all, err := i.resolveAllReferences(ctx, imageRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img, err := i.resolveImage(ctx, imageRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imgID := image.ID(img.Target.Digest)
|
||||
|
||||
explicitDanglingRef := strings.HasPrefix(imageRef, imageNameDanglingPrefix) && isDanglingImage(img)
|
||||
if isImageIDPrefix(imgID.String(), imageRef) || explicitDanglingRef {
|
||||
return i.deleteAll(ctx, img, force, prune)
|
||||
}
|
||||
|
||||
singleRef, err := i.isSingleReference(ctx, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !singleRef {
|
||||
err := i.client.ImageService().Delete(ctx, img.Name)
|
||||
var imgID image.ID
|
||||
if img == nil {
|
||||
if len(all) == 0 {
|
||||
parsed, _ := reference.ParseAnyReference(imageRef)
|
||||
return nil, dimages.ErrImageDoesNotExist{Ref: parsed}
|
||||
}
|
||||
imgID = image.ID(all[0].Target.Digest)
|
||||
sameRef, err := i.getSameReferences(ctx, nil, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag)
|
||||
records := []imagetypes.DeleteResponse{{Untagged: reference.FamiliarString(reference.TagNameOnly(parsedRef))}}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
using := func(c *container.Container) bool {
|
||||
return c.ImageID == imgID
|
||||
}
|
||||
ctr := i.containers.First(using)
|
||||
if ctr != nil {
|
||||
if !force {
|
||||
// If we removed the repository reference then
|
||||
// this image would remain "dangling" and since
|
||||
// we really want to avoid that the client must
|
||||
// explicitly force its removal.
|
||||
refString := reference.FamiliarString(reference.TagNameOnly(parsedRef))
|
||||
err := &imageDeleteConflict{
|
||||
reference: refString,
|
||||
used: true,
|
||||
message: fmt.Sprintf("container %s is using its referenced image %s",
|
||||
stringid.TruncateID(ctr.ID),
|
||||
stringid.TruncateID(imgID.String())),
|
||||
if len(sameRef) == len(all) && !force {
|
||||
c &= ^conflictActiveReference
|
||||
}
|
||||
} else {
|
||||
imgID = image.ID(img.Target.Digest)
|
||||
explicitDanglingRef := strings.HasPrefix(imageRef, imageNameDanglingPrefix) && isDanglingImage(*img)
|
||||
if isImageIDPrefix(imgID.String(), imageRef) || explicitDanglingRef {
|
||||
return i.deleteAll(ctx, imgID, all, c, prune)
|
||||
}
|
||||
parsedRef, err := reference.ParseNormalizedNamed(img.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sameRef, err := i.getSameReferences(ctx, img, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sameRef) != len(all) {
|
||||
var records []imagetypes.DeleteResponse
|
||||
for _, ref := range sameRef {
|
||||
// TODO: Add with target
|
||||
err := i.images.Delete(ctx, ref.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nn, err := reference.ParseNormalizedNamed(ref.Name); err == nil {
|
||||
familiarRef := reference.FamiliarString(nn)
|
||||
i.logImageEvent(ref, familiarRef, events.ActionUnTag)
|
||||
records = append(records, imagetypes.DeleteResponse{Untagged: familiarRef})
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
return records, nil
|
||||
} else if !force {
|
||||
// Since only a single used reference, remove all active
|
||||
// TODO: Consider keeping the conflict and changing active
|
||||
// reference calculation in image checker.
|
||||
c &= ^conflictActiveReference
|
||||
}
|
||||
|
||||
err := i.softImageDelete(ctx, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
using := func(c *container.Container) bool {
|
||||
return c.ImageID == imgID
|
||||
}
|
||||
ctr := i.containers.First(using)
|
||||
if ctr != nil {
|
||||
familiarRef := reference.FamiliarString(parsedRef)
|
||||
if !force {
|
||||
// If we removed the repository reference then
|
||||
// this image would remain "dangling" and since
|
||||
// we really want to avoid that the client must
|
||||
// explicitly force its removal.
|
||||
err := &imageDeleteConflict{
|
||||
reference: familiarRef,
|
||||
used: true,
|
||||
message: fmt.Sprintf("container %s is using its referenced image %s",
|
||||
stringid.TruncateID(ctr.ID),
|
||||
stringid.TruncateID(imgID.String())),
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag)
|
||||
records := []imagetypes.DeleteResponse{{Untagged: reference.FamiliarString(reference.TagNameOnly(parsedRef))}}
|
||||
return records, nil
|
||||
// Delete all images
|
||||
err := i.softImageDelete(ctx, *img, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.logImageEvent(*img, familiarRef, events.ActionUnTag)
|
||||
records := []imagetypes.DeleteResponse{{Untagged: familiarRef}}
|
||||
return records, nil
|
||||
}
|
||||
}
|
||||
|
||||
return i.deleteAll(ctx, img, force, prune)
|
||||
return i.deleteAll(ctx, imgID, all, c, prune)
|
||||
}
|
||||
|
||||
// deleteAll deletes the image from the daemon, and if prune is true,
|
||||
// also deletes dangling parents if there is no conflict in doing so.
|
||||
// Parent images are removed quietly, and if there is any issue/conflict
|
||||
// it is logged but does not halt execution/an error is not returned.
|
||||
func (i *ImageService) deleteAll(ctx context.Context, img images.Image, force, prune bool) ([]imagetypes.DeleteResponse, error) {
|
||||
var records []imagetypes.DeleteResponse
|
||||
|
||||
func (i *ImageService) deleteAll(ctx context.Context, imgID image.ID, all []images.Image, c conflictType, prune bool) (records []imagetypes.DeleteResponse, err error) {
|
||||
// Workaround for: https://github.com/moby/buildkit/issues/3797
|
||||
possiblyDeletedConfigs := map[digest.Digest]struct{}{}
|
||||
err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, d ocispec.Descriptor) error {
|
||||
if images.IsConfigType(d.MediaType) {
|
||||
possiblyDeletedConfigs[d.Digest] = struct{}{}
|
||||
if len(all) > 0 && i.content != nil {
|
||||
handled := map[digest.Digest]struct{}{}
|
||||
for _, img := range all {
|
||||
if _, ok := handled[img.Target.Digest]; ok {
|
||||
continue
|
||||
} else {
|
||||
handled[img.Target.Digest] = struct{}{}
|
||||
}
|
||||
err := i.walkPresentChildren(ctx, img.Target, func(_ context.Context, d ocispec.Descriptor) error {
|
||||
if images.IsConfigType(d.MediaType) {
|
||||
possiblyDeletedConfigs[d.Digest] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil {
|
||||
log.G(ctx).WithError(err).Warn("failed to unlease snapshots")
|
||||
if len(possiblyDeletedConfigs) > 0 {
|
||||
if err := i.unleaseSnapshotsFromDeletedConfigs(compatcontext.WithoutCancel(ctx), possiblyDeletedConfigs); err != nil {
|
||||
log.G(ctx).WithError(err).Warn("failed to unlease snapshots")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
imgID := img.Target.Digest.String()
|
||||
|
||||
var parents []imageWithRootfs
|
||||
if prune {
|
||||
parents, err = i.parents(ctx, image.ID(imgID))
|
||||
// TODO(dmcgowan): Consider using GC labels to walk for deletion
|
||||
parents, err = i.parents(ctx, imgID)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Warn("failed to get image parents")
|
||||
}
|
||||
sortParentsByAffinity(parents)
|
||||
}
|
||||
|
||||
imageRefs, err := i.client.ImageService().List(ctx, "target.digest=="+imgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, imageRef := range imageRefs {
|
||||
if err := i.imageDeleteHelper(ctx, imageRef, &records, force); err != nil {
|
||||
for _, imageRef := range all {
|
||||
if err := i.imageDeleteHelper(ctx, imageRef, all, &records, c); err != nil {
|
||||
return records, err
|
||||
}
|
||||
}
|
||||
i.LogImageEvent(imgID, imgID, events.ActionDelete)
|
||||
records = append(records, imagetypes.DeleteResponse{Deleted: imgID})
|
||||
i.LogImageEvent(imgID.String(), imgID.String(), events.ActionDelete)
|
||||
records = append(records, imagetypes.DeleteResponse{Deleted: imgID.String()})
|
||||
|
||||
for _, parent := range parents {
|
||||
if !isDanglingImage(parent.img) {
|
||||
break
|
||||
}
|
||||
err = i.imageDeleteHelper(ctx, parent.img, &records, false)
|
||||
err = i.imageDeleteHelper(ctx, parent.img, all, &records, conflictSoft)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Warn("failed to remove image parent")
|
||||
break
|
||||
|
@ -205,19 +243,71 @@ func sortParentsByAffinity(parents []imageWithRootfs) {
|
|||
})
|
||||
}
|
||||
|
||||
// isSingleReference returns true if there are no other images in the
|
||||
// daemon targeting the same content as `img` that are not dangling.
|
||||
func (i *ImageService) isSingleReference(ctx context.Context, img images.Image) (bool, error) {
|
||||
refs, err := i.client.ImageService().List(ctx, "target.digest=="+img.Target.Digest.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if !isDanglingImage(ref) && ref.Name != img.Name {
|
||||
return false, nil
|
||||
// getSameReferences returns the set of images which are the same as:
|
||||
// - the provided img if non-nil
|
||||
// - OR the first named image found in the provided image set
|
||||
// - OR the full set of provided images if no named references in the set
|
||||
//
|
||||
// References are considered the same if:
|
||||
// - Both contain the same name and tag
|
||||
// - Both contain the same name, one is untagged and no other differing tags in set
|
||||
// - One is dangling
|
||||
//
|
||||
// Note: All imgs should have the same target, only the image name will be considered
|
||||
// for determining whether images are the same.
|
||||
func (i *ImageService) getSameReferences(ctx context.Context, img *images.Image, imgs []images.Image) ([]images.Image, error) {
|
||||
var (
|
||||
named reference.Named
|
||||
tag string
|
||||
sameRef []images.Image
|
||||
digestRefs = []images.Image{}
|
||||
)
|
||||
if img != nil {
|
||||
repoRef, err := reference.ParseNamed(img.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
named = repoRef
|
||||
if tagged, ok := named.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
for _, ref := range imgs {
|
||||
if !isDanglingImage(ref) {
|
||||
if repoRef, err := reference.ParseNamed(ref.Name); err == nil {
|
||||
if named == nil {
|
||||
named = repoRef
|
||||
if tagged, ok := named.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
} else if named.Name() != repoRef.Name() {
|
||||
continue
|
||||
} else if tagged, ok := repoRef.(reference.Tagged); ok {
|
||||
if tag == "" {
|
||||
tag = tagged.Tag()
|
||||
} else if tag != tagged.Tag() {
|
||||
// Same repo, different tag, do not include digest refs
|
||||
digestRefs = nil
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if digestRefs != nil {
|
||||
digestRefs = append(digestRefs, ref)
|
||||
}
|
||||
// Add digest refs at end if no other tags in the same name
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Ignore names which do not parse
|
||||
log.G(ctx).WithError(err).WithField("image", ref.Name).Info("failed to parse image name, ignoring")
|
||||
}
|
||||
}
|
||||
sameRef = append(sameRef, ref)
|
||||
}
|
||||
if digestRefs != nil {
|
||||
sameRef = append(sameRef, digestRefs...)
|
||||
}
|
||||
return sameRef, nil
|
||||
}
|
||||
|
||||
type conflictType int
|
||||
|
@ -238,17 +328,14 @@ const (
|
|||
// images and untagged references are appended to the given records. If any
|
||||
// error or conflict is encountered, it will be returned immediately without
|
||||
// deleting the image.
|
||||
func (i *ImageService) imageDeleteHelper(ctx context.Context, img images.Image, records *[]imagetypes.DeleteResponse, force bool) error {
|
||||
func (i *ImageService) imageDeleteHelper(ctx context.Context, img images.Image, all []images.Image, records *[]imagetypes.DeleteResponse, extra conflictType) error {
|
||||
// First, determine if this image has any conflicts. Ignore soft conflicts
|
||||
// if force is true.
|
||||
c := conflictHard
|
||||
if !force {
|
||||
c |= conflictSoft
|
||||
}
|
||||
c := conflictHard | extra
|
||||
|
||||
imgID := image.ID(img.Target.Digest)
|
||||
|
||||
err := i.checkImageDeleteConflict(ctx, imgID, c)
|
||||
err := i.checkImageDeleteConflict(ctx, imgID, all, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -257,13 +344,15 @@ func (i *ImageService) imageDeleteHelper(ctx context.Context, img images.Image,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = i.client.ImageService().Delete(ctx, img.Name, images.SynchronousDelete())
|
||||
|
||||
// TODO: Add target option
|
||||
err = i.images.Delete(ctx, img.Name, images.SynchronousDelete())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isDanglingImage(img) {
|
||||
i.LogImageEvent(imgID.String(), imgID.String(), events.ActionUnTag)
|
||||
i.logImageEvent(img, reference.FamiliarString(untaggedRef), events.ActionUnTag)
|
||||
*records = append(*records, imagetypes.DeleteResponse{Untagged: reference.FamiliarString(untaggedRef)})
|
||||
}
|
||||
|
||||
|
@ -299,7 +388,7 @@ func (imageDeleteConflict) Conflict() {}
|
|||
// nil if there are none. It takes a bitmask representing a
|
||||
// filter for which conflict types the caller cares about,
|
||||
// and will only check for these conflict types.
|
||||
func (i *ImageService) checkImageDeleteConflict(ctx context.Context, imgID image.ID, mask conflictType) error {
|
||||
func (i *ImageService) checkImageDeleteConflict(ctx context.Context, imgID image.ID, all []images.Image, mask conflictType) error {
|
||||
if mask&conflictRunningContainer != 0 {
|
||||
running := func(c *container.Container) bool {
|
||||
return c.ImageID == imgID && c.IsRunning()
|
||||
|
@ -328,11 +417,8 @@ func (i *ImageService) checkImageDeleteConflict(ctx context.Context, imgID image
|
|||
}
|
||||
|
||||
if mask&conflictActiveReference != 0 {
|
||||
refs, err := i.client.ImageService().List(ctx, "target.digest=="+imgID.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(refs) > 1 {
|
||||
// TODO: Count unexpired references...
|
||||
if len(all) > 1 {
|
||||
return &imageDeleteConflict{
|
||||
reference: stringid.TruncateID(imgID.String()),
|
||||
message: "image is referenced in multiple repositories",
|
||||
|
|
218
daemon/containerd/image_delete_test.go
Normal file
218
daemon/containerd/image_delete_test.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/metadata"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/log/logtest"
|
||||
"github.com/docker/docker/container"
|
||||
daemonevents "github.com/docker/docker/daemon/events"
|
||||
dimages "github.com/docker/docker/daemon/images"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestImageDelete(t *testing.T) {
|
||||
ctx := namespaces.WithNamespace(context.TODO(), "testing")
|
||||
|
||||
for _, tc := range []struct {
|
||||
ref string
|
||||
starting []images.Image
|
||||
remaining []images.Image
|
||||
err error
|
||||
// TODO: Records
|
||||
// TODO: Containers
|
||||
// TODO: Events
|
||||
}{
|
||||
{
|
||||
ref: "nothingthere",
|
||||
err: dimages.ErrImageDoesNotExist{Ref: nameTag("nothingthere", "latest")},
|
||||
},
|
||||
{
|
||||
ref: "justoneimage",
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/justoneimage:latest",
|
||||
Target: desc(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: "justoneref",
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/justoneref:latest",
|
||||
Target: desc(10),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/differentrepo:latest",
|
||||
Target: desc(10),
|
||||
},
|
||||
},
|
||||
remaining: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/differentrepo:latest",
|
||||
Target: desc(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: "hasdigest",
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/hasdigest:latest",
|
||||
Target: desc(10),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/hasdigest@" + digestFor(10).String(),
|
||||
Target: desc(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: digestFor(11).String(),
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/byid:latest",
|
||||
Target: desc(11),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/byid@" + digestFor(11).String(),
|
||||
Target: desc(11),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: "bydigest@" + digestFor(12).String(),
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/bydigest:latest",
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/bydigest@" + digestFor(12).String(),
|
||||
Target: desc(12),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: "onerefoftwo",
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/onerefoftwo:latest",
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/onerefoftwo:other",
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/onerefoftwo@" + digestFor(12).String(),
|
||||
Target: desc(12),
|
||||
},
|
||||
},
|
||||
remaining: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/onerefoftwo:other",
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/onerefoftwo@" + digestFor(12).String(),
|
||||
Target: desc(12),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ref: "otherreporemaining",
|
||||
starting: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/otherreporemaining:latest",
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/otherreporemaining@" + digestFor(12).String(),
|
||||
Target: desc(12),
|
||||
},
|
||||
{
|
||||
Name: "docker.io/library/someotherrepo:latest",
|
||||
Target: desc(12),
|
||||
},
|
||||
},
|
||||
remaining: []images.Image{
|
||||
{
|
||||
Name: "docker.io/library/someotherrepo:latest",
|
||||
Target: desc(12),
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.ref, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := logtest.WithT(ctx, t)
|
||||
mdb := newTestDB(ctx, t)
|
||||
service := &ImageService{
|
||||
images: metadata.NewImageStore(mdb),
|
||||
containers: emptyTestContainerStore(),
|
||||
eventsService: daemonevents.New(),
|
||||
}
|
||||
for _, img := range tc.starting {
|
||||
if _, err := service.images.Create(ctx, img); err != nil {
|
||||
t.Fatalf("failed to create image %q: %v", img.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := service.ImageDelete(ctx, tc.ref, false, false)
|
||||
if tc.err == nil {
|
||||
assert.NilError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err, tc.err.Error())
|
||||
}
|
||||
|
||||
all, err := service.images.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, len(tc.remaining), len(all))
|
||||
|
||||
// Order should match
|
||||
for i := range all {
|
||||
assert.Check(t, is.Equal(all[i].Name, tc.remaining[i].Name), "image[%d]", i)
|
||||
assert.Check(t, is.Equal(all[i].Target.Digest, tc.remaining[i].Target.Digest), "image[%d]", i)
|
||||
// TODO: Check labels too
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type testContainerStore struct{}
|
||||
|
||||
func emptyTestContainerStore() container.Store {
|
||||
return &testContainerStore{}
|
||||
}
|
||||
|
||||
func (*testContainerStore) Add(string, *container.Container) {}
|
||||
|
||||
func (*testContainerStore) Get(string) *container.Container {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*testContainerStore) Delete(string) {}
|
||||
|
||||
func (*testContainerStore) List() []*container.Container {
|
||||
return []*container.Container{}
|
||||
}
|
||||
|
||||
func (*testContainerStore) Size() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (*testContainerStore) First(container.StoreFilter) *container.Container {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*testContainerStore) ApplyAll(container.StoreReducer) {}
|
|
@ -3,6 +3,7 @@ package containerd
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
)
|
||||
|
@ -27,6 +28,18 @@ func (i *ImageService) LogImageEvent(imageID, refName string, action events.Acti
|
|||
})
|
||||
}
|
||||
|
||||
// logImageEvent generates an event related to an image with only name attribute.
|
||||
func (i *ImageService) logImageEvent(img images.Image, refName string, action events.Action) {
|
||||
attributes := map[string]string{}
|
||||
if refName != "" {
|
||||
attributes["name"] = refName
|
||||
}
|
||||
i.eventsService.Log(action, events.ImageEventType, events.Actor{
|
||||
ID: img.Target.Digest.String(),
|
||||
Attributes: attributes,
|
||||
})
|
||||
}
|
||||
|
||||
// copyAttributes guarantees that labels are not mutated by event triggers.
|
||||
func copyAttributes(attributes, labels map[string]string) {
|
||||
if labels == nil {
|
||||
|
|
|
@ -64,10 +64,8 @@ func (i *ImageService) ImagesPrune(ctx context.Context, fltrs filters.Args) (*ty
|
|||
|
||||
func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFunc, danglingOnly bool) (*types.ImagesPruneReport, error) {
|
||||
report := types.ImagesPruneReport{}
|
||||
is := i.client.ImageService()
|
||||
store := i.client.ContentStore()
|
||||
|
||||
allImages, err := i.client.ImageService().List(ctx)
|
||||
allImages, err := i.images.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -173,7 +171,7 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
|
|||
}
|
||||
continue
|
||||
}
|
||||
err = is.Delete(ctx, img.Name, containerdimages.SynchronousDelete())
|
||||
err = i.images.Delete(ctx, img.Name, containerdimages.SynchronousDelete())
|
||||
if err != nil && !cerrdefs.IsNotFound(err) {
|
||||
errs = multierror.Append(errs, err)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
|
@ -190,7 +188,7 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
|
|||
|
||||
// Check which blobs have been deleted and sum their sizes
|
||||
for _, blob := range blobs {
|
||||
_, err := store.ReaderAt(ctx, blob)
|
||||
_, err := i.content.ReaderAt(ctx, blob)
|
||||
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
report.ImagesDeleted = append(report.ImagesDeleted,
|
||||
|
@ -211,10 +209,7 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
|
|||
// This is a temporary solution to the rootfs snapshot not being deleted when there's a buildkit history
|
||||
// item referencing an image config.
|
||||
func (i *ImageService) unleaseSnapshotsFromDeletedConfigs(ctx context.Context, possiblyDeletedConfigs map[digest.Digest]struct{}) error {
|
||||
is := i.client.ImageService()
|
||||
store := i.client.ContentStore()
|
||||
|
||||
all, err := is.List(ctx)
|
||||
all, err := i.images.List(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list images during snapshot lease removal")
|
||||
}
|
||||
|
@ -238,7 +233,7 @@ func (i *ImageService) unleaseSnapshotsFromDeletedConfigs(ctx context.Context, p
|
|||
|
||||
// At this point, all configs that are used by any image has been removed from the slice
|
||||
for cfgDigest := range possiblyDeletedConfigs {
|
||||
info, err := store.Info(ctx, cfgDigest)
|
||||
info, err := i.content.Info(ctx, cfgDigest)
|
||||
if err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
log.G(ctx).WithField("config", cfgDigest).Debug("config already gone")
|
||||
|
@ -254,7 +249,7 @@ func (i *ImageService) unleaseSnapshotsFromDeletedConfigs(ctx context.Context, p
|
|||
label := "containerd.io/gc.ref.snapshot." + i.StorageDriver()
|
||||
|
||||
delete(info.Labels, label)
|
||||
_, err = store.Update(ctx, info, "labels."+label)
|
||||
_, err = i.content.Update(ctx, info, "labels."+label)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, errors.Wrapf(err, "failed to remove gc.ref.snapshot label from %s", cfgDigest))
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package containerd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cerrdefs "github.com/containerd/containerd/errdefs"
|
||||
containerdimages "github.com/containerd/containerd/images"
|
||||
|
@ -34,9 +35,11 @@ func (i *ImageService) TagImage(ctx context.Context, imageID image.ID, newTag re
|
|||
return errdefs.System(errors.Wrapf(err, "failed to create image with name %s and target %s", newImg.Name, newImg.Target.Digest.String()))
|
||||
}
|
||||
|
||||
replacedImg, err := is.Get(ctx, newImg.Name)
|
||||
replacedImg, all, err := i.resolveAllReferences(ctx, newImg.Name)
|
||||
if err != nil {
|
||||
return errdefs.Unknown(errors.Wrapf(err, "creating image %s failed because it already exists, but accessing it also failed", newImg.Name))
|
||||
} else if replacedImg == nil {
|
||||
return errdefs.Unknown(fmt.Errorf("creating image %s failed because it already exists, but failed to resolve", newImg.Name))
|
||||
}
|
||||
|
||||
// Check if image we would replace already resolves to the same target.
|
||||
|
@ -47,7 +50,7 @@ func (i *ImageService) TagImage(ctx context.Context, imageID image.ID, newTag re
|
|||
}
|
||||
|
||||
// If there already exists an image with this tag, delete it
|
||||
if err := i.softImageDelete(ctx, replacedImg); err != nil {
|
||||
if err := i.softImageDelete(ctx, *replacedImg, all); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete previous image %s", replacedImg.Name)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/content"
|
||||
cerrdefs "github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/plugin"
|
||||
|
@ -15,7 +16,7 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/container"
|
||||
daemonevents "github.com/docker/docker/daemon/events"
|
||||
daemonimages "github.com/docker/docker/daemon/images"
|
||||
dimages "github.com/docker/docker/daemon/images"
|
||||
"github.com/docker/docker/daemon/snapshotter"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/image"
|
||||
|
@ -29,6 +30,7 @@ import (
|
|||
type ImageService struct {
|
||||
client *containerd.Client
|
||||
images images.Store
|
||||
content content.Store
|
||||
containers container.Store
|
||||
snapshotter string
|
||||
registryHosts docker.RegistryHosts
|
||||
|
@ -62,6 +64,7 @@ func NewService(config ImageServiceConfig) *ImageService {
|
|||
return &ImageService{
|
||||
client: config.Client,
|
||||
images: config.Client.ImageService(),
|
||||
content: config.Client.ContentStore(),
|
||||
containers: config.Containers,
|
||||
snapshotter: config.Snapshotter,
|
||||
registryHosts: config.RegistryHosts,
|
||||
|
@ -73,8 +76,8 @@ func NewService(config ImageServiceConfig) *ImageService {
|
|||
}
|
||||
|
||||
// DistributionServices return services controlling daemon image storage.
|
||||
func (i *ImageService) DistributionServices() daemonimages.DistributionServices {
|
||||
return daemonimages.DistributionServices{}
|
||||
func (i *ImageService) DistributionServices() dimages.DistributionServices {
|
||||
return dimages.DistributionServices{}
|
||||
}
|
||||
|
||||
// CountImages returns the number of images stored by ImageService
|
||||
|
|
|
@ -17,23 +17,13 @@ const imageNameDanglingPrefix = "moby-dangling@"
|
|||
// softImageDelete deletes the image, making sure that there are other images
|
||||
// that reference the content of the deleted image.
|
||||
// If no other image exists, a dangling one is created.
|
||||
func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages.Image) error {
|
||||
is := i.client.ImageService()
|
||||
|
||||
// If the image already exists, persist it as dangling image
|
||||
// but only if no other image has the same target.
|
||||
dgst := img.Target.Digest.String()
|
||||
imgs, err := is.List(ctx, "target.digest=="+dgst)
|
||||
if err != nil {
|
||||
return errdefs.System(errors.Wrapf(err, "failed to check if there are images targeting digest %s", dgst))
|
||||
}
|
||||
|
||||
func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages.Image, imgs []containerdimages.Image) error {
|
||||
// From this point explicitly ignore the passed context
|
||||
// and don't allow to interrupt operation in the middle.
|
||||
|
||||
// Create dangling image if this is the last image pointing to this target.
|
||||
if len(imgs) == 1 {
|
||||
err = i.ensureDanglingImage(compatcontext.WithoutCancel(ctx), img)
|
||||
err := i.ensureDanglingImage(compatcontext.WithoutCancel(ctx), img)
|
||||
|
||||
// Error out in case we couldn't persist the old image.
|
||||
if err != nil {
|
||||
|
@ -43,7 +33,8 @@ func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages
|
|||
}
|
||||
|
||||
// Free the target name.
|
||||
err = is.Delete(compatcontext.WithoutCancel(ctx), img.Name)
|
||||
// TODO: Add with target option
|
||||
err := i.images.Delete(compatcontext.WithoutCancel(ctx), img.Name)
|
||||
if err != nil {
|
||||
if !cerrdefs.IsNotFound(err) {
|
||||
return errdefs.System(errors.Wrapf(err, "failed to delete image %s which existed a moment before", img.Name))
|
||||
|
@ -67,7 +58,7 @@ func (i *ImageService) ensureDanglingImage(ctx context.Context, from containerdi
|
|||
}
|
||||
danglingImage.Name = danglingImageName(from.Target.Digest)
|
||||
|
||||
_, err := i.client.ImageService().Create(compatcontext.WithoutCancel(ctx), danglingImage)
|
||||
_, err := i.images.Create(compatcontext.WithoutCancel(ctx), danglingImage)
|
||||
// If it already exists, then just continue.
|
||||
if cerrdefs.IsAlreadyExists(err) {
|
||||
return nil
|
||||
|
@ -81,5 +72,6 @@ func danglingImageName(digest digest.Digest) string {
|
|||
}
|
||||
|
||||
func isDanglingImage(image containerdimages.Image) bool {
|
||||
// TODO: Also check for expired
|
||||
return image.Name == danglingImageName(image.Target.Digest)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue