mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 10:19:50 +00:00
Implementation of the compute "filter" handling for List()
This commit is contained in:
parent
b19149406e
commit
94ddfd17e7
303
pkg/cloudprovider/providers/gce/cloud/filter/filter.go
Normal file
303
pkg/cloudprovider/providers/gce/cloud/filter/filter.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package filter encapsulates the filter argument to compute API calls.
|
||||||
|
//
|
||||||
|
// // List all global addresses (no filter).
|
||||||
|
// c.GlobalAddresses().List(ctx, filter.None)
|
||||||
|
//
|
||||||
|
// // List global addresses filtering for name matching "abc.*".
|
||||||
|
// c.GlobalAddresses().List(ctx, filter.Regexp("name", "abc.*"))
|
||||||
|
//
|
||||||
|
// // List on multiple conditions.
|
||||||
|
// f := filter.Regexp("name", "homer.*").AndNotRegexp("name", "homers")
|
||||||
|
// c.GlobalAddresses().List(ctx, f)
|
||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// None indicates that the List result set should not be filter (i.e.
|
||||||
|
// return all values).
|
||||||
|
None *F
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regexp returns a filter for fieldName matches regexp v.
|
||||||
|
func Regexp(fieldName, v string) *F {
|
||||||
|
return (&F{}).AndRegexp(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRegexp returns a filter for fieldName not matches regexp v.
|
||||||
|
func NotRegexp(fieldName, v string) *F {
|
||||||
|
return (&F{}).AndNotRegexp(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualInt returns a filter for fieldName ~ v.
|
||||||
|
func EqualInt(fieldName string, v int) *F {
|
||||||
|
return (&F{}).AndEqualInt(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqualInt returns a filter for fieldName != v.
|
||||||
|
func NotEqualInt(fieldName string, v int) *F {
|
||||||
|
return (&F{}).AndNotEqualInt(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualBool returns a filter for fieldName == v.
|
||||||
|
func EqualBool(fieldName string, v bool) *F {
|
||||||
|
return (&F{}).AndEqualBool(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEqualBool returns a filter for fieldName != v.
|
||||||
|
func NotEqualBool(fieldName string, v bool) *F {
|
||||||
|
return (&F{}).AndNotEqualBool(fieldName, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F is a filter to be used with List() operations.
|
||||||
|
//
|
||||||
|
// From the compute API description:
|
||||||
|
//
|
||||||
|
// Sets a filter {expression} for filtering listed resources. Your {expression}
|
||||||
|
// must be in the format: field_name comparison_string literal_string.
|
||||||
|
//
|
||||||
|
// The field_name is the name of the field you want to compare. Only atomic field
|
||||||
|
// types are supported (string, number, boolean). The comparison_string must be
|
||||||
|
// either eq (equals) or ne (not equals). The literal_string is the string value
|
||||||
|
// to filter to. The literal value must be valid for the type of field you are
|
||||||
|
// filtering by (string, number, boolean). For string fields, the literal value is
|
||||||
|
// interpreted as a regular expression using RE2 syntax. The literal value must
|
||||||
|
// match the entire field.
|
||||||
|
//
|
||||||
|
// For example, to filter for instances that do not have a name of
|
||||||
|
// example-instance, you would use name ne example-instance.
|
||||||
|
//
|
||||||
|
// You can filter on nested fields. For example, you could filter on instances
|
||||||
|
// that have set the scheduling.automaticRestart field to true. Use filtering on
|
||||||
|
// nested fields to take advantage of labels to organize and search for results
|
||||||
|
// based on label values.
|
||||||
|
//
|
||||||
|
// To filter on multiple expressions, provide each separate expression within
|
||||||
|
// parentheses. For example, (scheduling.automaticRestart eq true)
|
||||||
|
// (zone eq us-central1-f). Multiple expressions are treated as AND expressions,
|
||||||
|
// meaning that resources must match all expressions to pass the filters.
|
||||||
|
type F struct {
|
||||||
|
predicates []filterPredicate
|
||||||
|
}
|
||||||
|
|
||||||
|
// And joins two filters together.
|
||||||
|
func (fl *F) And(rest *F) *F {
|
||||||
|
fl.predicates = append(fl.predicates, rest.predicates...)
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndRegexp adds a field match string predicate.
|
||||||
|
func (fl *F) AndRegexp(fieldName, v string) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, s: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndNotRegexp adds a field not match string predicate.
|
||||||
|
func (fl *F) AndNotRegexp(fieldName, v string) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, s: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndEqualInt adds a field == int predicate.
|
||||||
|
func (fl *F) AndEqualInt(fieldName string, v int) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, i: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndNotEqualInt adds a field != int predicate.
|
||||||
|
func (fl *F) AndNotEqualInt(fieldName string, v int) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, i: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndEqualBool adds a field == bool predicate.
|
||||||
|
func (fl *F) AndEqualBool(fieldName string, v bool) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, b: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndNotEqualBool adds a field != bool predicate.
|
||||||
|
func (fl *F) AndNotEqualBool(fieldName string, v bool) *F {
|
||||||
|
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, b: &v})
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fl *F) String() string {
|
||||||
|
if len(fl.predicates) == 1 {
|
||||||
|
return fl.predicates[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pl []string
|
||||||
|
for _, p := range fl.predicates {
|
||||||
|
pl = append(pl, "("+p.String()+")")
|
||||||
|
}
|
||||||
|
return strings.Join(pl, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns true if the F as specifies matches the given object. This
|
||||||
|
// is used by the Mock implementations to perform filtering and SHOULD NOT be
|
||||||
|
// used in production code as it is not well-tested to be equivalent to the
|
||||||
|
// actual compute API.
|
||||||
|
func (fl *F) Match(obj interface{}) bool {
|
||||||
|
if fl == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, p := range fl.predicates {
|
||||||
|
if !p.match(obj) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type filterOp int
|
||||||
|
|
||||||
|
const (
|
||||||
|
equals filterOp = iota
|
||||||
|
notEquals filterOp = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// filterPredicate is an individual predicate for a fieldName and value.
|
||||||
|
type filterPredicate struct {
|
||||||
|
fieldName string
|
||||||
|
|
||||||
|
op filterOp
|
||||||
|
s *string
|
||||||
|
i *int
|
||||||
|
b *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *filterPredicate) String() string {
|
||||||
|
var op string
|
||||||
|
switch fp.op {
|
||||||
|
case equals:
|
||||||
|
op = "eq"
|
||||||
|
case notEquals:
|
||||||
|
op = "ne"
|
||||||
|
default:
|
||||||
|
op = "invalidOp"
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch {
|
||||||
|
case fp.s != nil:
|
||||||
|
// There does not seem to be any sort of escaping as specified in the
|
||||||
|
// document. This means it's possible to create malformed expressions.
|
||||||
|
value = *fp.s
|
||||||
|
case fp.i != nil:
|
||||||
|
value = fmt.Sprintf("%d", *fp.i)
|
||||||
|
case fp.b != nil:
|
||||||
|
value = fmt.Sprintf("%t", *fp.b)
|
||||||
|
default:
|
||||||
|
value = "invalidValue"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s %s", fp.fieldName, op, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *filterPredicate) match(o interface{}) bool {
|
||||||
|
v, err := extractValue(fp.fieldName, o)
|
||||||
|
glog.V(6).Infof("extractValue(%q, %#v) = %v, %v", fp.fieldName, o, v, err)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var match bool
|
||||||
|
switch x := v.(type) {
|
||||||
|
case string:
|
||||||
|
if fp.s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(*fp.s)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Match regexp %q is invalid: %v", *fp.s, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
match = re.Match([]byte(x))
|
||||||
|
case int:
|
||||||
|
if fp.i == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
match = x == *fp.i
|
||||||
|
case bool:
|
||||||
|
if fp.b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
match = x == *fp.b
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fp.op {
|
||||||
|
case equals:
|
||||||
|
return match
|
||||||
|
case notEquals:
|
||||||
|
return !match
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// snakeToCamelCase converts from "names_like_this" to "NamesLikeThis" to
|
||||||
|
// interoperate between proto and Golang naming conventions.
|
||||||
|
func snakeToCamelCase(s string) string {
|
||||||
|
parts := strings.Split(s, "_")
|
||||||
|
var ret string
|
||||||
|
for _, x := range parts {
|
||||||
|
ret += strings.Title(x)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractValue returns the value of the field named by path in object o if it exists.
|
||||||
|
func extractValue(path string, o interface{}) (interface{}, error) {
|
||||||
|
parts := strings.Split(path, ".")
|
||||||
|
for _, f := range parts {
|
||||||
|
v := reflect.ValueOf(o)
|
||||||
|
// Dereference Ptr to handle *struct.
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
return nil, errors.New("field is nil")
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("cannot get field from non-struct (%T)", o)
|
||||||
|
}
|
||||||
|
v = v.FieldByName(snakeToCamelCase(f))
|
||||||
|
if !v.IsValid() {
|
||||||
|
return nil, fmt.Errorf("cannot get field %q as it is not a valid field in %T", f, o)
|
||||||
|
}
|
||||||
|
if !v.CanInterface() {
|
||||||
|
return nil, fmt.Errorf("cannot get field %q in obj of type %T", f, o)
|
||||||
|
}
|
||||||
|
o = v.Interface()
|
||||||
|
}
|
||||||
|
switch o.(type) {
|
||||||
|
case string, int, bool:
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unhandled object of type %T", o)
|
||||||
|
}
|
176
pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go
Normal file
176
pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilterToString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
f *F
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{Regexp("field1", "abc"), `field1 eq abc`},
|
||||||
|
{NotRegexp("field1", "abc"), `field1 ne abc`},
|
||||||
|
{EqualInt("field1", 13), "field1 eq 13"},
|
||||||
|
{NotEqualInt("field1", 13), "field1 ne 13"},
|
||||||
|
{EqualBool("field1", true), "field1 eq true"},
|
||||||
|
{NotEqualBool("field1", true), "field1 ne true"},
|
||||||
|
{Regexp("field1", "abc").AndRegexp("field2", "def"), `(field1 eq abc) (field2 eq def)`},
|
||||||
|
{Regexp("field1", "abc").AndNotEqualInt("field2", 17), `(field1 eq abc) (field2 ne 17)`},
|
||||||
|
{Regexp("field1", "abc").And(EqualInt("field2", 17)), `(field1 eq abc) (field2 eq 17)`},
|
||||||
|
} {
|
||||||
|
if tc.f.String() != tc.want {
|
||||||
|
t.Errorf("filter %#v String() = %q, want %q", tc.f, tc.f.String(), tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterMatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type inner struct {
|
||||||
|
X string
|
||||||
|
}
|
||||||
|
type S struct {
|
||||||
|
S string
|
||||||
|
I int
|
||||||
|
B bool
|
||||||
|
Unhandled struct{}
|
||||||
|
NestedField *inner
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
f *F
|
||||||
|
o interface{}
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{f: None, o: &S{}, want: true},
|
||||||
|
{f: Regexp("s", "abc"), o: &S{}},
|
||||||
|
{f: EqualInt("i", 10), o: &S{}},
|
||||||
|
{f: EqualBool("b", true), o: &S{}},
|
||||||
|
{f: NotRegexp("s", "abc"), o: &S{}, want: true},
|
||||||
|
{f: NotEqualInt("i", 10), o: &S{}, want: true},
|
||||||
|
{f: NotEqualBool("b", true), o: &S{}, want: true},
|
||||||
|
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{}},
|
||||||
|
{f: Regexp("s", "abc"), o: &S{S: "abc"}, want: true},
|
||||||
|
{f: Regexp("s", "a.*"), o: &S{S: "abc"}, want: true},
|
||||||
|
{f: Regexp("s", "a((("), o: &S{S: "abc"}},
|
||||||
|
{f: NotRegexp("s", "abc"), o: &S{S: "abc"}},
|
||||||
|
{f: EqualInt("i", 10), o: &S{I: 11}},
|
||||||
|
{f: EqualInt("i", 10), o: &S{I: 10}, want: true},
|
||||||
|
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{S: "abc"}},
|
||||||
|
{f: Regexp("s", "abcd").AndEqualBool("b", true), o: &S{S: "abc"}},
|
||||||
|
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{S: "abc", B: true}, want: true},
|
||||||
|
{f: Regexp("s", "abc").And(EqualBool("b", true)), o: &S{S: "abc", B: true}, want: true},
|
||||||
|
{f: Regexp("unhandled", "xyz"), o: &S{}},
|
||||||
|
{f: Regexp("nested_field.x", "xyz"), o: &S{}},
|
||||||
|
{f: Regexp("nested_field.x", "xyz"), o: &S{NestedField: &inner{"xyz"}}, want: true},
|
||||||
|
{f: NotRegexp("nested_field.x", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
|
||||||
|
{f: Regexp("nested_field.y", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
|
||||||
|
{f: Regexp("nested_field", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
|
||||||
|
} {
|
||||||
|
got := tc.f.Match(tc.o)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("%v: Match(%+v) = %v, want %v", tc.f, tc.o, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterSnakeToCamelCase(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
s string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"abc", "Abc"},
|
||||||
|
{"_foo", "Foo"},
|
||||||
|
{"a_b_c", "ABC"},
|
||||||
|
{"a_BC_def", "ABCDef"},
|
||||||
|
{"a_Bc_def", "ABcDef"},
|
||||||
|
} {
|
||||||
|
got := snakeToCamelCase(tc.s)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("snakeToCamelCase(%q) = %q, want %q", tc.s, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterExtractValue(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type nest2 struct {
|
||||||
|
Y string
|
||||||
|
}
|
||||||
|
type nest struct {
|
||||||
|
X string
|
||||||
|
Nest2 nest2
|
||||||
|
}
|
||||||
|
st := &struct {
|
||||||
|
S string
|
||||||
|
I int
|
||||||
|
F bool
|
||||||
|
Nest nest
|
||||||
|
NestPtr *nest
|
||||||
|
|
||||||
|
Unhandled float64
|
||||||
|
}{
|
||||||
|
"abc",
|
||||||
|
13,
|
||||||
|
true,
|
||||||
|
nest{"xyz", nest2{"zzz"}},
|
||||||
|
&nest{"yyy", nest2{}},
|
||||||
|
0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
path string
|
||||||
|
o interface{}
|
||||||
|
want interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{path: "s", o: st, want: "abc"},
|
||||||
|
{path: "i", o: st, want: 13},
|
||||||
|
{path: "f", o: st, want: true},
|
||||||
|
{path: "nest.x", o: st, want: "xyz"},
|
||||||
|
{path: "nest_ptr.x", o: st, want: "yyy"},
|
||||||
|
// Error cases.
|
||||||
|
{path: "", o: st, wantErr: true},
|
||||||
|
{path: "no_such_field", o: st, wantErr: true},
|
||||||
|
{path: "s.invalid_type", o: st, wantErr: true},
|
||||||
|
{path: "unhandled", o: st, wantErr: true},
|
||||||
|
{path: "nest.x", o: &struct{ Nest *nest }{}, wantErr: true},
|
||||||
|
} {
|
||||||
|
o, err := extractValue(tc.path, tc.o)
|
||||||
|
gotErr := err != nil
|
||||||
|
if gotErr != tc.wantErr {
|
||||||
|
t.Errorf("extractValue(%v, %+v) = %v, %v; gotErr = %v, tc.wantErr = %v", tc.path, tc.o, o, err, gotErr, tc.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(o, tc.want) {
|
||||||
|
t.Errorf("extractValue(%v, %+v) = %v, nil; want %v, nil", tc.path, tc.o, o, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user