automation-tests/common/libimage/filters.go

273 lines
7.5 KiB
Go

package libimage
import (
"context"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
filtersPkg "github.com/containers/common/pkg/filters"
"github.com/containers/common/pkg/timetype"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// filterFunc is a prototype for a positive image filter. Returning `true`
// indicates that the image matches the criteria.
type filterFunc func(*Image) (bool, error)
// filterImages returns a slice of images which are passing all specified
// filters.
func filterImages(images []*Image, filters []filterFunc) ([]*Image, error) {
if len(filters) == 0 {
return images, nil
}
result := []*Image{}
for i := range images {
include := true
var err error
for _, filter := range filters {
include, err = filter(images[i])
if err != nil {
return nil, err
}
if !include {
break
}
}
if include {
result = append(result, images[i])
}
}
return result, nil
}
// compileImageFilters creates `filterFunc`s for the specified filters. The
// required format is `key=value` with the following supported keys:
// after, since, before, containers, dangling, id, label, readonly, reference, intermediate
func (r *Runtime) compileImageFilters(ctx context.Context, options *ListImagesOptions) ([]filterFunc, error) {
logrus.Tracef("Parsing image filters %s", options.Filters)
var tree *layerTree
getTree := func() (*layerTree, error) {
if tree == nil {
t, err := r.layerTree()
if err != nil {
return nil, err
}
tree = t
}
return tree, nil
}
filterFuncs := []filterFunc{}
for _, filter := range options.Filters {
var key, value string
split := strings.SplitN(filter, "=", 2)
if len(split) != 2 {
return nil, errors.Errorf("invalid image filter %q: must be in the format %q", filter, "filter=value")
}
key = split[0]
value = split[1]
switch key {
case "after", "since":
img, _, err := r.LookupImage(value, nil)
if err != nil {
return nil, errors.Wrapf(err, "could not find local image for filter %q", filter)
}
filterFuncs = append(filterFuncs, filterAfter(img.Created()))
case "before":
img, _, err := r.LookupImage(value, nil)
if err != nil {
return nil, errors.Wrapf(err, "could not find local image for filter %q", filter)
}
filterFuncs = append(filterFuncs, filterBefore(img.Created()))
case "containers":
switch value {
case "false", "true":
case "external":
if options.IsExternalContainerFunc == nil {
return nil, fmt.Errorf("libimage error: external containers filter without callback")
}
default:
return nil, fmt.Errorf("unsupported value %q for containers filter", value)
}
filterFuncs = append(filterFuncs, filterContainers(value, options.IsExternalContainerFunc))
case "dangling":
dangling, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Wrapf(err, "non-boolean value %q for dangling filter", value)
}
t, err := getTree()
if err != nil {
return nil, err
}
filterFuncs = append(filterFuncs, filterDangling(ctx, dangling, t))
case "id":
filterFuncs = append(filterFuncs, filterID(value))
case "intermediate":
intermediate, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Wrapf(err, "non-boolean value %q for intermediate filter", value)
}
t, err := getTree()
if err != nil {
return nil, err
}
filterFuncs = append(filterFuncs, filterIntermediate(ctx, intermediate, t))
case "label":
filterFuncs = append(filterFuncs, filterLabel(ctx, value))
case "readonly":
readOnly, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Wrapf(err, "non-boolean value %q for readonly filter", value)
}
filterFuncs = append(filterFuncs, filterReadOnly(readOnly))
case "reference":
filterFuncs = append(filterFuncs, filterReference(value))
case "until":
ts, err := timetype.GetTimestamp(value, time.Now())
if err != nil {
return nil, err
}
seconds, nanoseconds, err := timetype.ParseTimestamps(ts, 0)
if err != nil {
return nil, err
}
until := time.Unix(seconds, nanoseconds)
filterFuncs = append(filterFuncs, filterBefore(until))
default:
return nil, errors.Errorf("unsupported image filter %q", key)
}
}
return filterFuncs, nil
}
// filterReference creates a reference filter for matching the specified value.
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) {
if len(value) < 1 {
return true, nil
}
for _, name := range img.Names() {
newName := strings.ReplaceAll(name, "/", "|")
match, _ := filepath.Match(filter, newName)
if match {
return true, nil
}
}
return false, nil
}
}
// filterLabel creates a label for matching the specified value.
func filterLabel(ctx context.Context, value string) filterFunc {
return func(img *Image) (bool, error) {
labels, err := img.Labels(ctx)
if err != nil {
return false, err
}
return filtersPkg.MatchLabelFilters([]string{value}, labels), nil
}
}
// filterAfter creates an after filter for matching the specified value.
func filterAfter(value time.Time) filterFunc {
return func(img *Image) (bool, error) {
return img.Created().After(value), nil
}
}
// filterBefore creates a before filter for matching the specified value.
func filterBefore(value time.Time) filterFunc {
return func(img *Image) (bool, error) {
return img.Created().Before(value), nil
}
}
// filterReadOnly creates a readonly filter for matching the specified value.
func filterReadOnly(value bool) filterFunc {
return func(img *Image) (bool, error) {
return img.IsReadOnly() == value, nil
}
}
// filterContainers creates a container filter for matching the specified value.
func filterContainers(value string, fn IsExternalContainerFunc) filterFunc {
return func(img *Image) (bool, error) {
ctrs, err := img.Containers()
if err != nil {
return false, err
}
if value != "external" {
boolValue := value == "true"
return (len(ctrs) > 0) == boolValue, nil
}
// Check whether all associated containers are external ones.
for _, c := range ctrs {
isExternal, err := fn(c)
if err != nil {
return false, fmt.Errorf("checking if %s is an external container in filter: %w", c, err)
}
if !isExternal {
return isExternal, nil
}
}
return true, nil
}
}
// filterDangling creates a dangling filter for matching the specified value.
func filterDangling(ctx context.Context, value bool, tree *layerTree) filterFunc {
return func(img *Image) (bool, error) {
isDangling, err := img.isDangling(ctx, tree)
if err != nil {
return false, err
}
return isDangling == value, nil
}
}
// filterID creates an image-ID filter for matching the specified value.
func filterID(value string) filterFunc {
return func(img *Image) (bool, error) {
return img.ID() == value, nil
}
}
// filterIntermediate creates an intermediate filter for images. An image is
// considered to be an intermediate image if it is dangling (i.e., no tags) and
// has no children (i.e., no other image depends on it).
func filterIntermediate(ctx context.Context, value bool, tree *layerTree) filterFunc {
return func(img *Image) (bool, error) {
isIntermediate, err := img.isIntermediate(ctx, tree)
if err != nil {
return false, err
}
return isIntermediate == value, nil
}
}