reference filter: match exact behavior of Docker

The previously inherited behavior from Podman was matching too
aggressively.  Now, the filter matches the exact behavior of
Docker, simplifies the code and is tested directly in libimage.

Context: containers/podman#11905
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg 2021-12-03 10:46:13 +01:00
parent 057e879635
commit c15939746c
3 changed files with 91 additions and 13 deletions

View File

@ -3,13 +3,13 @@ package libimage
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
filtersPkg "github.com/containers/common/pkg/filters" filtersPkg "github.com/containers/common/pkg/filters"
"github.com/containers/common/pkg/timetype" "github.com/containers/common/pkg/timetype"
"github.com/containers/image/v5/docker/reference"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -160,20 +160,16 @@ func (r *Runtime) compileImageFilters(ctx context.Context, options *ListImagesOp
// filterReference creates a reference filter for matching the specified value. // filterReference creates a reference filter for matching the specified value.
func filterReference(value string) filterFunc { func filterReference(value string) filterFunc {
// Replacing all '/' with '|' so that filepath.Match() can work '|'
// character is not valid in image name, so this is safe.
//
// TODO: this has been copied from Podman and requires some more review
// and especially tests.
filter := fmt.Sprintf("*%s*", value)
filter = strings.ReplaceAll(filter, "/", "|")
return func(img *Image) (bool, error) { return func(img *Image) (bool, error) {
if len(value) < 1 { refs, err := img.NamesReferences()
return true, nil if err != nil {
return false, err
} }
for _, name := range img.Names() { for _, ref := range refs {
newName := strings.ReplaceAll(name, "/", "|") match, err := reference.FamiliarMatch(value, ref)
match, _ := filepath.Match(filter, newName) if err != nil {
return false, err
}
if match { if match {
return true, nil return true, nil
} }

View File

@ -0,0 +1,62 @@
package libimage
import (
"context"
"os"
"testing"
"github.com/containers/common/pkg/config"
"github.com/stretchr/testify/require"
)
func TestFilterReference(t *testing.T) {
busyboxLatest := "quay.io/libpod/busybox:latest"
alpineLatest := "quay.io/libpod/alpine:latest"
runtime, cleanup := testNewRuntime(t)
defer cleanup()
ctx := context.Background()
pullOptions := &PullOptions{}
pullOptions.Writer = os.Stdout
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
busybox := pulledImages[0]
pulledImages, err = runtime.Pull(ctx, alpineLatest, config.PullPolicyMissing, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
alpine := pulledImages[0]
err = busybox.Tag("localhost/image:tag")
require.NoError(t, err)
err = alpine.Tag("localhost/image:another-tag")
require.NoError(t, err)
for _, test := range []struct {
filter string
matches int
}{
{"image", 0},
{"localhost/image", 2},
{"localhost/image:tag", 1},
{"localhost/image:another-tag", 1},
{"localhost/*", 2},
{"localhost/image:*tag", 2},
{"busybox", 0},
{"alpine", 0},
{"quay.io/libpod/busybox", 1},
{"quay.io/libpod/alpine", 1},
{"quay.io/libpod", 0},
{"quay.io/libpod/*", 2},
} {
listOptions := &ListImagesOptions{
Filters: []string{"reference=" + test.filter},
}
listedImages, err := runtime.ListImages(ctx, nil, listOptions)
require.NoError(t, err, "%v", test)
require.Len(t, listedImages, test.matches, "%s -> %v", test.filter, listedImages)
}
}

View File

@ -44,6 +44,8 @@ type Image struct {
completeInspectData *ImageData completeInspectData *ImageData
// Corresponding OCI image. // Corresponding OCI image.
ociv1Image *ociv1.Image ociv1Image *ociv1.Image
// Names() parsed into references.
namesReferences []reference.Reference
} }
} }
@ -59,6 +61,7 @@ func (i *Image) reload() error {
i.cached.partialInspectData = nil i.cached.partialInspectData = nil
i.cached.completeInspectData = nil i.cached.completeInspectData = nil
i.cached.ociv1Image = nil i.cached.ociv1Image = nil
i.cached.namesReferences = nil
return nil return nil
} }
@ -89,6 +92,23 @@ func (i *Image) Names() []string {
return i.storageImage.Names return i.storageImage.Names
} }
// NamesReferences returns Names() as references.
func (i *Image) NamesReferences() ([]reference.Reference, error) {
if i.cached.namesReferences != nil {
return i.cached.namesReferences, nil
}
refs := make([]reference.Reference, 0, len(i.Names()))
for _, name := range i.Names() {
ref, err := reference.Parse(name)
if err != nil {
return nil, err
}
refs = append(refs, ref)
}
i.cached.namesReferences = refs
return refs, nil
}
// StorageImage returns the underlying storage.Image. // StorageImage returns the underlying storage.Image.
func (i *Image) StorageImage() *storage.Image { func (i *Image) StorageImage() *storage.Image {
return i.storageImage return i.storageImage