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,