-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pkg: add new
imagefilter
package with an ImageFilter
This commit creates a new `imagefilter` package that can be used to create a filter for the available images that can be build by the images library. Support for prefixes and fnmatch style globbing is available. This will eventually allows us to do: ``` $ image-builder --list-images --filter arch:aarch64 --filter type:qcow* or $ image-builder --list-images --filter ami ``` The filtering can be both with or without a prefix, i.e. when doing a "simple" search all common fields (disro,arch,imageType) are checked but with a prefix search things can be narrowed down more. Initially the following prefixes filters are supported: "distro:" - the distro name, e.g. rhel-9, or fedora* "arch:" - the architecture, e.g. x86_64 "type": - the image type, e.g. ami, or qcow? "bootmode": - the bootmode, e.g. "legacy", "uefi", "hybrid" (Split out from the spike in #997)
- Loading branch information
Showing
4 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package imagefilter | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/gobwas/glob" | ||
|
||
"github.com/osbuild/images/pkg/distro" | ||
) | ||
|
||
func splitPrefixSearchTerm(s string) (string, string) { | ||
l := strings.SplitN(s, ":", 2) | ||
if len(l) == 1 { | ||
return "", l[0] | ||
} | ||
return l[0], l[1] | ||
} | ||
|
||
// newFilter creates an image filter based on the given filter terms. Glob like | ||
// patterns (?, *) are supported, see fnmatch(3). | ||
// | ||
// Without a prefix in the filter term a simple name filtering is performed. | ||
// With a prefix the specified property is filtered, e.g. "arch:i386". Adding | ||
// filtering will narrow down the filtering (terms are combined via AND). | ||
// | ||
// The following prefixes are supported: | ||
// "distro:" - the distro name, e.g. rhel-9, or fedora* | ||
// "arch:" - the architecture, e.g. x86_64 | ||
// "type": - the image type, e.g. ami, or qcow? | ||
// "bootmode": - the bootmode, e.g. "legacy", "uefi", "hybrid" | ||
func newFilter(sl ...string) (*filter, error) { | ||
filter := &filter{ | ||
terms: make([]term, len(sl)), | ||
} | ||
for i, s := range sl { | ||
prefix, searchTerm := splitPrefixSearchTerm(s) | ||
if !slices.Contains(supportedFilters, prefix) { | ||
return nil, fmt.Errorf("unsupported filter prefix: %q", prefix) | ||
} | ||
gl, err := glob.Compile(searchTerm) | ||
if err != nil { | ||
return nil, err | ||
} | ||
filter.terms[i].prefix = prefix | ||
filter.terms[i].pattern = gl | ||
} | ||
return filter, nil | ||
} | ||
|
||
var supportedFilters = []string{ | ||
"", "distro", "arch", "type", "bootmode", | ||
} | ||
|
||
type term struct { | ||
prefix string | ||
pattern glob.Glob | ||
} | ||
|
||
// filter provides a way to filter a list of image defintions for the | ||
// given filter terms. | ||
type filter struct { | ||
terms []term | ||
} | ||
|
||
// Matches returns true if the given (distro,arch,imgType) tuple matches | ||
// the filter expressions | ||
func (fl filter) Matches(distro distro.Distro, arch distro.Arch, imgType distro.ImageType) bool { | ||
m := true | ||
for _, term := range fl.terms { | ||
switch term.prefix { | ||
case "": | ||
// no prefix, do a "fuzzy" search accross the common | ||
// things users may want | ||
m1 := term.pattern.Match(distro.Name()) | ||
m2 := term.pattern.Match(arch.Name()) | ||
m3 := term.pattern.Match(imgType.Name()) | ||
m = m && (m1 || m2 || m3) | ||
case "distro": | ||
m = m && term.pattern.Match(distro.Name()) | ||
case "arch": | ||
m = m && term.pattern.Match(arch.Name()) | ||
case "type": | ||
m = m && term.pattern.Match(imgType.Name()) | ||
// mostly here to show how flexible this is | ||
case "bootmode": | ||
m = m && term.pattern.Match(imgType.BootMode().String()) | ||
} | ||
} | ||
return m | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package imagefilter | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/osbuild/images/pkg/distrofactory" | ||
) | ||
|
||
func TestImageFilterFilter(t *testing.T) { | ||
fac := distrofactory.NewTestDefault() | ||
|
||
for _, tc := range []struct { | ||
searchExpr []string | ||
distro, arch, imgType string | ||
expectsMatch bool | ||
}{ | ||
// no prefix is a "fuzzy" filter and will check distro/arch/imgType | ||
{[]string{"foo"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"test-distro-1"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"test-distro*"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"test_arch3"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"qcow2"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
// distro: prefix (exact matches only) | ||
{[]string{"distro:bar"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"distro:test-distro-1"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"distro:test-distro"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
// arch: prefix | ||
{[]string{"arch:amd64"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"arch:test_arch3"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"arch:test_ar"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"arch:test_ar*"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
// type: prefix | ||
{[]string{"type:ami"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"type:qcow2"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"type:qcow"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"type:qcow?"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
// bootmode: prefix | ||
{[]string{"bootmode:uefi"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"bootmode:hybrid"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
// multiple filters are AND | ||
{[]string{"distro:test-distro-1", "type:ami"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
{[]string{"distro:test-distro-1", "type:qcow2"}, "test-distro-1", "test_arch3", "qcow2", true}, | ||
{[]string{"distro:test-distro-1", "arch:amd64", "type:qcow2"}, "test-distro-1", "test_arch3", "qcow2", false}, | ||
} { | ||
// XXX: it would be nice if TestDistro would support constructing | ||
// like GetDistro("rhel-8.1:i386,amd64:ami,qcow2") instead of | ||
// the current very static setup | ||
di := fac.GetDistro(tc.distro) | ||
require.NotNil(t, di) | ||
ar, err := di.GetArch(tc.arch) | ||
require.NoError(t, err) | ||
im, err := ar.GetImageType(tc.imgType) | ||
require.NoError(t, err) | ||
ff, err := newFilter(tc.searchExpr...) | ||
require.NoError(t, err) | ||
|
||
match := ff.Matches(di, ar, im) | ||
assert.Equal(t, tc.expectsMatch, match, tc) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package imagefilter | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/osbuild/images/pkg/distro" | ||
"github.com/osbuild/images/pkg/distrofactory" | ||
"github.com/osbuild/images/pkg/distrosort" | ||
) | ||
|
||
type DistroLister interface { | ||
ListDistros() []string | ||
} | ||
|
||
// Result contains a result from a imagefilter.Filter run | ||
type Result struct { | ||
Distro distro.Distro | ||
Arch distro.Arch | ||
ImgType distro.ImageType | ||
} | ||
|
||
// ImageFilter is an a flexible way to filter the available images. | ||
type ImageFilter struct { | ||
fac *distrofactory.Factory | ||
repos DistroLister | ||
} | ||
|
||
// New creates a new ImageFilter that can be used to filter the list | ||
// of available images | ||
func New(fac *distrofactory.Factory, repos DistroLister) (*ImageFilter, error) { | ||
if fac == nil { | ||
return nil, fmt.Errorf("cannot create ImageFilter without a valid distrofactory") | ||
} | ||
if repos == nil { | ||
return nil, fmt.Errorf("cannot create ImageFilter without a valid reporegistry") | ||
} | ||
|
||
return &ImageFilter{fac: fac, repos: repos}, nil | ||
} | ||
|
||
// Filter filters the available images for the given | ||
// distrofactory/reporegistry based on the given filter terms. Glob | ||
// like patterns (?, *) are supported, see fnmatch(3). | ||
// | ||
// Without a prefix in the filter term a simple name filtering is performed. | ||
// With a prefix the specified property is filtered, e.g. "arch:i386". Adding | ||
// filtering will narrow down the filtering (terms are combined via AND). | ||
// | ||
// The following prefixes are supported: | ||
// "distro:" - the distro name, e.g. rhel-9, or fedora* | ||
// "arch:" - the architecture, e.g. x86_64 | ||
// "type": - the image type, e.g. ami, or qcow? | ||
// "bootmode": - the bootmode, e.g. "legacy", "uefi", "hybrid" | ||
func (i *ImageFilter) Filter(searchTerms ...string) ([]Result, error) { | ||
var res []Result | ||
|
||
distroNames := i.repos.ListDistros() | ||
filter, err := newFilter(searchTerms...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := distrosort.Names(distroNames); err != nil { | ||
return nil, err | ||
} | ||
for _, distroName := range distroNames { | ||
distro := i.fac.GetDistro(distroName) | ||
if distro == nil { | ||
// XXX: log here? | ||
continue | ||
} | ||
for _, archName := range distro.ListArches() { | ||
a, err := distro.GetArch(archName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, imgTypeName := range a.ListImageTypes() { | ||
imgType, err := a.GetImageType(imgTypeName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if filter.Matches(distro, a, imgType) { | ||
res = append(res, Result{distro, a, imgType}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
return res, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package imagefilter_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/sirupsen/logrus" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/osbuild/images/pkg/distrofactory" | ||
"github.com/osbuild/images/pkg/imagefilter" | ||
"github.com/osbuild/images/pkg/reporegistry" | ||
) | ||
|
||
func TestImageFilterSmoke(t *testing.T) { | ||
logrus.SetLevel(logrus.WarnLevel) | ||
|
||
fac := distrofactory.NewDefault() | ||
repos, err := reporegistry.NewTestedDefault() | ||
require.NoError(t, err) | ||
|
||
imgFilter, err := imagefilter.New(fac, repos) | ||
require.NoError(t, err) | ||
res, err := imgFilter.Filter("*") | ||
require.NoError(t, err) | ||
assert.True(t, len(res) > 0) | ||
} | ||
|
||
func TestImageFilterFilter(t *testing.T) { | ||
fac := distrofactory.NewDefault() | ||
repos, err := reporegistry.NewTestedDefault() | ||
require.NoError(t, err) | ||
|
||
imgFilter, err := imagefilter.New(fac, repos) | ||
require.NoError(t, err) | ||
|
||
for _, tc := range []struct { | ||
searchExpr []string | ||
expectsMatch bool | ||
}{ | ||
// no prefix is a "fuzzy" filter and will check distro/arch/imgType | ||
{[]string{"foo"}, false}, | ||
{[]string{"rhel-9.1"}, true}, | ||
{[]string{"rhel*"}, true}, | ||
{[]string{"x86_64"}, true}, | ||
{[]string{"qcow2"}, true}, | ||
// distro: prefix | ||
{[]string{"distro:foo"}, false}, | ||
{[]string{"distro:centos-9"}, true}, | ||
{[]string{"distro:centos*"}, true}, | ||
{[]string{"distro:centos"}, false}, | ||
// arch: prefix | ||
{[]string{"arch:foo"}, false}, | ||
{[]string{"arch:x86_64"}, true}, | ||
{[]string{"arch:x86*"}, true}, | ||
{[]string{"arch:x86"}, false}, | ||
// type: prefix | ||
{[]string{"type:foo"}, false}, | ||
{[]string{"type:qcow2"}, true}, | ||
{[]string{"type:qcow?"}, true}, | ||
{[]string{"type:qcow"}, false}, | ||
// bootmode: prefix | ||
{[]string{"bootmode:foo"}, false}, | ||
{[]string{"bootmode:hybrid"}, true}, | ||
// multiple filters are AND | ||
{[]string{"distro:centos-9", "type:foo"}, false}, | ||
{[]string{"distro:centos-9", "type:qcow2"}, true}, | ||
{[]string{"distro:centos-9", "arch:foo", "type:qcow2"}, false}, | ||
} { | ||
|
||
matches, err := imgFilter.Filter(tc.searchExpr...) | ||
assert.NoError(t, err) | ||
assert.Equal(t, tc.expectsMatch, len(matches) > 0, tc) | ||
} | ||
} |