diff --git a/pkg/cloudprovider/providers/gce/cloud/filter/filter.go b/pkg/cloudprovider/providers/gce/cloud/filter/filter.go new file mode 100644 index 00000000000..c08005726c8 --- /dev/null +++ b/pkg/cloudprovider/providers/gce/cloud/filter/filter.go @@ -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) +} diff --git a/pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go b/pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go new file mode 100644 index 00000000000..46b3c279a47 --- /dev/null +++ b/pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go @@ -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) + } + } +}