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:
Derek McGowan 2023-11-16 21:55:54 -08:00
parent 529d19bad8
commit 87c87bccb5
No known key found for this signature in database
GPG key ID: F58C5D0A4405ACDB
10 changed files with 449 additions and 146 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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",

View 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) {}

View file

@ -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 {

View file

@ -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) {

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}