From dda8680ba2723a0346f861a2161eaa6c8476881a Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Tue, 8 Oct 2024 23:30:01 +0900 Subject: [PATCH] fix: Allow to delete images when names of images are short digest ids of another images. "nerdctl rmi " can be run to delete the target images. However, at the current implementation, the deletion fails when images names are the short digest ids of another images. The specific behavior is described below, which is how it works in the current implementation. First, suppose there are alpine and busybox images. ``` > nerdctl images REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE busybox latest 768e5c6f5cb6 3 seconds ago linux/arm64 4.092MB 1.845MB alpine latest beefdbd8a1da 11 seconds ago linux/arm64 10.46MB 4.09MB ``` Then, we tag the alpine image using digest id of the busybox image. ``` > nerdctl tag alpine $(dn inspect busybox | jq -rc .[0].RepoDigests[0] | awk -F':' '{print substr($2, 1, 8)}') > nerdctl images REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE 768e5c6f latest beefdbd8a1da 4 seconds ago linux/arm64 10.46MB 4.09MB busybox latest 768e5c6f5cb6 22 hours ago linux/arm64 4.092MB 1.845MB alpine latest beefdbd8a1da 22 hours ago linux/arm64 10.46MB 4.09MB ``` In this situation, running 'nerdctl rmi "$(dn inspect busybox | jq -rc .[0].RepoDigests[0] | awk -F':' '{print substr($2, 1, 8)}')"' will fail to remove the image. The details of the error are as follows. ``` > nerdctl rmi "$(dn inspect busybox | jq -rc .[0].RepoDigests[0] | awk -F':' '{print substr($2, 1, 8)}')" FATA[0000] 1 errors: multiple IDs found with provided prefix: 768e5c6f ``` This issue is reported in the following issue. - https://github.com/containerd/nerdctl/issues/3016 Therefore, this pull request modifies this so that images can be deleted with "nerdctl rmi " when images names are the short digest ids of another images. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/image/image_remove_linux_test.go | 57 ++++++++++++++++++++ pkg/idutil/imagewalker/imagewalker.go | 14 ++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_remove_linux_test.go b/cmd/nerdctl/image/image_remove_linux_test.go index 5752aa04aa4..4b0de895e69 100644 --- a/cmd/nerdctl/image/image_remove_linux_test.go +++ b/cmd/nerdctl/image/image_remove_linux_test.go @@ -17,9 +17,14 @@ package image import ( + "strings" "testing" + "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestRemoveImage(t *testing.T) { @@ -105,3 +110,55 @@ func TestRemoveImageWithCreatedContainer(t *testing.T) { base.Cmd("rmi", "-f", testutil.NginxAlpineImage).AssertOK() base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.NginxAlpineImage)) } + +// TestIssue3016 tests https://github.com/containerd/nerdctl/issues/3016 +func TestIssue3016(t *testing.T) { + nerdtest.Setup() + + const ( + alpineImageName = "alpine" + busyboxImageName = "busybox" + tagIDKey = "tagID" + ) + + testCase := &test.Group{ + { + Description: "Issue #3016 - Tags created using the short digest ids of container images cannot be deleted using the nerdctl rmi command.", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", alpineImageName) + helpers.Ensure("pull", busyboxImageName) + + img := nerdtest.InspectImage(helpers, busyboxImageName) + tagID := strings.TrimPrefix(img.RepoDigests[0], "busybox@sha256:")[0:8] + assert.Equal(t, len(tagID), 8) + + helpers.Ensure("tag", alpineImageName, tagID) + + data.Set(tagIDKey, tagID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", alpineImageName) + helpers.Anyhow("rmi", busyboxImageName) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("rmi", data.Get(tagIDKey)) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: []error{}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images", data.Get(tagIDKey)).Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + assert.Equal(t, len(strings.Split(stdout, "\n")), 2) + }, + }) + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/pkg/idutil/imagewalker/imagewalker.go b/pkg/idutil/imagewalker/imagewalker.go index f6effd0fa0a..77af5ac7d80 100644 --- a/pkg/idutil/imagewalker/imagewalker.go +++ b/pkg/idutil/imagewalker/imagewalker.go @@ -64,7 +64,6 @@ func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) { return -1, err } - matchCount := len(images) // to handle the `rmi -f` case where returned images are different but // have the same short prefix. uniqueImages := make(map[digest.Digest]bool) @@ -72,6 +71,19 @@ func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) { uniqueImages[image.Target.Digest] = true } + // Allow to nerdctl rmi to remove images. + if len(uniqueImages) > 1 { + imageIDPrefix := fmt.Sprintf("sha256:%s", regexp.QuoteMeta(req)) + for i := len(images) - 1; i >= 0; i-- { + if strings.HasPrefix(images[i].Target.Digest.String(), imageIDPrefix) { + delete(uniqueImages, images[i].Target.Digest) + images = append(images[:i], images[i+1:]...) + } + } + } + + matchCount := len(images) + for i, img := range images { f := Found{ Image: img,