mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Merge pull request #46840 from dmcgowan/c8d-rmi-cleanup
containerd: Image delete fixes and cleanup
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 = ©Img
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
286
daemon/containerd/image_delete_test.go
Normal file
286
daemon/containerd/image_delete_test.go
Normal 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) {}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
298
daemon/containerd/image_test.go
Normal file
298
daemon/containerd/image_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
56
vendor/github.com/containerd/log/logtest/context.go
generated
vendored
Normal 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
50
vendor/github.com/containerd/log/logtest/log_hook.go
generated
vendored
Normal 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
1
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user