| /* |
| Copyright 2018 Google LLC |
| |
| 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 |
| |
| https://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" |
| |
| "k8s.io/klog" |
| ) |
| |
| 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 ~ regexp v. |
| func Regexp(fieldName, v string) *F { |
| return (&F{}).AndRegexp(fieldName, v) |
| } |
| |
| // NotRegexp returns a filter for fieldName !~ 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 |
| } |
| |
| // TODO(rramkumar): Support logical OR |
| |
| // 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 ~ string predicate. |
| func (fl *F) AndRegexp(fieldName, v string) *F { |
| fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: regexpEquals, s: &v}) |
| return fl |
| } |
| |
| // AndNotRegexp adds a field !~ string predicate. |
| func (fl *F) AndNotRegexp(fieldName, v string) *F { |
| fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: regexpNotEquals, 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 fmt.Sprintf("%q", 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 ( |
| regexpEquals filterOp = iota |
| regexpNotEquals filterOp = iota |
| 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 regexpEquals: |
| op = "~" |
| case regexpNotEquals: |
| op = "!~" |
| case equals: |
| op = "=" |
| case notEquals: |
| op = "!=" |
| 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) |
| klog.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 { |
| klog.Errorf("Match regexp %q is invalid: %v", *fp.s, err) |
| return false |
| } |
| match = x == *fp.s |
| if fp.op < regexpNotEquals { |
| 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 regexpEquals: |
| return match |
| case regexpNotEquals: |
| return !match |
| 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) |
| } |