Merge pull request #45839 from vvoland/c8d-dont-prune-used
c8d/prune: Fix images being deleted when they're still used with a different reference
This commit is contained in:
commit
a61494e634
5 changed files with 120 additions and 35 deletions
|
@ -2,6 +2,7 @@ package containerd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/containerd/errdefs"
|
cerrdefs "github.com/containerd/containerd/errdefs"
|
||||||
containerdimages "github.com/containerd/containerd/images"
|
containerdimages "github.com/containerd/containerd/images"
|
||||||
|
@ -70,35 +71,50 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How many images make reference to a particular target digest.
|
||||||
|
digestRefCount := map[digest.Digest]int{}
|
||||||
|
// Images considered for pruning.
|
||||||
imagesToPrune := map[string]containerdimages.Image{}
|
imagesToPrune := map[string]containerdimages.Image{}
|
||||||
for _, img := range allImages {
|
for _, img := range allImages {
|
||||||
|
digestRefCount[img.Target.Digest] += 1
|
||||||
|
|
||||||
if !danglingOnly || isDanglingImage(img) {
|
if !danglingOnly || isDanglingImage(img) {
|
||||||
|
canBePruned := filterFunc(img)
|
||||||
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
|
"image": img.Name,
|
||||||
|
"canBePruned": canBePruned,
|
||||||
|
}).Debug("considering image for pruning")
|
||||||
|
|
||||||
|
if canBePruned {
|
||||||
imagesToPrune[img.Name] = img
|
imagesToPrune[img.Name] = img
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Apply filters
|
|
||||||
for name, img := range imagesToPrune {
|
|
||||||
filteredOut := !filterFunc(img)
|
|
||||||
log.G(ctx).WithFields(logrus.Fields{
|
|
||||||
"image": name,
|
|
||||||
"filteredOut": filteredOut,
|
|
||||||
}).Debug("filtering image")
|
|
||||||
|
|
||||||
if filteredOut {
|
|
||||||
delete(imagesToPrune, name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
containers := i.containers.List()
|
// Image specified by digests that are used by containers.
|
||||||
|
usedDigests := map[digest.Digest]struct{}{}
|
||||||
|
|
||||||
var errs error
|
|
||||||
// Exclude images used by existing containers
|
// Exclude images used by existing containers
|
||||||
for _, ctr := range containers {
|
for _, ctr := range i.containers.List() {
|
||||||
|
// If the original image was deleted, make sure we don't delete the dangling image
|
||||||
|
delete(imagesToPrune, danglingImageName(ctr.ImageID.Digest()))
|
||||||
|
|
||||||
// Config.Image is the image reference passed by user.
|
// Config.Image is the image reference passed by user.
|
||||||
// For example: container created by `docker run alpine` will have Image="alpine"
|
// Config.ImageID is the resolved content digest based on the user's Config.Image.
|
||||||
// Warning: This doesn't handle truncated ids:
|
// For example: container created by:
|
||||||
// `docker run 124c7d2` will have Image="124c7d270790"
|
// `docker run alpine` will have Config.Image="alpine"
|
||||||
|
// `docker run 82d1e9d` will have Config.Image="82d1e9d"
|
||||||
|
// but both will have ImageID="sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1"
|
||||||
|
imageDgst := ctr.ImageID.Digest()
|
||||||
|
|
||||||
|
// If user didn't specify an explicit image, mark the digest as used.
|
||||||
|
normalizedImageID := "sha256:" + strings.TrimPrefix(ctr.Config.Image, "sha256:")
|
||||||
|
if strings.HasPrefix(imageDgst.String(), normalizedImageID) {
|
||||||
|
usedDigests[imageDgst] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
ref, err := reference.ParseNormalizedNamed(ctr.Config.Image)
|
ref, err := reference.ParseNormalizedNamed(ctr.Config.Image)
|
||||||
log.G(ctx).WithFields(logrus.Fields{
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
"ctr": ctr.ID,
|
"ctr": ctr.ID,
|
||||||
|
@ -107,12 +123,28 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
|
||||||
}).Debug("filtering container's image")
|
}).Debug("filtering container's image")
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// If user provided a specific image name, exclude that image.
|
||||||
name := reference.TagNameOnly(ref)
|
name := reference.TagNameOnly(ref)
|
||||||
delete(imagesToPrune, name.String())
|
delete(imagesToPrune, name.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create dangling images for images that will be deleted but are still in use.
|
||||||
|
for _, img := range imagesToPrune {
|
||||||
|
dgst := img.Target.Digest
|
||||||
|
|
||||||
|
digestRefCount[dgst] -= 1
|
||||||
|
if digestRefCount[dgst] == 0 {
|
||||||
|
if _, isUsed := usedDigests[dgst]; isUsed {
|
||||||
|
if err := i.ensureDanglingImage(ctx, img); err != nil {
|
||||||
|
return &report, errors.Wrapf(err, "failed to create ensure dangling image for %s", img.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
possiblyDeletedConfigs := map[digest.Digest]struct{}{}
|
possiblyDeletedConfigs := map[digest.Digest]struct{}{}
|
||||||
|
var errs error
|
||||||
|
|
||||||
// Workaround for https://github.com/moby/buildkit/issues/3797
|
// Workaround for https://github.com/moby/buildkit/issues/3797
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -30,19 +30,12 @@ func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages
|
||||||
|
|
||||||
// Create dangling image if this is the last image pointing to this target.
|
// Create dangling image if this is the last image pointing to this target.
|
||||||
if len(imgs) == 1 {
|
if len(imgs) == 1 {
|
||||||
danglingImage := img
|
err = i.ensureDanglingImage(context.Background(), img)
|
||||||
|
|
||||||
danglingImage.Name = danglingImageName(img.Target.Digest)
|
|
||||||
delete(danglingImage.Labels, containerdimages.AnnotationImageName)
|
|
||||||
delete(danglingImage.Labels, ocispec.AnnotationRefName)
|
|
||||||
|
|
||||||
_, err = is.Create(context.Background(), danglingImage)
|
|
||||||
|
|
||||||
// Error out in case we couldn't persist the old image.
|
// Error out in case we couldn't persist the old image.
|
||||||
// If it already exists, then just continue.
|
if err != nil {
|
||||||
if err != nil && !cerrdefs.IsAlreadyExists(err) {
|
|
||||||
return errdefs.System(errors.Wrapf(err, "failed to create a dangling image for the replaced image %s with digest %s",
|
return errdefs.System(errors.Wrapf(err, "failed to create a dangling image for the replaced image %s with digest %s",
|
||||||
danglingImage.Name, danglingImage.Target.Digest.String()))
|
img.Name, img.Target.Digest.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +50,29 @@ func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *ImageService) ensureDanglingImage(ctx context.Context, from containerdimages.Image) error {
|
||||||
|
danglingImage := from
|
||||||
|
|
||||||
|
danglingImage.Labels = make(map[string]string)
|
||||||
|
for k, v := range from.Labels {
|
||||||
|
switch k {
|
||||||
|
case containerdimages.AnnotationImageName, ocispec.AnnotationRefName:
|
||||||
|
// Don't copy name labels.
|
||||||
|
default:
|
||||||
|
danglingImage.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
danglingImage.Name = danglingImageName(from.Target.Digest)
|
||||||
|
|
||||||
|
_, err := i.client.ImageService().Create(context.Background(), danglingImage)
|
||||||
|
// If it already exists, then just continue.
|
||||||
|
if cerrdefs.IsAlreadyExists(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func danglingImageName(digest digest.Digest) string {
|
func danglingImageName(digest digest.Digest) string {
|
||||||
return "moby-dangling@" + digest.String()
|
return "moby-dangling@" + digest.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,9 @@ func TestImageInspectEmptyTagsAndDigests(t *testing.T) {
|
||||||
client := testEnv.APIClient()
|
client := testEnv.APIClient()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
danglingId := environment.DanglingImageIdGraphDriver
|
danglingID := environment.GetTestDanglingImageId(testEnv)
|
||||||
if testEnv.UsingSnapshotter() {
|
|
||||||
danglingId = environment.DanglingImageIdSnapshotter
|
|
||||||
}
|
|
||||||
|
|
||||||
inspect, raw, err := client.ImageInspectWithRaw(ctx, danglingId)
|
inspect, raw, err := client.ImageInspectWithRaw(ctx, danglingID)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
// Must be a zero length array, not null.
|
// Must be a zero length array, not null.
|
||||||
|
|
33
integration/image/prune_test.go
Normal file
33
integration/image/prune_test.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/integration/internal/container"
|
||||||
|
"github.com/docker/docker/testutil/environment"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
"gotest.tools/v3/skip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regression test for: https://github.com/moby/moby/issues/45732
|
||||||
|
func TestPruneDontDeleteUsedDangling(t *testing.T) {
|
||||||
|
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME: hack/make/.build-empty-images doesn't run on Windows")
|
||||||
|
|
||||||
|
defer setupTest(t)()
|
||||||
|
client := testEnv.APIClient()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
danglingID := environment.GetTestDanglingImageId(testEnv)
|
||||||
|
|
||||||
|
container.Create(ctx, t, client,
|
||||||
|
container.WithImage(danglingID),
|
||||||
|
container.WithCmd("sleep", "60"))
|
||||||
|
|
||||||
|
pruned, err := client.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "true")))
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Check(t, is.Len(pruned.ImagesDeleted, 0))
|
||||||
|
}
|
|
@ -5,3 +5,10 @@ const DanglingImageIdGraphDriver = "sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70
|
||||||
|
|
||||||
// The containerd image store identifies images by the ID of their manifest/manifest list.
|
// The containerd image store identifies images by the ID of their manifest/manifest list.
|
||||||
const DanglingImageIdSnapshotter = "sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456"
|
const DanglingImageIdSnapshotter = "sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456"
|
||||||
|
|
||||||
|
func GetTestDanglingImageId(testEnv *Execution) string {
|
||||||
|
if testEnv.UsingSnapshotter() {
|
||||||
|
return DanglingImageIdSnapshotter
|
||||||
|
}
|
||||||
|
return DanglingImageIdGraphDriver
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue