Skip to content

Commit 3952c67

Browse files
Merge pull request #2413 from Honny1/list-images
Fix unexpected results when filtering images
2 parents 1bc9d17 + ec24373 commit 3952c67

File tree

4 files changed

+154
-49
lines changed

4 files changed

+154
-49
lines changed

libimage/filters.go

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type compiledFilters map[string][]filterFunc
2525

2626
// Apply the specified filters. All filters of each key must apply.
2727
// tree must be provided if compileImageFilters indicated it is necessary.
28+
// WARNING: Application of filterReferences sets the image names to matched names, but this only affects the values in memory, they are not written to storage.
2829
func (i *Image) applyFilters(ctx context.Context, filters compiledFilters, tree *layerTree) (bool, error) {
2930
for key := range filters {
3031
for _, filter := range filters[key] {
@@ -51,6 +52,7 @@ func (i *Image) applyFilters(ctx context.Context, filters compiledFilters, tree
5152
// filterImages returns a slice of images which are passing all specified
5253
// filters.
5354
// tree must be provided if compileImageFilters indicated it is necessary.
55+
// WARNING: Application of filterReferences sets the image names to matched names, but this only affects the values in memory, they are not written to storage.
5456
func (r *Runtime) filterImages(ctx context.Context, images []*Image, filters compiledFilters, tree *layerTree) ([]*Image, error) {
5557
result := []*Image{}
5658
for i := range images {
@@ -272,39 +274,87 @@ func filterReferences(r *Runtime, wantedReferenceMatches, unwantedReferenceMatch
272274
return true, nil
273275
}
274276

275-
unwantedMatched := false
276277
// Go through the unwanted matches first
277278
for _, value := range unwantedReferenceMatches {
278279
matches, err := imageMatchesReferenceFilter(r, img, value)
279280
if err != nil {
280281
return false, err
281282
}
282283
if matches {
283-
unwantedMatched = true
284+
return false, nil
284285
}
285286
}
286287

287288
// If there are no wanted match filters, then return false for the image
288289
// that matched the unwanted value otherwise return true
289290
if len(wantedReferenceMatches) == 0 {
290-
return !unwantedMatched, nil
291+
return true, nil
291292
}
292293

293-
// Go through the wanted matches
294-
// If an image matches the wanted filter but it also matches the unwanted
295-
// filter, don't add it to the output
294+
matchedReference := ""
296295
for _, value := range wantedReferenceMatches {
297296
matches, err := imageMatchesReferenceFilter(r, img, value)
298297
if err != nil {
299298
return false, err
300299
}
301-
if matches && !unwantedMatched {
300+
if matches {
301+
matchedReference = value
302+
break
303+
}
304+
}
305+
306+
if matchedReference == "" {
307+
return false, nil
308+
}
309+
310+
// If there is exactly one wanted reference match and no unwanted matches,
311+
// the filter is treated as a query, so it sets the matching names to
312+
// the image in memory.
313+
if len(wantedReferenceMatches) == 1 && len(unwantedReferenceMatches) == 0 {
314+
ref, ok := isFullyQualifiedReference(matchedReference)
315+
if !ok {
302316
return true, nil
303317
}
318+
namesThatMatch := []string{}
319+
for _, name := range img.Names() {
320+
if nameMatchesReference(name, ref) {
321+
namesThatMatch = append(namesThatMatch, name)
322+
}
323+
}
324+
img.setEphemeralNames(namesThatMatch)
304325
}
326+
return true, nil
327+
}
328+
}
329+
330+
// isFullyQualifiedReference checks if the provided string is a fully qualified
331+
// reference (i.e., it contains a domain, path, and tag or digest).
332+
// It returns a reference.Named and a boolean indicating whether the
333+
// reference is fully qualified. If the reference is not fully qualified,
334+
// it returns nil and false.
335+
func isFullyQualifiedReference(r string) (reference.Named, bool) {
336+
ref, err := reference.ParseNamed(r)
337+
// If there is an error parsing the reference, it is not a valid reference
338+
if err != nil {
339+
return nil, false
340+
}
341+
// If it's name-only (no tag/digest), it's not fully qualified
342+
if reference.IsNameOnly(ref) {
343+
return nil, false
344+
}
345+
return ref, true
346+
}
305347

306-
return false, nil
348+
func nameMatchesReference(name string, ref reference.Named) bool {
349+
_, containsDigest := ref.(reference.Digested)
350+
if containsDigest {
351+
nameRef, err := reference.ParseNamed(name)
352+
if err != nil {
353+
return false
354+
}
355+
return nameRef.Name() == ref.Name()
307356
}
357+
return name == ref.String()
308358
}
309359

310360
// imageMatchesReferenceFilter returns true if an image matches the filter value given
@@ -352,7 +402,6 @@ func imageMatchesReferenceFilter(r *Runtime, img *Image, value string) (bool, er
352402
}
353403
}
354404
}
355-
356405
return false, nil
357406
}
358407

libimage/filters_test.go

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,51 +40,82 @@ func TestFilterReference(t *testing.T) {
4040
require.NoError(t, err)
4141
err = alpine.Tag("docker.io/library/image:another-tag")
4242
require.NoError(t, err)
43+
err = alpine.Tag("docker.io/library/image:latest")
44+
require.NoError(t, err)
45+
err = busybox.Tag("localhost/aa:tag")
46+
require.NoError(t, err)
47+
err = busybox.Tag("localhost/a:tag")
48+
require.NoError(t, err)
49+
50+
allAlpineNames := []string{
51+
"docker.io/library/image:another-tag",
52+
"quay.io/libpod/alpine:latest",
53+
"localhost/another-image:tag",
54+
"docker.io/library/image:latest",
55+
}
56+
allBusyBoxNames := []string{
57+
"quay.io/libpod/busybox:latest",
58+
"localhost/image:tag",
59+
"localhost/a:tag",
60+
"localhost/aa:tag",
61+
}
62+
allNames := []string{}
63+
allNames = append(allNames, allBusyBoxNames...)
64+
allNames = append(allNames, allAlpineNames...)
4365

4466
for _, test := range []struct {
45-
filters []string
46-
matches int
67+
filters []string
68+
matchedImages int
69+
names []string
4770
}{
48-
{[]string{"image"}, 2},
49-
{[]string{"*mage*"}, 2},
50-
{[]string{"image:*"}, 2},
51-
{[]string{"image:tag"}, 1},
52-
{[]string{"image:another-tag"}, 1},
53-
{[]string{"localhost/image"}, 1},
54-
{[]string{"localhost/image:tag"}, 1},
55-
{[]string{"library/image"}, 1},
56-
{[]string{"docker.io/library/image*"}, 1},
57-
{[]string{"docker.io/library/image:*"}, 1},
58-
{[]string{"docker.io/library/image:another-tag"}, 1},
59-
{[]string{"localhost/*"}, 2},
60-
{[]string{"localhost/image:*tag"}, 1},
61-
{[]string{"localhost/*mage:*ag"}, 2},
62-
{[]string{"quay.io/libpod/busybox"}, 1},
63-
{[]string{"quay.io/libpod/alpine"}, 1},
64-
{[]string{"quay.io/libpod"}, 0},
65-
{[]string{"quay.io/libpod/*"}, 2},
66-
{[]string{"busybox"}, 1},
67-
{[]string{"alpine"}, 1},
68-
{[]string{"alpine@" + alpine.Digest().String()}, 1},
69-
{[]string{"alpine:latest@" + alpine.Digest().String()}, 1},
70-
{[]string{"quay.io/libpod/alpine@" + alpine.Digest().String()}, 1},
71-
{[]string{"quay.io/libpod/alpine:latest@" + alpine.Digest().String()}, 1},
71+
{[]string{"image"}, 2, allNames},
72+
{[]string{"*mage*"}, 2, allNames},
73+
{[]string{"image:*"}, 2, allNames},
74+
{[]string{"image:tag"}, 1, allBusyBoxNames},
75+
{[]string{"image:another-tag"}, 1, allAlpineNames},
76+
{[]string{"localhost/image"}, 1, allBusyBoxNames},
77+
{[]string{"localhost/image:tag"}, 1, []string{"localhost/image:tag"}},
78+
{[]string{"library/image"}, 1, allAlpineNames},
79+
{[]string{"docker.io/library/image*"}, 1, allAlpineNames},
80+
{[]string{"docker.io/library/image:*"}, 1, allAlpineNames},
81+
{[]string{"docker.io/library/image:another-tag"}, 1, []string{"docker.io/library/image:another-tag"}},
82+
{[]string{"localhost/*"}, 2, allNames},
83+
{[]string{"localhost/image:*tag"}, 1, allBusyBoxNames},
84+
{[]string{"localhost/*mage:*ag"}, 2, allNames},
85+
{[]string{"quay.io/libpod/busybox"}, 1, allBusyBoxNames},
86+
{[]string{"quay.io/libpod/alpine"}, 1, allAlpineNames},
87+
{[]string{"quay.io/libpod"}, 0, []string{}},
88+
{[]string{"quay.io/libpod/*"}, 2, allNames},
89+
{[]string{"busybox"}, 1, allBusyBoxNames},
90+
{[]string{"alpine"}, 1, allAlpineNames},
91+
7292
// Make sure negate works as expected
73-
{[]string{"!alpine"}, 1},
74-
{[]string{"!alpine", "!busybox"}, 0},
75-
{[]string{"!alpine", "busybox"}, 1},
76-
{[]string{"alpine", "busybox"}, 2},
77-
{[]string{"*test", "!*box"}, 1},
93+
{[]string{"!alpine"}, 1, allBusyBoxNames},
94+
{[]string{"!alpine", "!busybox"}, 0, []string{}},
95+
{[]string{"!alpine", "busybox"}, 1, allBusyBoxNames},
96+
{[]string{"alpine", "busybox"}, 2, allNames},
97+
{[]string{"*test", "!*box"}, 1, allAlpineNames},
98+
99+
{[]string{"quay.io/libpod/alpine@" + alpine.Digest().String()}, 1, []string{"quay.io/libpod/alpine:latest"}},
100+
101+
{[]string{"alpine@" + alpine.Digest().String()}, 1, allAlpineNames},
102+
{[]string{"alpine:latest@" + alpine.Digest().String()}, 1, allAlpineNames},
103+
{[]string{"quay.io/libpod/alpine:latest@" + alpine.Digest().String()}, 1, []string{"quay.io/libpod/alpine:latest"}},
104+
{[]string{"docker.io/library/image@" + alpine.Digest().String()}, 1, []string{"docker.io/library/image:latest", "docker.io/library/image:another-tag"}},
105+
{[]string{"localhost/aa@" + busybox.Digest().String()}, 1, []string{"localhost/aa:tag"}},
106+
{[]string{"localhost/a@" + busybox.Digest().String()}, 1, []string{"localhost/a:tag"}},
107+
78108
// Make sure that tags are ignored
79-
{[]string{"alpine:ignoreme@" + alpine.Digest().String()}, 1},
80-
{[]string{"alpine:123@" + alpine.Digest().String()}, 1},
81-
{[]string{"quay.io/libpod/alpine:hurz@" + alpine.Digest().String()}, 1},
82-
{[]string{"quay.io/libpod/alpine:456@" + alpine.Digest().String()}, 1},
109+
{[]string{"alpine:ignoreme@" + alpine.Digest().String()}, 1, allAlpineNames},
110+
{[]string{"alpine:123@" + alpine.Digest().String()}, 1, allAlpineNames},
111+
{[]string{"quay.io/libpod/alpine:hurz@" + alpine.Digest().String()}, 1, []string{"quay.io/libpod/alpine:latest"}},
112+
{[]string{"quay.io/libpod/alpine:456@" + alpine.Digest().String()}, 1, []string{"quay.io/libpod/alpine:latest"}},
113+
83114
// Make sure that repo and digest must match
84-
{[]string{"alpine:busyboxdigest@" + busybox.Digest().String()}, 0},
85-
{[]string{"alpine:busyboxdigest@" + busybox.Digest().String()}, 0},
86-
{[]string{"quay.io/libpod/alpine:busyboxdigest@" + busybox.Digest().String()}, 0},
87-
{[]string{"quay.io/libpod/alpine:busyboxdigest@" + busybox.Digest().String()}, 0},
115+
{[]string{"alpine:busyboxdigest@" + busybox.Digest().String()}, 0, []string{}},
116+
{[]string{"alpine:busyboxdigest@" + busybox.Digest().String()}, 0, []string{}},
117+
{[]string{"quay.io/libpod/alpine:busyboxdigest@" + busybox.Digest().String()}, 0, []string{}},
118+
{[]string{"quay.io/libpod/alpine:busyboxdigest@" + busybox.Digest().String()}, 0, []string{}},
88119
} {
89120
var filters []string
90121
for _, filter := range test.filters {
@@ -94,12 +125,20 @@ func TestFilterReference(t *testing.T) {
94125
filters = append(filters, "reference="+filter)
95126
}
96127
}
128+
97129
listOptions := &ListImagesOptions{
98130
Filters: filters,
99131
}
100132
listedImages, err := runtime.ListImages(ctx, listOptions)
133+
101134
require.NoError(t, err, "%v", test)
102-
require.Len(t, listedImages, test.matches, "%s -> %v", test.filters, listedImages)
135+
require.Len(t, listedImages, test.matchedImages, "%s -> %v", test.filters, listedImages)
136+
137+
resultNames := []string{}
138+
for _, image := range listedImages {
139+
resultNames = append(resultNames, image.Names()...)
140+
}
141+
require.ElementsMatch(t, test.names, resultNames, "filters: %s ", test.filters)
103142
}
104143
}
105144

libimage/image.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ func (i *Image) Names() []string {
112112
return i.storageImage.Names
113113
}
114114

115+
// setEphemeralNames sets the names of the image.
116+
//
117+
// WARNING: this only affects the in-memory values, they are not written into the backing storage.
118+
func (i *Image) setEphemeralNames(names []string) {
119+
i.storageImage.Names = names
120+
}
121+
115122
// NamesReferences returns Names() as references.
116123
func (i *Image) NamesReferences() ([]reference.Reference, error) {
117124
if i.cached.namesReferences != nil {

libimage/runtime.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,16 @@ func (r *Runtime) ListImagesByNames(names []string) ([]*Image, error) {
599599
}
600600

601601
// ListImages lists the images in the local container storage and filter the images by ListImagesOptions
602+
//
603+
// podman images consumes the output of ListImages and produces one line for each tag in each Image.Names value,
604+
// rather than one line for each Image with all Names, so if options.Filters contains one reference filter
605+
// with a fully qualified image name without negation, it is considered a query so it makes more sense for
606+
// the user to see only the corresponding names in the output, not all the names of the deduplicated
607+
// image; therefore, we make the corresponding names available to the caller by overwriting the actual image names
608+
// with the corresponding names when the reference filter matches and the reference is a fully qualified image name
609+
// (i.e., contains a tag or digest, not just a bare repository name).
610+
//
611+
// This overwriting is done only in memory and is not written to storage in any way.
602612
func (r *Runtime) ListImages(ctx context.Context, options *ListImagesOptions) ([]*Image, error) {
603613
if options == nil {
604614
options = &ListImagesOptions{}

0 commit comments

Comments
 (0)