Merge pull request #46840 from dmcgowan/c8d-rmi-cleanup

containerd: Image delete fixes and cleanup
This commit is contained in:
Sebastiaan van Stijn
2023-12-19 18:07:16 +01:00
committed by GitHub
15 changed files with 1129 additions and 149 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

@@ -29,6 +29,8 @@ import (
var truncatedID = regexp.MustCompile(`^(sha256:)?([a-f0-9]{4,64})$`)
var errInconsistentData error = errors.New("consistency error: data changed during operation, retry")
// GetImage returns an image corresponding to the image referred to by refOrID.
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*image.Image, error) {
desc, err := i.resolveImage(ctx, refOrID)
@@ -41,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)
@@ -57,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(),
@@ -99,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
}
@@ -264,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")
}
@@ -298,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 {
@@ -315,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
}
@@ -382,3 +380,154 @@ func (i *ImageService) getImageLabelByDigest(ctx context.Context, target digest.
return value, nil
}
func convertError(err error) error {
// TODO: Convert containerd error to Docker error
return err
}
// resolveAllReferences resolves the reference name or ID to an image and returns all the images with
// the same target.
//
// Returns:
//
// 1: *(github.com/containerd/containerd/images).Image
//
// An image match from the image store with the provided refOrID
//
// 2: [](github.com/containerd/containerd/images).Image
//
// List of all images with the same target that matches the refOrID. If the first argument is
// non-nil, the image list will all have the same target as the matched image. If the first
// argument is nil but the list is non-empty, this value is a list of all the images with a
// target that matches the digest provided in the refOrID, but none are an image name match
// to refOrID.
//
// 3: error
//
// An error looking up refOrID or no images found with matching name or target. Note that the first
// argument may be nil with a nil error if the second argument is non-empty.
func (i *ImageService) resolveAllReferences(ctx context.Context, refOrID string) (*containerdimages.Image, []containerdimages.Image, error) {
parsed, err := reference.ParseAnyReference(refOrID)
if err != nil {
return nil, nil, errdefs.InvalidParameter(err)
}
var dgst digest.Digest
var img *containerdimages.Image
if truncatedID.MatchString(refOrID) {
if d, ok := parsed.(reference.Digested); ok {
if cimg, err := i.images.Get(ctx, d.String()); err == nil {
img = &cimg
dgst = d.Digest()
if cimg.Target.Digest != dgst {
// Ambiguous image reference, use reference name
log.G(ctx).WithField("image", refOrID).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest")
dgst = cimg.Target.Digest
}
} else if !cerrdefs.IsNotFound(err) {
return nil, nil, convertError(err)
} else {
dgst = d.Digest()
}
} else {
idWithoutAlgo := strings.TrimPrefix(refOrID, "sha256:")
name := reference.TagNameOnly(parsed.(reference.Named)).String()
filters := []string{
fmt.Sprintf("name==%q", name), // 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 := i.images.List(ctx, filters...)
if err != nil {
return nil, nil, convertError(err)
}
if len(imgs) == 0 {
return nil, nil, images.ErrImageDoesNotExist{Ref: parsed}
}
for _, limg := range imgs {
if limg.Name == name {
copyImg := limg
img = &copyImg
}
if dgst != "" {
if limg.Target.Digest != dgst {
return nil, nil, errdefs.NotFound(errors.New("ambiguous reference"))
}
} else {
dgst = limg.Target.Digest
}
}
// Return immediately if target digest matches already included
if img == nil || len(imgs) > 1 {
return img, imgs, nil
}
}
} else {
named, ok := parsed.(reference.Named)
if !ok {
return nil, nil, errdefs.InvalidParameter(errors.New("invalid name reference"))
}
digested, ok := parsed.(reference.Digested)
if ok {
dgst = digested.Digest()
}
name := reference.TagNameOnly(named).String()
cimg, err := i.images.Get(ctx, name)
if err != nil {
if !cerrdefs.IsNotFound(err) {
return nil, nil, convertError(err)
}
// If digest is given, continue looking up for matching targets.
// There will be no exact match found but the caller may attempt
// to match across images with the matching target.
if dgst == "" {
return nil, nil, images.ErrImageDoesNotExist{Ref: parsed}
}
} else {
img = &cimg
if dgst != "" && img.Target.Digest != dgst {
// Ambiguous image reference, use reference name
log.G(ctx).WithField("image", name).WithField("target", cimg.Target.Digest).Warn("digest reference points to image with a different digest")
}
dgst = img.Target.Digest
}
}
// Lookup up all associated images and check for consistency with first reference
// Ideally operations dependent on multiple values will rely on the garbage collector,
// this logic will just check for consistency and throw an error
imgs, err := i.images.List(ctx, "target.digest=="+dgst.String())
if err != nil {
return nil, nil, errors.Wrap(err, "failed to lookup digest")
}
if len(imgs) == 0 {
if img == nil {
return nil, nil, images.ErrImageDoesNotExist{Ref: parsed}
}
err = errInconsistentData
} else if img != nil {
// Check to ensure the original img is in the list still
err = errInconsistentData
for _, rimg := range imgs {
if rimg.Name == img.Name {
err = nil
break
}
}
}
if errors.Is(err, errInconsistentData) {
if retries, ok := ctx.Value(errInconsistentData).(int); !ok || retries < 3 {
log.G(ctx).WithFields(log.Fields{"retry": retries, "ref": refOrID}).Info("image changed during lookup, retrying")
return i.resolveAllReferences(context.WithValue(ctx, errInconsistentData, retries+1), refOrID)
}
return nil, nil, err
}
return img, imgs, nil
}

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

@@ -5,13 +5,16 @@ import (
"fmt"
"sort"
"strings"
"time"
cerrdefs "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/log"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/events"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/container"
dimages "github.com/docker/docker/daemon/images"
"github.com/docker/docker/image"
"github.com/docker/docker/internal/compatcontext"
"github.com/docker/docker/pkg/stringid"
@@ -53,123 +56,188 @@ 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)
if err != nil {
return nil, err
var imgID image.ID
if img == nil {
if len(all) == 0 {
parsed, _ := reference.ParseAnyReference(imageRef)
return nil, dimages.ErrImageDoesNotExist{Ref: parsed}
}
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())),
imgID = image.ID(all[0].Target.Digest)
var named reference.Named
if !isImageIDPrefix(imgID.String(), imageRef) {
if nn, err := reference.ParseNormalizedNamed(imageRef); err == nil {
named = nn
}
return nil, err
}
err := i.softImageDelete(ctx, img)
sameRef, err := i.getSameReferences(ctx, named, 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
if len(sameRef) == 0 && named != nil {
return nil, dimages.ErrImageDoesNotExist{Ref: named}
}
if len(sameRef) == len(all) && !force {
c &= ^conflictActiveReference
}
if named != nil && len(sameRef) > 0 && 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 records, nil
}
} 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, parsedRef, 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 records, nil
} else if len(all) > 1 && !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
}
using := func(c *container.Container) bool {
return c.ImageID == imgID
}
// TODO: Should this also check parentage here?
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
}
// 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 +273,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, named reference.Named, imgs []images.Image) ([]images.Image, error) {
var (
tag string
sameRef []images.Image
digestRefs = []images.Image{}
allTags bool
)
if named != nil {
if tagged, ok := named.(reference.Tagged); ok {
tag = tagged.Tag()
} else if _, ok := named.(reference.Digested); ok {
// If digest is explicitly provided, match all tags
allTags = true
}
}
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 !allTags {
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 +358,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 +374,33 @@ 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())
if !isDanglingImage(img) && len(all) == 1 && extra&conflictActiveReference != 0 {
children, err := i.Children(ctx, imgID)
if err != nil {
return err
}
if len(children) > 0 {
img := images.Image{
Name: danglingImageName(img.Target.Digest),
Target: img.Target,
CreatedAt: time.Now(),
Labels: img.Labels,
}
if _, err = i.client.ImageService().Create(ctx, img); err != nil && !cerrdefs.IsAlreadyExists(err) {
return fmt.Errorf("failed to create dangling image: %w", err)
}
}
}
// 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 +436,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 +465,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,286 @@
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),
},
},
},
{
ref: "repoanddigest@" + digestFor(15).String(),
starting: []images.Image{
{
Name: "docker.io/library/repoanddigest:latest",
Target: desc(15),
},
{
Name: "docker.io/library/repoanddigest:latest@" + digestFor(15).String(),
Target: desc(15),
},
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(15),
},
},
remaining: []images.Image{
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(15),
},
},
},
{
ref: "repoanddigestothertags@" + digestFor(15).String(),
starting: []images.Image{
{
Name: "docker.io/library/repoanddigestothertags:v1",
Target: desc(15),
},
{
Name: "docker.io/library/repoanddigestothertags:v1@" + digestFor(15).String(),
Target: desc(15),
},
{
Name: "docker.io/library/repoanddigestothertags:v2",
Target: desc(15),
},
{
Name: "docker.io/library/repoanddigestothertags:v2@" + digestFor(15).String(),
Target: desc(15),
},
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(15),
},
},
remaining: []images.Image{
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(15),
},
},
},
{
ref: "repoanddigestzerocase@" + digestFor(16).String(),
starting: []images.Image{
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(16),
},
},
remaining: []images.Image{
{
Name: "docker.io/library/someotherrepo:latest",
Target: desc(16),
},
},
err: dimages.ErrImageDoesNotExist{Ref: nameDigest("repoanddigestzerocase", digestFor(16))},
},
} {
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)
assert.NilError(t, err)
assert.Assert(t, is.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

@@ -0,0 +1,298 @@
package containerd
import (
"context"
"io"
"math/rand"
"path/filepath"
"testing"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/log/logtest"
"github.com/distribution/reference"
dockerimages "github.com/docker/docker/daemon/images"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"go.etcd.io/bbolt"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestLookup(t *testing.T) {
ctx := namespaces.WithNamespace(context.TODO(), "testing")
ctx = logtest.WithT(ctx, t)
mdb := newTestDB(ctx, t)
service := &ImageService{
images: metadata.NewImageStore(mdb),
}
ubuntuLatest := images.Image{
Name: "docker.io/library/ubuntu:latest",
Target: desc(10),
}
ubuntuLatestWithDigest := images.Image{
Name: "docker.io/library/ubuntu:latest@" + digestFor(10).String(),
Target: desc(10),
}
ubuntuLatestWithOldDigest := images.Image{
Name: "docker.io/library/ubuntu:latest@" + digestFor(11).String(),
Target: desc(11),
}
ambiguousShortName := images.Image{
Name: "docker.io/library/abcdef:latest",
Target: desc(12),
}
ambiguousShortNameWithDigest := images.Image{
Name: "docker.io/library/abcdef:latest@" + digestFor(12).String(),
Target: desc(12),
}
shortNameIsHashAlgorithm := images.Image{
Name: "docker.io/library/sha256:defcab",
Target: desc(13),
}
testImages := []images.Image{
ubuntuLatest,
ubuntuLatestWithDigest,
ubuntuLatestWithOldDigest,
ambiguousShortName,
ambiguousShortNameWithDigest,
shortNameIsHashAlgorithm,
{
Name: "docker.io/test/volatile:retried",
Target: desc(14),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(15),
},
}
for _, img := range testImages {
if _, err := service.images.Create(ctx, img); err != nil {
t.Fatalf("failed to create image %q: %v", img.Name, err)
}
}
for _, tc := range []struct {
lookup string
img *images.Image
all []images.Image
err error
}{
{
// Get ubuntu images with default "latest" tag
lookup: "ubuntu",
img: &ubuntuLatest,
all: []images.Image{ubuntuLatest, ubuntuLatestWithDigest},
},
{
// Get all images by image id
lookup: ubuntuLatest.Target.Digest.String(),
img: nil,
all: []images.Image{ubuntuLatest, ubuntuLatestWithDigest},
},
{
// Fail to lookup reference with no tag, reference has both tag and digest
lookup: "ubuntu@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
img: nil,
all: []images.Image{ubuntuLatestWithOldDigest},
},
{
// Get all image with both tag and digest
lookup: "ubuntu:latest@" + ubuntuLatestWithOldDigest.Target.Digest.String(),
img: &ubuntuLatestWithOldDigest,
all: []images.Image{ubuntuLatestWithOldDigest},
},
{
// Fail to lookup reference with no tag for digest that doesn't exist
lookup: "ubuntu@" + digestFor(20).String(),
err: dockerimages.ErrImageDoesNotExist{Ref: nameDigest("ubuntu", digestFor(20))},
},
{
// Fail to lookup reference with nonexistent tag
lookup: "ubuntu:nonexistent",
err: dockerimages.ErrImageDoesNotExist{Ref: nameTag("ubuntu", "nonexistent")},
},
{
// Get abcdef image which also matches short image id
lookup: "abcdef",
img: &ambiguousShortName,
all: []images.Image{ambiguousShortName, ambiguousShortNameWithDigest},
},
{
// Fail to lookup image named "sha256" with tag that doesn't exist
lookup: "sha256:abcdef",
err: dockerimages.ErrImageDoesNotExist{Ref: nameTag("sha256", "abcdef")},
},
{
// Lookup with shortened image id
lookup: ambiguousShortName.Target.Digest.Encoded()[:8],
img: nil,
all: []images.Image{ambiguousShortName, ambiguousShortNameWithDigest},
},
{
// Lookup an actual image named "sha256" in the default namespace
lookup: "sha256:defcab",
img: &shortNameIsHashAlgorithm,
all: []images.Image{shortNameIsHashAlgorithm},
},
} {
tc := tc
t.Run(tc.lookup, func(t *testing.T) {
t.Parallel()
img, all, err := service.resolveAllReferences(ctx, tc.lookup)
if tc.err == nil {
assert.NilError(t, err)
} else {
assert.Error(t, err, tc.err.Error())
}
if tc.img == nil {
assert.Assert(t, is.Nil(img))
} else {
assert.Assert(t, img != nil)
assert.Check(t, is.Equal(img.Name, tc.img.Name))
assert.Check(t, is.Equal(img.Target.Digest, tc.img.Target.Digest))
}
assert.Assert(t, is.Len(tc.all, len(all)))
// Order should match
for i := range all {
assert.Check(t, is.Equal(all[i].Name, tc.all[i].Name), "image[%d]", i)
assert.Check(t, is.Equal(all[i].Target.Digest, tc.all[i].Target.Digest), "image[%d]", i)
}
})
}
t.Run("fail-inconsistency", func(t *testing.T) {
service := &ImageService{
images: &mutateOnGetImageStore{
Store: service.images,
getMutations: []images.Image{
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(18),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(19),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(20),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(21),
},
{
Name: "docker.io/test/volatile:inconsistent",
Target: desc(22),
},
},
t: t,
},
}
_, _, err := service.resolveAllReferences(ctx, "test/volatile:inconsistent")
assert.ErrorIs(t, err, errInconsistentData)
})
t.Run("retry-inconsistency", func(t *testing.T) {
service := &ImageService{
images: &mutateOnGetImageStore{
Store: service.images,
getMutations: []images.Image{
{
Name: "docker.io/test/volatile:retried",
Target: desc(16),
},
{
Name: "docker.io/test/volatile:retried",
Target: desc(17),
},
},
t: t,
},
}
img, all, err := service.resolveAllReferences(ctx, "test/volatile:retried")
assert.NilError(t, err)
assert.Assert(t, img != nil)
assert.Check(t, is.Equal(img.Name, "docker.io/test/volatile:retried"))
assert.Check(t, is.Equal(img.Target.Digest, digestFor(17)))
assert.Assert(t, is.Len(all, 1))
assert.Check(t, is.Equal(all[0].Name, "docker.io/test/volatile:retried"))
assert.Check(t, is.Equal(all[0].Target.Digest, digestFor(17)))
})
}
type mutateOnGetImageStore struct {
images.Store
getMutations []images.Image
t *testing.T
}
func (m *mutateOnGetImageStore) Get(ctx context.Context, name string) (images.Image, error) {
img, err := m.Store.Get(ctx, name)
if len(m.getMutations) > 0 {
m.Store.Update(ctx, m.getMutations[0])
m.getMutations = m.getMutations[1:]
m.t.Logf("Get %s", name)
}
return img, err
}
func nameDigest(name string, dgst digest.Digest) reference.Reference {
named, _ := reference.WithName(name)
digested, _ := reference.WithDigest(named, dgst)
return digested
}
func nameTag(name, tag string) reference.Reference {
named, _ := reference.WithName(name)
tagged, _ := reference.WithTag(named, tag)
return tagged
}
func desc(size int64) ocispec.Descriptor {
return ocispec.Descriptor{
Digest: digestFor(size),
Size: size,
MediaType: ocispec.MediaTypeImageIndex,
}
}
func digestFor(i int64) digest.Digest {
r := rand.New(rand.NewSource(i))
dgstr := digest.SHA256.Digester()
_, err := io.Copy(dgstr.Hash(), io.LimitReader(r, i))
if err != nil {
panic(err)
}
return dgstr.Digest()
}
func newTestDB(ctx context.Context, t *testing.T) *metadata.DB {
t.Helper()
p := filepath.Join(t.TempDir(), "metadata")
bdb, err := bbolt.Open(p, 0600, &bbolt.Options{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { bdb.Close() })
mdb := metadata.NewDB(bdb, nil, nil)
if err := mdb.Init(ctx); err != nil {
t.Fatal(err)
}
return mdb
}

View File

@@ -6,7 +6,9 @@ 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"
"github.com/containerd/containerd/remotes/docker"
"github.com/containerd/containerd/snapshots"
@@ -14,7 +16,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/docker/container"
daemonevents "github.com/docker/docker/daemon/events"
"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"
@@ -27,6 +29,8 @@ import (
// ImageService implements daemon.ImageService
type ImageService struct {
client *containerd.Client
images images.Store
content content.Store
containers container.Store
snapshotter string
registryHosts docker.RegistryHosts
@@ -59,6 +63,8 @@ type ImageServiceConfig struct {
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,
@@ -70,8 +76,8 @@ func NewService(config ImageServiceConfig) *ImageService {
}
// DistributionServices return services controlling daemon image storage.
func (i *ImageService) DistributionServices() images.DistributionServices {
return images.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)
}

View File

@@ -295,7 +295,7 @@ RUN echo 2 #layer2
// should not be untagged without the -f flag
assert.ErrorContains(c, err, "")
assert.Assert(c, strings.Contains(out, cID[:12]))
assert.Assert(c, strings.Contains(out, "(must force)"))
assert.Assert(c, strings.Contains(out, "(must force)") || strings.Contains(out, "(must be forced)"))
// Add the -f flag and test again.
out = cli.DockerCmd(c, "rmi", "-f", newTag).Combined()
// should be allowed to untag with the -f flag

56
vendor/github.com/containerd/log/logtest/context.go generated vendored Normal file
View File

@@ -0,0 +1,56 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logtest
import (
"context"
"fmt"
"io"
"path/filepath"
"runtime"
"testing"
"github.com/containerd/log"
"github.com/sirupsen/logrus"
)
// WithT adds a logging hook for the given test
// Changes debug level to debug, clears output, and
// outputs all log messages as test logs.
func WithT(ctx context.Context, t testing.TB) context.Context {
// Create a new logger to avoid adding hooks from multiple tests
l := logrus.New()
// Increase debug level for tests
l.SetLevel(logrus.DebugLevel)
l.SetOutput(io.Discard)
l.SetReportCaller(true)
// Add testing hook
l.AddHook(&testHook{
t: t,
fmt: &logrus.TextFormatter{
DisableColors: true,
TimestampFormat: log.RFC3339NanoFixed,
CallerPrettyfier: func(frame *runtime.Frame) (string, string) {
return filepath.Base(frame.Function), fmt.Sprintf("%s:%d", frame.File, frame.Line)
},
},
})
return log.WithLogger(ctx, logrus.NewEntry(l).WithField("testcase", t.Name()))
}

50
vendor/github.com/containerd/log/logtest/log_hook.go generated vendored Normal file
View File

@@ -0,0 +1,50 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logtest
import (
"bytes"
"sync"
"testing"
"github.com/sirupsen/logrus"
)
type testHook struct {
t testing.TB
fmt logrus.Formatter
mu sync.Mutex
}
func (*testHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *testHook) Fire(e *logrus.Entry) error {
s, err := h.fmt.Format(e)
if err != nil {
return err
}
// Because the logger could be called from multiple goroutines,
// but t.Log() is not designed for.
h.mu.Lock()
defer h.mu.Unlock()
h.t.Log(string(bytes.TrimRight(s, "\n")))
return nil
}

1
vendor/modules.txt vendored
View File

@@ -359,6 +359,7 @@ github.com/containerd/go-runc
# github.com/containerd/log v0.1.0
## explicit; go 1.20
github.com/containerd/log
github.com/containerd/log/logtest
# github.com/containerd/nydus-snapshotter v0.8.2
## explicit; go 1.19
github.com/containerd/nydus-snapshotter/pkg/converter