Skip to content

Commit ec24373

Browse files
committed
Fix unexpected results when filtering images
The filtered image contains all Names of image. This causes the podman to list images that are not expected. Fixes: containers/podman#25725 Fixes: https://issues.redhat.com/browse/RUN-2726 Signed-off-by: Jan Rodák <[email protected]>
1 parent 8dc0e3b commit ec24373

File tree

4 files changed

+90
-16
lines changed

4 files changed

+90
-16
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: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ func TestFilterReference(t *testing.T) {
4242
require.NoError(t, err)
4343
err = alpine.Tag("docker.io/library/image:latest")
4444
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)
4549

4650
allAlpineNames := []string{
4751
"docker.io/library/image:another-tag",
@@ -52,6 +56,8 @@ func TestFilterReference(t *testing.T) {
5256
allBusyBoxNames := []string{
5357
"quay.io/libpod/busybox:latest",
5458
"localhost/image:tag",
59+
"localhost/a:tag",
60+
"localhost/aa:tag",
5561
}
5662
allNames := []string{}
5763
allNames = append(allNames, allBusyBoxNames...)
@@ -68,11 +74,11 @@ func TestFilterReference(t *testing.T) {
6874
{[]string{"image:tag"}, 1, allBusyBoxNames},
6975
{[]string{"image:another-tag"}, 1, allAlpineNames},
7076
{[]string{"localhost/image"}, 1, allBusyBoxNames},
71-
{[]string{"localhost/image:tag"}, 1, allBusyBoxNames},
77+
{[]string{"localhost/image:tag"}, 1, []string{"localhost/image:tag"}},
7278
{[]string{"library/image"}, 1, allAlpineNames},
7379
{[]string{"docker.io/library/image*"}, 1, allAlpineNames},
7480
{[]string{"docker.io/library/image:*"}, 1, allAlpineNames},
75-
{[]string{"docker.io/library/image:another-tag"}, 1, allAlpineNames},
81+
{[]string{"docker.io/library/image:another-tag"}, 1, []string{"docker.io/library/image:another-tag"}},
7682
{[]string{"localhost/*"}, 2, allNames},
7783
{[]string{"localhost/image:*tag"}, 1, allBusyBoxNames},
7884
{[]string{"localhost/*mage:*ag"}, 2, allNames},
@@ -90,18 +96,20 @@ func TestFilterReference(t *testing.T) {
9096
{[]string{"alpine", "busybox"}, 2, allNames},
9197
{[]string{"*test", "!*box"}, 1, allAlpineNames},
9298

93-
{[]string{"quay.io/libpod/alpine@" + alpine.Digest().String()}, 1, allAlpineNames},
99+
{[]string{"quay.io/libpod/alpine@" + alpine.Digest().String()}, 1, []string{"quay.io/libpod/alpine:latest"}},
94100

95101
{[]string{"alpine@" + alpine.Digest().String()}, 1, allAlpineNames},
96102
{[]string{"alpine:latest@" + alpine.Digest().String()}, 1, allAlpineNames},
97-
{[]string{"quay.io/libpod/alpine:latest@" + alpine.Digest().String()}, 1, allAlpineNames},
98-
{[]string{"docker.io/library/image@" + 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"}},
99107

100108
// Make sure that tags are ignored
101109
{[]string{"alpine:ignoreme@" + alpine.Digest().String()}, 1, allAlpineNames},
102110
{[]string{"alpine:123@" + alpine.Digest().String()}, 1, allAlpineNames},
103-
{[]string{"quay.io/libpod/alpine:hurz@" + alpine.Digest().String()}, 1, allAlpineNames},
104-
{[]string{"quay.io/libpod/alpine:456@" + 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"}},
105113

106114
// Make sure that repo and digest must match
107115
{[]string{"alpine:busyboxdigest@" + busybox.Digest().String()}, 0, []string{}},

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)