Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 6d30b92

Browse files
authored
fix: handling untagged images when tagging and removing (#1511 + #1512) (#1513)
1 parent fadb607 commit 6d30b92

File tree

4 files changed

+109
-58
lines changed

4 files changed

+109
-58
lines changed

pkg/cli/images_rm.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package cli
22

33
import (
44
"fmt"
5-
"strings"
65

76
cli "github.com/acorn-io/acorn/pkg/cli/builder"
87
"github.com/acorn-io/acorn/pkg/client"
9-
"github.com/acorn-io/acorn/pkg/tags"
10-
"github.com/google/go-containerregistry/pkg/name"
118
"github.com/spf13/cobra"
129
)
1310

@@ -34,18 +31,7 @@ func (a *ImageDelete) Run(cmd *cobra.Command, args []string) error {
3431
}
3532

3633
for _, image := range args {
37-
opts := []name.Option{name.WithDefaultRegistry("")}
38-
39-
if strings.HasPrefix("sha256:", image) || tags.SHAPermissivePrefixPattern.MatchString(image) {
40-
opts = append(opts, name.WithDefaultTag(""))
41-
}
42-
43-
// normalize image name (adding :latest if no tag is specified and it's not a digest or potential ID)
44-
ref, err := name.ParseReference(image, opts...)
45-
if err != nil {
46-
return err
47-
}
48-
deleted, err := c.ImageDelete(cmd.Context(), strings.TrimSuffix(ref.Name(), ":"), &client.ImageDeleteOptions{Force: a.Force})
34+
deleted, err := c.ImageDelete(cmd.Context(), image, &client.ImageDeleteOptions{Force: a.Force})
4935
if err != nil {
5036
return fmt.Errorf("deleting %s: %w", image, err)
5137
}

pkg/server/registry/apigroups/acorn/images/storage_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,84 @@ func TestFindMatchingImage(t *testing.T) {
1414
},
1515
{
1616
Digest: "sha256:987654321",
17+
Tags: []string{
18+
"foo:latest",
19+
},
1720
},
1821
{
1922
Digest: "sha256:123409876",
23+
Tags: []string{
24+
"foo/bar",
25+
"foo/bar:dev",
26+
},
2027
},
2128
{
2229
Digest: "sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb",
2330
Tags: []string{
2431
"foo/bar",
32+
"spam/eggs:v1",
2533
},
2634
},
2735
}
2836
il := apiv1.ImageList{
2937
Items: images,
3038
}
3139

40+
// pass: digest prefix
3241
image, ref, err := findImageMatch(il, "12345")
3342
if err != nil {
3443
t.Fatal(err)
3544
}
3645
assert.Equal(t, "sha256:12345678", image.Digest)
3746
assert.Equal(t, "", ref)
3847

48+
// err: ambiguous digest prefix
3949
_, _, err = findImageMatch(il, "123")
4050
assert.Error(t, err)
51+
assert.Equal(t, "Image identifier 123 is not unique", err.Error())
4152

53+
// pass: full digest reference
4254
image, ref, err = findImageMatch(il, "foo/bar@sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb")
4355
if err != nil {
4456
t.Fatal(err)
4557
}
4658
assert.Equal(t, "sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb", image.Digest)
4759
assert.Equal(t, "foo/bar", ref)
4860

61+
// err: not found by full digest reference
4962
_, _, err = findImageMatch(il, "ghcr.io/acorn-io/library/hello-world@sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb")
5063
assert.Error(t, err)
5164
assert.Equal(t, "images.api.acorn.io \"ghcr.io/acorn-io/library/hello-world@sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb\" not found", err.Error())
65+
66+
// err: ambiguous reg/repo reference
67+
_, _, err = findImageMatch(il, "foo/bar")
68+
assert.Error(t, err)
69+
assert.Equal(t, "Image identifier foo/bar is not unique", err.Error())
70+
71+
// pass: full tag reference
72+
image, ref, err = findImageMatch(il, "spam/eggs:v1")
73+
if err != nil {
74+
t.Fatal(err)
75+
}
76+
assert.Equal(t, "sha256:1a6c64d2ccd0bb035f9c8196d3bfe72a7fdbddc4530dfcb3ab2a0ab8afb57eeb", image.Digest)
77+
assert.Equal(t, "spam/eggs:v1", ref)
78+
79+
// pass: repo without tag, defaulting to :latest
80+
image, ref, err = findImageMatch(il, "foo")
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
assert.Equal(t, "sha256:987654321", image.Digest)
85+
assert.Equal(t, "foo:latest", ref)
86+
87+
// err: tiny string that's neither a digest nor an existing tag
88+
// (it is a valid image tag, but not an acorn image tag item)
89+
_, _, err = findImageMatch(il, "dev")
90+
assert.Error(t, err)
91+
assert.Equal(t, "images.api.acorn.io \"dev\" not found", err.Error())
92+
93+
// err: same as above, but with repo part
94+
_, _, err = findImageMatch(il, "bar:dev")
95+
assert.Error(t, err)
96+
assert.Equal(t, "images.api.acorn.io \"bar:dev\" not found", err.Error())
5297
}

pkg/server/registry/apigroups/acorn/images/strategy.go

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -191,29 +191,31 @@ func (s *Strategy) findImage(ctx context.Context, namespace, imageName string) (
191191
// - tag name (with default): <registry>/<repo> or <repo> -> Will be matched against the default tag (:latest)
192192
// - Note: if we get some string here, that matches the SHAPermissivePrefixPattern, it could be both a digest or a name without a tag
193193
// so we will try to match it against the default tag (:latest) first and if that fails, we treat it as a digest(-prefix)
194-
func findImageMatch(images apiv1.ImageList, imageName string) (*apiv1.Image, string, error) {
194+
func findImageMatch(images apiv1.ImageList, search string) (*apiv1.Image, string, error) {
195195
var (
196196
repoDigest name.Digest
197197
digest string
198198
digestPrefix string
199199
tagName string
200200
tagNameDefault string
201+
canBeMultiple bool // if true, we will not return on first match
201202
)
202-
if strings.HasPrefix(imageName, "sha256:") {
203-
digest = imageName
204-
} else if tags2.SHAPattern.MatchString(imageName) {
205-
digest = "sha256:" + imageName
206-
tagNameDefault = imageName
207-
} else if tags2.SHAPermissivePrefixPattern.MatchString(imageName) {
208-
digestPrefix = "sha256:" + imageName
209-
tagNameDefault = imageName
203+
if strings.HasPrefix(search, "sha256:") {
204+
digest = search
205+
} else if tags2.SHAPattern.MatchString(search) {
206+
digest = "sha256:" + search
207+
tagNameDefault = search // this could as well be some name without registry/repo path and tag
208+
} else if tags2.SHAPermissivePrefixPattern.MatchString(search) {
209+
digestPrefix = "sha256:" + search
210+
tagNameDefault = search // this could as well be some name without registry/repo path and tag
210211
} else {
211-
ref, err := name.ParseReference(imageName, name.WithDefaultRegistry(""), name.WithDefaultTag(""))
212+
ref, err := name.ParseReference(search, name.WithDefaultRegistry(""), name.WithDefaultTag(""))
212213
if err != nil {
213214
return nil, "", err
214215
}
215216
if ref.Identifier() == "" {
216-
tagNameDefault = ref.Name()
217+
tagNameDefault = ref.Name() // some name without a tag, so we will try to match it against the default tag (:latest)
218+
canBeMultiple = true
217219
} else if dig, ok := ref.(name.Digest); ok {
218220
repoDigest = dig
219221
} else {
@@ -231,28 +233,31 @@ func findImageMatch(images apiv1.ImageList, imageName string) (*apiv1.Image, str
231233
}
232234

233235
var matchedImage apiv1.Image
236+
var matchedTag string
234237
for _, image := range images.Items {
238+
// >>> match by tag name with default tag (:latest)
235239
if tagNameDefault != "" {
236240
for _, tag := range image.Tags {
237241
if tag == tagNameDefault {
238-
return &image, "", nil
242+
return &image, tag, nil
239243
}
240244
}
241245
}
242246

247+
// >>> match by digest or digest prefix
243248
if image.Digest == digest {
244249
return &image, "", nil
245250
} else if digestPrefix != "" && strings.HasPrefix(image.Digest, digestPrefix) {
246251
if matchedImage.Digest != "" && matchedImage.Digest != image.Digest {
247-
reason := fmt.Sprintf("Image identifier %v is not unique", imageName)
248-
return nil, "", apierrors.NewBadRequest(reason)
252+
return nil, "", apierrors.NewBadRequest(fmt.Sprintf("Image identifier %v is not unique", search))
249253
}
250254
matchedImage = image
251255
}
252256

257+
// >>> match by repo digest
258+
// this returns an image which matches the digest and has at least one tag
259+
// which matches the repo part of the repo digest.
253260
if repoDigest.Name() != "" && image.Digest == repoDigest.DigestStr() {
254-
// Matching by repo digest returns an image which matches the digest and has at least one tag
255-
// which matches the repo part of the repo digest.
256261
for _, tag := range image.Tags {
257262
imageParsedTag, err := name.NewTag(tag, name.WithDefaultRegistry(""))
258263
if err != nil {
@@ -264,27 +269,42 @@ func findImageMatch(images apiv1.ImageList, imageName string) (*apiv1.Image, str
264269
}
265270
}
266271

267-
for i, tag := range image.Tags {
268-
if tag == imageName {
269-
return &image, image.Tags[i], nil
272+
// >>> match by tag name
273+
for _, tag := range image.Tags {
274+
if tag == search {
275+
if !canBeMultiple {
276+
return &image, tag, nil
277+
}
278+
if matchedImage.Digest != "" && matchedImage.Digest != image.Digest {
279+
return nil, "", apierrors.NewBadRequest(fmt.Sprintf("Image identifier %v is not unique", search))
280+
}
281+
matchedImage = image
282+
matchedTag = tag
270283
} else if tag != "" {
271-
imageParsedTag, err := name.NewTag(tag, name.WithDefaultRegistry(""))
284+
imageParsedTag, err := name.NewTag(tag, name.WithDefaultRegistry(""), name.WithDefaultTag("")) // no default here, as we also have repo-only tag items
272285
if err != nil {
273286
continue
274287
}
275288
if imageParsedTag.Name() == tagName {
276-
return &image, tag, nil
289+
if !canBeMultiple {
290+
return &image, tag, nil
291+
}
292+
if matchedImage.Digest != "" && matchedImage.Digest != image.Digest {
293+
return nil, "", apierrors.NewBadRequest(fmt.Sprintf("Image identifier %v is not unique", search))
294+
}
295+
matchedImage = image
296+
matchedTag = tag
277297
}
278298
}
279299
}
280300
}
281301

282302
if matchedImage.Digest != "" {
283-
return &matchedImage, "", nil
303+
return &matchedImage, matchedTag, nil
284304
}
285305

286306
return nil, "", apierrors.NewNotFound(schema.GroupResource{
287307
Group: api.Group,
288308
Resource: "images",
289-
}, imageName)
309+
}, search)
290310
}

pkg/server/registry/apigroups/acorn/images/tag.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,34 +95,34 @@ func (t *TagStrategy) ImageTag(ctx context.Context, namespace, imageName string,
9595
set.Insert(imageRef.Context().Name())
9696
} else {
9797
if set.Has(imageRef.Context().Name()) {
98-
// we're inserting a tag, so we can remove any digest-only entry
98+
// we're inserting a tag, so we can remove any repo-only entry
9999
set.Delete(imageRef.Context().Name())
100100
}
101101
set.Insert(imageRef.Name())
102-
}
103102

104-
// remove the tag from any other image
105-
hasChanged := false
106-
for _, img := range imageList.Items {
107-
if img.Name == image.Name {
108-
continue
109-
}
110-
res, err = normalizeTags(img.Tags, false)
111-
if err != nil {
112-
return nil, err
113-
}
114-
for i, tag := range res {
115-
if set.Has(tag) {
116-
img.Tags = append(img.Tags[:i], img.Tags[i+1:]...)
117-
hasChanged = true
103+
// remove the tag from any other image (not if it's a repo-only reference, e.g. pulled by digest, as those can be duplicated)
104+
hasChanged := false
105+
for _, img := range imageList.Items {
106+
if img.Name == image.Name {
107+
continue
118108
}
119-
}
120-
if hasChanged {
121-
err = t.client.Update(ctx, &img)
109+
res, err = normalizeTags(img.Tags, false)
122110
if err != nil {
123111
return nil, err
124112
}
125-
hasChanged = false
113+
for i, tag := range res {
114+
if set.Has(tag) {
115+
img.Tags = append(img.Tags[:i], img.Tags[i+1:]...)
116+
hasChanged = true
117+
}
118+
}
119+
if hasChanged {
120+
err = t.client.Update(ctx, &img)
121+
if err != nil {
122+
return nil, err
123+
}
124+
hasChanged = false
125+
}
126126
}
127127
}
128128

0 commit comments

Comments
 (0)