mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +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