Skip to content

Commit c3627e1

Browse files
committed
fix: Allow to delete images when names of images are short digest ids of another images.
"nerdctl rmi <REPOSITORY>" 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. - #3016 Therefore, this pull request modifies this so that images can be deleted with "nerdctl rmi <short digest id of another image>" when images names are the short digest ids of another images. Signed-off-by: Hayato Kiwata <[email protected]>
1 parent 26a2297 commit c3627e1

File tree

3 files changed

+84
-17
lines changed

3 files changed

+84
-17
lines changed

cmd/nerdctl/image/image_remove_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ package image
1818

1919
import (
2020
"errors"
21+
"strings"
2122
"testing"
2223

24+
"gotest.tools/v3/assert"
25+
2326
"github.com/containerd/nerdctl/v2/pkg/imgutil"
2427
"github.com/containerd/nerdctl/v2/pkg/testutil"
2528
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
@@ -302,3 +305,49 @@ func TestRemove(t *testing.T) {
302305

303306
testCase.Run(t)
304307
}
308+
309+
// TestIssue3016 tests https://github.com/containerd/nerdctl/issues/3016
310+
func TestIssue3016(t *testing.T) {
311+
testCase := nerdtest.Setup()
312+
313+
const (
314+
tagIDKey = "tagID"
315+
)
316+
317+
testCase.SubTests = []*test.Case{
318+
{
319+
Description: "Issue #3016 - Tags created using the short digest ids of container images cannot be deleted using the nerdctl rmi command.",
320+
Setup: func(data test.Data, helpers test.Helpers) {
321+
helpers.Ensure("pull", testutil.CommonImage)
322+
helpers.Ensure("pull", testutil.NginxAlpineImage)
323+
324+
img := nerdtest.InspectImage(helpers, testutil.NginxAlpineImage)
325+
repoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage)
326+
tagID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8]
327+
328+
helpers.Ensure("tag", testutil.CommonImage, tagID)
329+
330+
data.Set(tagIDKey, tagID)
331+
},
332+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
333+
return helpers.Command("rmi", data.Get(tagIDKey))
334+
},
335+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
336+
return &test.Expected{
337+
ExitCode: 0,
338+
Errors: []error{},
339+
Output: func(stdout string, info string, t *testing.T) {
340+
helpers.Command("images", data.Get(tagIDKey)).Run(&test.Expected{
341+
ExitCode: 0,
342+
Output: func(stdout string, info string, t *testing.T) {
343+
assert.Equal(t, len(strings.Split(stdout, "\n")), 2)
344+
},
345+
})
346+
},
347+
}
348+
},
349+
},
350+
}
351+
352+
testCase.Run(t)
353+
}

pkg/cmd/image/remove.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,18 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio
6565
walker := &imagewalker.ImageWalker{
6666
Client: client,
6767
OnFound: func(ctx context.Context, found imagewalker.Found) error {
68-
// if found multiple images, return error unless in force-mode and
69-
// there is only 1 unique image.
70-
if found.MatchCount > 1 && !(options.Force && found.UniqueImages == 1) {
71-
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
68+
if found.NameMatchIndex == -1 {
69+
// if found multiple images, return error unless in force-mode and
70+
// there is only 1 unique image.
71+
if found.MatchCount > 1 && !(options.Force && found.UniqueImages == 1) {
72+
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
73+
}
74+
} else if found.NameMatchIndex != found.MatchIndex {
75+
// when there is an image with a name matching the argument but the argument is a digest short id,
76+
// the deletion process is not performed.
77+
return nil
7278
}
79+
7380
if cid, ok := runningImages[found.Image.Name]; ok {
7481
return fmt.Errorf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", found.Req, cid)
7582
}

pkg/idutil/imagewalker/imagewalker.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import (
3131
)
3232

3333
type Found struct {
34-
Image images.Image
35-
Req string // The raw request string. name, short ID, or long ID.
36-
MatchIndex int // Begins with 0, up to MatchCount - 1.
37-
MatchCount int // 1 on exact match. > 1 on ambiguous match. Never be <= 0.
38-
UniqueImages int // Number of unique images in all found images.
34+
Image images.Image
35+
Req string // The raw request string. name, short ID, or long ID.
36+
MatchIndex int // Begins with 0, up to MatchCount - 1.
37+
MatchCount int // 1 on exact match. > 1 on ambiguous match. Never be <= 0.
38+
UniqueImages int // Number of unique images in all found images.
39+
NameMatchIndex int // Image index with a name matching the argument for `nerdctl rmi`.
3940
}
4041

4142
type OnFound func(ctx context.Context, found Found) error
@@ -50,8 +51,12 @@ type ImageWalker struct {
5051
// Returns the number of the found entries.
5152
func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
5253
var filters []string
53-
if parsedReference, err := referenceutil.Parse(req); err == nil {
54-
filters = append(filters, fmt.Sprintf("name==%s", parsedReference.String()))
54+
var parsedReferenceStr string
55+
56+
parsedReference, err := referenceutil.Parse(req)
57+
if err == nil {
58+
parsedReferenceStr = parsedReference.String()
59+
filters = append(filters, fmt.Sprintf("name==%s", parsedReferenceStr))
5560
}
5661
filters = append(filters,
5762
fmt.Sprintf("name==%s", req),
@@ -68,17 +73,23 @@ func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
6873
// to handle the `rmi -f` case where returned images are different but
6974
// have the same short prefix.
7075
uniqueImages := make(map[digest.Digest]bool)
71-
for _, image := range images {
76+
nameMatchIndex := -1
77+
for i, image := range images {
7278
uniqueImages[image.Target.Digest] = true
79+
// to get target image index for `nerdctl rmi <short digest ids of another images>`.
80+
if (parsedReferenceStr != "" && image.Name == parsedReferenceStr) || image.Name == req {
81+
nameMatchIndex = i
82+
}
7383
}
7484

7585
for i, img := range images {
7686
f := Found{
77-
Image: img,
78-
Req: req,
79-
MatchIndex: i,
80-
MatchCount: matchCount,
81-
UniqueImages: len(uniqueImages),
87+
Image: img,
88+
Req: req,
89+
MatchIndex: i,
90+
MatchCount: matchCount,
91+
UniqueImages: len(uniqueImages),
92+
NameMatchIndex: nameMatchIndex,
8293
}
8394
if e := w.OnFound(ctx, f); e != nil {
8495
return -1, e

0 commit comments

Comments
 (0)