Jelajahi Sumber

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
Sebastiaan van Stijn 2 tahun lalu
induk
melakukan
a61494e634

+ 51 - 19
daemon/containerd/image_prune.go

@@ -2,6 +2,7 @@ package containerd
 
 import (
 	"context"
+	"strings"
 
 	cerrdefs "github.com/containerd/containerd/errdefs"
 	containerdimages "github.com/containerd/containerd/images"
@@ -70,35 +71,50 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
 		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{}
 	for _, img := range allImages {
-		if !danglingOnly || isDanglingImage(img) {
-			imagesToPrune[img.Name] = img
-		}
-	}
+		digestRefCount[img.Target.Digest] += 1
 
-	// Apply filters
-	for name, img := range imagesToPrune {
-		filteredOut := !filterFunc(img)
-		log.G(ctx).WithFields(logrus.Fields{
-			"image":       name,
-			"filteredOut": filteredOut,
-		}).Debug("filtering image")
+		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
+			}
 
-		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
-	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.
-		// For example: container created by `docker run alpine` will have Image="alpine"
-		// Warning: This doesn't handle truncated ids:
-		//          `docker run 124c7d2` will have Image="124c7d270790"
+		// Config.ImageID is the resolved content digest based on the user's Config.Image.
+		// For example: container created by:
+		//           `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)
 		log.G(ctx).WithFields(logrus.Fields{
 			"ctr":          ctr.ID,
@@ -107,12 +123,28 @@ func (i *ImageService) pruneUnused(ctx context.Context, filterFunc imageFilterFu
 		}).Debug("filtering container's image")
 
 		if err == nil {
+			// If user provided a specific image name, exclude that image.
 			name := reference.TagNameOnly(ref)
 			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{}{}
+	var errs error
 
 	// Workaround for https://github.com/moby/buildkit/issues/3797
 	defer func() {

+ 26 - 10
daemon/containerd/soft_delete.go

@@ -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.
 	if len(imgs) == 1 {
-		danglingImage := img
-
-		danglingImage.Name = danglingImageName(img.Target.Digest)
-		delete(danglingImage.Labels, containerdimages.AnnotationImageName)
-		delete(danglingImage.Labels, ocispec.AnnotationRefName)
-
-		_, err = is.Create(context.Background(), danglingImage)
+		err = i.ensureDanglingImage(context.Background(), img)
 
 		// Error out in case we couldn't persist the old image.
-		// If it already exists, then just continue.
-		if err != nil && !cerrdefs.IsAlreadyExists(err) {
+		if err != nil {
 			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
 }
 
+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 {
 	return "moby-dangling@" + digest.String()
 }

+ 2 - 5
integration/image/inspect_test.go

@@ -19,12 +19,9 @@ func TestImageInspectEmptyTagsAndDigests(t *testing.T) {
 	client := testEnv.APIClient()
 	ctx := context.Background()
 
-	danglingId := environment.DanglingImageIdGraphDriver
-	if testEnv.UsingSnapshotter() {
-		danglingId = environment.DanglingImageIdSnapshotter
-	}
+	danglingID := environment.GetTestDanglingImageId(testEnv)
 
-	inspect, raw, err := client.ImageInspectWithRaw(ctx, danglingId)
+	inspect, raw, err := client.ImageInspectWithRaw(ctx, danglingID)
 	assert.NilError(t, err)
 
 	// Must be a zero length array, not null.

+ 33 - 0
integration/image/prune_test.go

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

+ 7 - 0
testutil/environment/special_images.go

@@ -5,3 +5,10 @@ const DanglingImageIdGraphDriver = "sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70
 
 // The containerd image store identifies images by the ID of their manifest/manifest list.
 const DanglingImageIdSnapshotter = "sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456"
+
+func GetTestDanglingImageId(testEnv *Execution) string {
+	if testEnv.UsingSnapshotter() {
+		return DanglingImageIdSnapshotter
+	}
+	return DanglingImageIdGraphDriver
+}