forked from kubernetes/kubernetes
-
Notifications
You must be signed in to change notification settings - Fork 1
/
filter.go
303 lines (266 loc) · 8.26 KB
/
filter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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)
}