Cleanup filter package.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
945d80cd6a
commit
065118390a
|
@ -0,0 +1,24 @@
|
||||||
|
package filters
|
||||||
|
|
||||||
|
func ExampleArgs_MatchKVList() {
|
||||||
|
args := NewArgs(
|
||||||
|
Arg("label", "image=foo"),
|
||||||
|
Arg("label", "state=running"))
|
||||||
|
|
||||||
|
// returns true because there are no values for bogus
|
||||||
|
args.MatchKVList("bogus", nil)
|
||||||
|
|
||||||
|
// returns false because there are no sources
|
||||||
|
args.MatchKVList("label", nil)
|
||||||
|
|
||||||
|
// returns true because all sources are matched
|
||||||
|
args.MatchKVList("label", map[string]string{
|
||||||
|
"image": "foo",
|
||||||
|
"state": "running",
|
||||||
|
})
|
||||||
|
|
||||||
|
// returns false because the values do not match
|
||||||
|
args.MatchKVList("label", map[string]string{
|
||||||
|
"image": "other",
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
// Package filters provides helper function to parse and handle command line
|
/*Package filters provides tools for encoding a mapping of keys to a set of
|
||||||
// filter, used for example in docker ps or docker images commands.
|
multiple values.
|
||||||
|
*/
|
||||||
package filters
|
package filters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -11,27 +12,34 @@ import (
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Args stores filter arguments as map key:{map key: bool}.
|
// Args stores a mapping of keys to a set of multiple values.
|
||||||
// It contains an aggregation of the map of arguments (which are in the form
|
|
||||||
// of -f 'key=value') based on the key, and stores values for the same key
|
|
||||||
// in a map with string keys and boolean values.
|
|
||||||
// e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu'
|
|
||||||
// the args will be {"image.name":{"ubuntu":true},"label":{"label1=1":true,"label2=2":true}}
|
|
||||||
type Args struct {
|
type Args struct {
|
||||||
fields map[string]map[string]bool
|
fields map[string]map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewArgs initializes a new Args struct.
|
// KeyValuePair are used to initialize a new Args
|
||||||
func NewArgs() Args {
|
type KeyValuePair struct {
|
||||||
return Args{fields: map[string]map[string]bool{}}
|
Key string
|
||||||
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFlag parses the argument to the filter flag. Like
|
// Arg creates a new KeyValuePair for initializing Args
|
||||||
|
func Arg(key, value string) KeyValuePair {
|
||||||
|
return KeyValuePair{Key: key, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArgs returns a new Args populated with the initial args
|
||||||
|
func NewArgs(initialArgs ...KeyValuePair) Args {
|
||||||
|
args := Args{fields: map[string]map[string]bool{}}
|
||||||
|
for _, arg := range initialArgs {
|
||||||
|
args.Add(arg.Key, arg.Value)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFlag parses a key=value string and adds it to an Args.
|
||||||
//
|
//
|
||||||
// `docker ps -f 'created=today' -f 'image.name=ubuntu*'`
|
// Deprecated: Use Args.Add()
|
||||||
//
|
|
||||||
// If prev map is provided, then it is appended to, and returned. By default a new
|
|
||||||
// map is created.
|
|
||||||
func ParseFlag(arg string, prev Args) (Args, error) {
|
func ParseFlag(arg string, prev Args) (Args, error) {
|
||||||
filters := prev
|
filters := prev
|
||||||
if len(arg) == 0 {
|
if len(arg) == 0 {
|
||||||
|
@ -52,74 +60,95 @@ func ParseFlag(arg string, prev Args) (Args, error) {
|
||||||
return filters, nil
|
return filters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBadFormat is an error returned in case of bad format for a filter.
|
// ErrBadFormat is an error returned when a filter is not in the form key=value
|
||||||
|
//
|
||||||
|
// Deprecated: this error will be removed in a future version
|
||||||
var ErrBadFormat = errors.New("bad format of filter (expected name=value)")
|
var ErrBadFormat = errors.New("bad format of filter (expected name=value)")
|
||||||
|
|
||||||
// ToParam packs the Args into a string for easy transport from client to server.
|
// ToParam encodes the Args as args JSON encoded string
|
||||||
|
//
|
||||||
|
// Deprecated: use ToJSON
|
||||||
func ToParam(a Args) (string, error) {
|
func ToParam(a Args) (string, error) {
|
||||||
// this way we don't URL encode {}, just empty space
|
return ToJSON(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON returns a JSON byte representation of the Args
|
||||||
|
func (args Args) MarshalJSON() ([]byte, error) {
|
||||||
|
if len(args.fields) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
return json.Marshal(args.fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON returns the Args as a JSON encoded string
|
||||||
|
func ToJSON(a Args) (string, error) {
|
||||||
if a.Len() == 0 {
|
if a.Len() == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
buf, err := json.Marshal(a)
|
||||||
buf, err := json.Marshal(a.fields)
|
return string(buf), err
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(buf), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToParamWithVersion packs the Args into a string for easy transport from client to server.
|
// ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22
|
||||||
// The generated string will depend on the specified version (corresponding to the API version).
|
// then the encoded format will use an older legacy format where the values are a
|
||||||
|
// list of strings, instead of a set.
|
||||||
|
//
|
||||||
|
// Deprecated: Use ToJSON
|
||||||
func ToParamWithVersion(version string, a Args) (string, error) {
|
func ToParamWithVersion(version string, a Args) (string, error) {
|
||||||
// this way we don't URL encode {}, just empty space
|
|
||||||
if a.Len() == 0 {
|
if a.Len() == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for daemons older than v1.10, filter must be of the form map[string][]string
|
|
||||||
var buf []byte
|
|
||||||
var err error
|
|
||||||
if version != "" && versions.LessThan(version, "1.22") {
|
if version != "" && versions.LessThan(version, "1.22") {
|
||||||
buf, err = json.Marshal(convertArgsToSlice(a.fields))
|
buf, err := json.Marshal(convertArgsToSlice(a.fields))
|
||||||
} else {
|
return string(buf), err
|
||||||
buf, err = json.Marshal(a.fields)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return ToJSON(a)
|
||||||
}
|
|
||||||
return string(buf), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromParam unpacks the filter Args.
|
// FromParam decodes a JSON encoded string into Args
|
||||||
|
//
|
||||||
|
// Deprecated: use FromJSON
|
||||||
func FromParam(p string) (Args, error) {
|
func FromParam(p string) (Args, error) {
|
||||||
if len(p) == 0 {
|
return FromJSON(p)
|
||||||
return NewArgs(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r := strings.NewReader(p)
|
|
||||||
d := json.NewDecoder(r)
|
|
||||||
|
|
||||||
m := map[string]map[string]bool{}
|
|
||||||
if err := d.Decode(&m); err != nil {
|
|
||||||
r.Seek(0, 0)
|
|
||||||
|
|
||||||
// Allow parsing old arguments in slice format.
|
|
||||||
// Because other libraries might be sending them in this format.
|
|
||||||
deprecated := map[string][]string{}
|
|
||||||
if deprecatedErr := d.Decode(&deprecated); deprecatedErr == nil {
|
|
||||||
m = deprecatedArgs(deprecated)
|
|
||||||
} else {
|
|
||||||
return NewArgs(), err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Args{m}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the list of values associates with a field.
|
// FromJSON decodes a JSON encoded string into Args
|
||||||
// It returns a slice of strings to keep backwards compatibility with old code.
|
func FromJSON(p string) (Args, error) {
|
||||||
func (filters Args) Get(field string) []string {
|
args := NewArgs()
|
||||||
values := filters.fields[field]
|
|
||||||
|
if p == "" {
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := []byte(p)
|
||||||
|
err := json.Unmarshal(raw, &args)
|
||||||
|
if err == nil {
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to parsing arguments in the legacy slice format
|
||||||
|
deprecated := map[string][]string{}
|
||||||
|
if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil {
|
||||||
|
return args, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args.fields = deprecatedArgs(deprecated)
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON populates the Args from JSON encode bytes
|
||||||
|
func (args Args) UnmarshalJSON(raw []byte) error {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(raw, &args.fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the list of values associated with the key
|
||||||
|
func (args Args) Get(key string) []string {
|
||||||
|
values := args.fields[key]
|
||||||
if values == nil {
|
if values == nil {
|
||||||
return make([]string, 0)
|
return make([]string, 0)
|
||||||
}
|
}
|
||||||
|
@ -130,37 +159,34 @@ func (filters Args) Get(field string) []string {
|
||||||
return slice
|
return slice
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a new value to a filter field.
|
// Add a new value to the set of values
|
||||||
func (filters Args) Add(name, value string) {
|
func (args Args) Add(key, value string) {
|
||||||
if _, ok := filters.fields[name]; ok {
|
if _, ok := args.fields[key]; ok {
|
||||||
filters.fields[name][value] = true
|
args.fields[key][value] = true
|
||||||
} else {
|
} else {
|
||||||
filters.fields[name] = map[string]bool{value: true}
|
args.fields[key] = map[string]bool{value: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Del removes a value from a filter field.
|
// Del removes a value from the set
|
||||||
func (filters Args) Del(name, value string) {
|
func (args Args) Del(key, value string) {
|
||||||
if _, ok := filters.fields[name]; ok {
|
if _, ok := args.fields[key]; ok {
|
||||||
delete(filters.fields[name], value)
|
delete(args.fields[key], value)
|
||||||
if len(filters.fields[name]) == 0 {
|
if len(args.fields[key]) == 0 {
|
||||||
delete(filters.fields, name)
|
delete(args.fields, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len returns the number of fields in the arguments.
|
// Len returns the number of keys in the mapping
|
||||||
func (filters Args) Len() int {
|
func (args Args) Len() int {
|
||||||
return len(filters.fields)
|
return len(args.fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchKVList returns true if the values for the specified field matches the ones
|
// MatchKVList returns true if all the pairs in sources exist as key=value
|
||||||
// from the sources.
|
// pairs in the mapping at key, or if there are no values at key.
|
||||||
// e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}},
|
func (args Args) MatchKVList(key string, sources map[string]string) bool {
|
||||||
// field is 'label' and sources are {'label1': '1', 'label2': '2'}
|
fieldValues := args.fields[key]
|
||||||
// it returns true.
|
|
||||||
func (filters Args) MatchKVList(field string, sources map[string]string) bool {
|
|
||||||
fieldValues := filters.fields[field]
|
|
||||||
|
|
||||||
//do not filter if there is no filter set or cannot determine filter
|
//do not filter if there is no filter set or cannot determine filter
|
||||||
if len(fieldValues) == 0 {
|
if len(fieldValues) == 0 {
|
||||||
|
@ -171,8 +197,8 @@ func (filters Args) MatchKVList(field string, sources map[string]string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for name2match := range fieldValues {
|
for value := range fieldValues {
|
||||||
testKV := strings.SplitN(name2match, "=", 2)
|
testKV := strings.SplitN(value, "=", 2)
|
||||||
|
|
||||||
v, ok := sources[testKV[0]]
|
v, ok := sources[testKV[0]]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -186,16 +212,13 @@ func (filters Args) MatchKVList(field string, sources map[string]string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match returns true if the values for the specified field matches the source string
|
// Match returns true if any of the values at key match the source string
|
||||||
// e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}},
|
func (args Args) Match(field, source string) bool {
|
||||||
// field is 'image.name' and source is 'ubuntu'
|
if args.ExactMatch(field, source) {
|
||||||
// it returns true.
|
|
||||||
func (filters Args) Match(field, source string) bool {
|
|
||||||
if filters.ExactMatch(field, source) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValues := filters.fields[field]
|
fieldValues := args.fields[field]
|
||||||
for name2match := range fieldValues {
|
for name2match := range fieldValues {
|
||||||
match, err := regexp.MatchString(name2match, source)
|
match, err := regexp.MatchString(name2match, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,9 +231,9 @@ func (filters Args) Match(field, source string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExactMatch returns true if the source matches exactly one of the filters.
|
// ExactMatch returns true if the source matches exactly one of the values.
|
||||||
func (filters Args) ExactMatch(field, source string) bool {
|
func (args Args) ExactMatch(key, source string) bool {
|
||||||
fieldValues, ok := filters.fields[field]
|
fieldValues, ok := args.fields[key]
|
||||||
//do not filter if there is no filter set or cannot determine filter
|
//do not filter if there is no filter set or cannot determine filter
|
||||||
if !ok || len(fieldValues) == 0 {
|
if !ok || len(fieldValues) == 0 {
|
||||||
return true
|
return true
|
||||||
|
@ -220,14 +243,15 @@ func (filters Args) ExactMatch(field, source string) bool {
|
||||||
return fieldValues[source]
|
return fieldValues[source]
|
||||||
}
|
}
|
||||||
|
|
||||||
// UniqueExactMatch returns true if there is only one filter and the source matches exactly this one.
|
// UniqueExactMatch returns true if there is only one value and the source
|
||||||
func (filters Args) UniqueExactMatch(field, source string) bool {
|
// matches exactly the value.
|
||||||
fieldValues := filters.fields[field]
|
func (args Args) UniqueExactMatch(key, source string) bool {
|
||||||
|
fieldValues := args.fields[key]
|
||||||
//do not filter if there is no filter set or cannot determine filter
|
//do not filter if there is no filter set or cannot determine filter
|
||||||
if len(fieldValues) == 0 {
|
if len(fieldValues) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(filters.fields[field]) != 1 {
|
if len(args.fields[key]) != 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,14 +259,14 @@ func (filters Args) UniqueExactMatch(field, source string) bool {
|
||||||
return fieldValues[source]
|
return fieldValues[source]
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzyMatch returns true if the source matches exactly one of the filters,
|
// FuzzyMatch returns true if the source matches exactly one value, or the
|
||||||
// or the source has one of the filters as a prefix.
|
// source has one of the values as a prefix.
|
||||||
func (filters Args) FuzzyMatch(field, source string) bool {
|
func (args Args) FuzzyMatch(key, source string) bool {
|
||||||
if filters.ExactMatch(field, source) {
|
if args.ExactMatch(key, source) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValues := filters.fields[field]
|
fieldValues := args.fields[key]
|
||||||
for prefix := range fieldValues {
|
for prefix := range fieldValues {
|
||||||
if strings.HasPrefix(source, prefix) {
|
if strings.HasPrefix(source, prefix) {
|
||||||
return true
|
return true
|
||||||
|
@ -251,9 +275,17 @@ func (filters Args) FuzzyMatch(field, source string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include returns true if the name of the field to filter is in the filters.
|
// Include returns true if the key exists in the mapping
|
||||||
func (filters Args) Include(field string) bool {
|
//
|
||||||
_, ok := filters.fields[field]
|
// Deprecated: use Contains
|
||||||
|
func (args Args) Include(field string) bool {
|
||||||
|
_, ok := args.fields[field]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the key exists in the mapping
|
||||||
|
func (args Args) Contains(field string) bool {
|
||||||
|
_, ok := args.fields[field]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,10 +297,10 @@ func (e invalidFilter) Error() string {
|
||||||
|
|
||||||
func (invalidFilter) InvalidParameter() {}
|
func (invalidFilter) InvalidParameter() {}
|
||||||
|
|
||||||
// Validate ensures that all the fields in the filter are valid.
|
// Validate compared the set of accepted keys against the keys in the mapping.
|
||||||
// It returns an error as soon as it finds an invalid field.
|
// An error is returned if any mapping keys are not in the accepted set.
|
||||||
func (filters Args) Validate(accepted map[string]bool) error {
|
func (args Args) Validate(accepted map[string]bool) error {
|
||||||
for name := range filters.fields {
|
for name := range args.fields {
|
||||||
if !accepted[name] {
|
if !accepted[name] {
|
||||||
return invalidFilter(name)
|
return invalidFilter(name)
|
||||||
}
|
}
|
||||||
|
@ -276,13 +308,14 @@ func (filters Args) Validate(accepted map[string]bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalkValues iterates over the list of filtered values for a field.
|
// WalkValues iterates over the list of values for a key in the mapping and calls
|
||||||
// It stops the iteration if it finds an error and it returns that error.
|
// op() for each value. If op returns an error the iteration stops and the
|
||||||
func (filters Args) WalkValues(field string, op func(value string) error) error {
|
// error is returned.
|
||||||
if _, ok := filters.fields[field]; !ok {
|
func (args Args) WalkValues(field string, op func(value string) error) error {
|
||||||
|
if _, ok := args.fields[field]; !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for v := range filters.fields[field] {
|
for v := range args.fields[field] {
|
||||||
if err := op(v); err != nil {
|
if err := op(v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ package filters
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseArgs(t *testing.T) {
|
func TestParseArgs(t *testing.T) {
|
||||||
|
@ -16,23 +19,18 @@ func TestParseArgs(t *testing.T) {
|
||||||
args = NewArgs()
|
args = NewArgs()
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
for i := range flagArgs {
|
for i := range flagArgs {
|
||||||
args, err = ParseFlag(flagArgs[i], args)
|
args, err = ParseFlag(flagArgs[i], args)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Errorf("failed to parse %s: %s", flagArgs[i], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(args.Get("created")) != 1 {
|
|
||||||
t.Error("failed to set this arg")
|
|
||||||
}
|
|
||||||
if len(args.Get("image.name")) != 2 {
|
|
||||||
t.Error("the args should have collapsed")
|
|
||||||
}
|
}
|
||||||
|
assert.Len(t, args.Get("created"), 1)
|
||||||
|
assert.Len(t, args.Get("image.name"), 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsEdgeCase(t *testing.T) {
|
func TestParseArgsEdgeCase(t *testing.T) {
|
||||||
var filters Args
|
var args Args
|
||||||
args, err := ParseFlag("", filters)
|
args, err := ParseFlag("", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -233,9 +231,8 @@ func TestArgsMatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for args, field := range matches {
|
for args, field := range matches {
|
||||||
if args.Match(field, source) != true {
|
assert.True(t, args.Match(field, source),
|
||||||
t.Fatalf("Expected true for %v on %v, got false", source, args)
|
"Expected field %s to match %s", field, source)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
differs := map[*Args]string{
|
differs := map[*Args]string{
|
||||||
|
@ -258,9 +255,8 @@ func TestArgsMatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for args, field := range differs {
|
for args, field := range differs {
|
||||||
if args.Match(field, source) != false {
|
assert.False(t, args.Match(field, source),
|
||||||
t.Fatalf("Expected false for %v on %v, got true", source, args)
|
"Expected field %s to not match %s", field, source)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue