forked from rancher/steve
-
Notifications
You must be signed in to change notification settings - Fork 0
/
processor.go
424 lines (388 loc) · 11.5 KB
/
processor.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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
// Package listprocessor contains methods for filtering, sorting, and paginating lists of objects.
package listprocessor
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/wrangler/v2/pkg/data"
"github.com/rancher/wrangler/v2/pkg/data/convert"
corecontrollers "github.com/rancher/wrangler/v2/pkg/generated/controllers/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
defaultLimit = 100000
continueParam = "continue"
limitParam = "limit"
filterParam = "filter"
sortParam = "sort"
pageSizeParam = "pagesize"
pageParam = "page"
revisionParam = "revision"
projectsOrNamespacesVar = "projectsornamespaces"
projectIDFieldLabel = "field.cattle.io/projectId"
orOp = ","
notOp = "!"
)
var opReg = regexp.MustCompile(`[!]?=`)
type op string
const (
eq op = ""
notEq op = "!="
)
// ListOptions represents the query parameters that may be included in a list request.
type ListOptions struct {
ChunkSize int
Resume string
Filters []OrFilter
Sort Sort
Pagination Pagination
Revision string
ProjectsOrNamespaces ProjectsOrNamespacesFilter
}
// Filter represents a field to filter by.
// A subfield in an object is represented in a request query using . notation, e.g. 'metadata.name'.
// The subfield is internally represented as a slice, e.g. [metadata, name].
type Filter struct {
field []string
match string
op op
}
// String returns the filter as a query string.
func (f Filter) String() string {
field := strings.Join(f.field, ".")
return field + "=" + f.match
}
// OrFilter represents a set of possible fields to filter by, where an item may match any filter in the set to be included in the result.
type OrFilter struct {
filters []Filter
}
// String returns the filter as a query string.
func (f OrFilter) String() string {
var fields strings.Builder
for i, field := range f.filters {
fields.WriteString(strings.Join(field.field, "."))
fields.WriteByte('=')
fields.WriteString(field.match)
if i < len(f.filters)-1 {
fields.WriteByte(',')
}
}
return fields.String()
}
// SortOrder represents whether the list should be ascending or descending.
type SortOrder int
const (
// ASC stands for ascending order.
ASC SortOrder = iota
// DESC stands for descending (reverse) order.
DESC
)
// Sort represents the criteria to sort on.
// The subfield to sort by is represented in a request query using . notation, e.g. 'metadata.name'.
// The subfield is internally represented as a slice, e.g. [metadata, name].
// The order is represented by prefixing the sort key by '-', e.g. sort=-metadata.name.
type Sort struct {
primaryField []string
secondaryField []string
primaryOrder SortOrder
secondaryOrder SortOrder
}
// String returns the sort parameters as a query string.
func (s Sort) String() string {
field := ""
if s.primaryOrder == DESC {
field = "-" + field
}
field += strings.Join(s.primaryField, ".")
if len(s.secondaryField) > 0 {
field += ","
if s.secondaryOrder == DESC {
field += "-"
}
field += strings.Join(s.secondaryField, ".")
}
return field
}
// Pagination represents how to return paginated results.
type Pagination struct {
pageSize int
page int
}
// PageSize returns the integer page size.
func (p Pagination) PageSize() int {
return p.pageSize
}
type ProjectsOrNamespacesFilter struct {
filter map[string]struct{}
op op
}
// ParseQuery parses the query params of a request and returns a ListOptions.
func ParseQuery(apiOp *types.APIRequest) *ListOptions {
opts := ListOptions{}
opts.ChunkSize = getLimit(apiOp)
q := apiOp.Request.URL.Query()
cont := q.Get(continueParam)
opts.Resume = cont
filterParams := q[filterParam]
filterOpts := []OrFilter{}
for _, filters := range filterParams {
orFilters := strings.Split(filters, orOp)
orFilter := OrFilter{}
for _, filter := range orFilters {
var op op
if strings.Contains(filter, "!=") {
op = "!="
}
filter := opReg.Split(filter, -1)
if len(filter) != 2 {
continue
}
orFilter.filters = append(orFilter.filters, Filter{field: strings.Split(filter[0], "."), match: filter[1], op: op})
}
filterOpts = append(filterOpts, orFilter)
}
// sort the filter fields so they can be used as a cache key in the store
for _, orFilter := range filterOpts {
sort.Slice(orFilter.filters, func(i, j int) bool {
fieldI := strings.Join(orFilter.filters[i].field, ".")
fieldJ := strings.Join(orFilter.filters[j].field, ".")
return fieldI < fieldJ
})
}
sort.Slice(filterOpts, func(i, j int) bool {
var fieldI, fieldJ strings.Builder
for _, f := range filterOpts[i].filters {
fieldI.WriteString(strings.Join(f.field, "."))
}
for _, f := range filterOpts[j].filters {
fieldJ.WriteString(strings.Join(f.field, "."))
}
return fieldI.String() < fieldJ.String()
})
opts.Filters = filterOpts
sortOpts := Sort{}
sortKeys := q.Get(sortParam)
if sortKeys != "" {
sortParts := strings.SplitN(sortKeys, ",", 2)
primaryField := sortParts[0]
if primaryField != "" && primaryField[0] == '-' {
sortOpts.primaryOrder = DESC
primaryField = primaryField[1:]
}
if primaryField != "" {
sortOpts.primaryField = strings.Split(primaryField, ".")
}
if len(sortParts) > 1 {
secondaryField := sortParts[1]
if secondaryField != "" && secondaryField[0] == '-' {
sortOpts.secondaryOrder = DESC
secondaryField = secondaryField[1:]
}
if secondaryField != "" {
sortOpts.secondaryField = strings.Split(secondaryField, ".")
}
}
}
opts.Sort = sortOpts
var err error
pagination := Pagination{}
pagination.pageSize, err = strconv.Atoi(q.Get(pageSizeParam))
if err != nil {
pagination.pageSize = 0
}
pagination.page, err = strconv.Atoi(q.Get(pageParam))
if err != nil {
pagination.page = 1
}
opts.Pagination = pagination
revision := q.Get(revisionParam)
opts.Revision = revision
projectsOptions := ProjectsOrNamespacesFilter{}
var op op
projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
if projectsOrNamespaces == "" {
projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" {
op = notEq
}
}
if projectsOrNamespaces != "" {
projectsOptions.filter = make(map[string]struct{})
for _, pn := range strings.Split(projectsOrNamespaces, ",") {
projectsOptions.filter[pn] = struct{}{}
}
projectsOptions.op = op
opts.ProjectsOrNamespaces = projectsOptions
}
return &opts
}
// getLimit extracts the limit parameter from the request or sets a default of 100000.
// The default limit can be explicitly disabled by setting it to zero or negative.
// If the default is accepted, clients must be aware that the list may be incomplete, and use the "continue" token to get the next chunk of results.
func getLimit(apiOp *types.APIRequest) int {
limitString := apiOp.Request.URL.Query().Get(limitParam)
limit, err := strconv.Atoi(limitString)
if err != nil {
limit = defaultLimit
}
return limit
}
// FilterList accepts a channel of unstructured objects and a slice of filters and returns the filtered list.
// Filters are ANDed together.
func FilterList(list <-chan []unstructured.Unstructured, filters []OrFilter) []unstructured.Unstructured {
result := []unstructured.Unstructured{}
for items := range list {
for _, item := range items {
if len(filters) == 0 {
result = append(result, item)
continue
}
if matchesAll(item.Object, filters) {
result = append(result, item)
}
}
}
return result
}
func matchesOne(obj map[string]interface{}, filter Filter) bool {
var objValue interface{}
var ok bool
subField := []string{}
for !ok && len(filter.field) > 0 {
objValue, ok = data.GetValue(obj, filter.field...)
if !ok {
subField = append(subField, filter.field[len(filter.field)-1])
filter.field = filter.field[:len(filter.field)-1]
}
}
if !ok {
return false
}
switch typedVal := objValue.(type) {
case string, int, bool:
if len(subField) > 0 {
return false
}
stringVal := convert.ToString(typedVal)
if strings.Contains(stringVal, filter.match) {
return true
}
case []interface{}:
filter = Filter{field: subField, match: filter.match, op: filter.op}
if matchesOneInList(typedVal, filter) {
return true
}
}
return false
}
func matchesOneInList(obj []interface{}, filter Filter) bool {
for _, v := range obj {
switch typedItem := v.(type) {
case string, int, bool:
stringVal := convert.ToString(typedItem)
if strings.Contains(stringVal, filter.match) {
return true
}
case map[string]interface{}:
if matchesOne(typedItem, filter) {
return true
}
case []interface{}:
if matchesOneInList(typedItem, filter) {
return true
}
}
}
return false
}
func matchesAny(obj map[string]interface{}, filter OrFilter) bool {
for _, f := range filter.filters {
matches := matchesOne(obj, f)
if (matches && f.op == eq) || (!matches && f.op == notEq) {
return true
}
}
return false
}
func matchesAll(obj map[string]interface{}, filters []OrFilter) bool {
for _, f := range filters {
if !matchesAny(obj, f) {
return false
}
}
return true
}
// SortList sorts the slice by the provided sort criteria.
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
if len(s.primaryField) == 0 {
return list
}
sort.Slice(list, func(i, j int) bool {
leftPrime := convert.ToString(data.GetValueN(list[i].Object, s.primaryField...))
rightPrime := convert.ToString(data.GetValueN(list[j].Object, s.primaryField...))
if leftPrime == rightPrime && len(s.secondaryField) > 0 {
leftSecond := convert.ToString(data.GetValueN(list[i].Object, s.secondaryField...))
rightSecond := convert.ToString(data.GetValueN(list[j].Object, s.secondaryField...))
if s.secondaryOrder == ASC {
return leftSecond < rightSecond
}
return rightSecond < leftSecond
}
if s.primaryOrder == ASC {
return leftPrime < rightPrime
}
return rightPrime < leftPrime
})
return list
}
// PaginateList returns a subset of the result based on the pagination criteria as well as the total number of pages the caller can expect.
func PaginateList(list []unstructured.Unstructured, p Pagination) ([]unstructured.Unstructured, int) {
if p.pageSize <= 0 {
return list, 0
}
page := p.page - 1
if p.page < 1 {
page = 0
}
pages := len(list) / p.pageSize
if len(list)%p.pageSize != 0 {
pages++
}
offset := p.pageSize * page
if offset > len(list) {
return []unstructured.Unstructured{}, pages
}
if offset+p.pageSize > len(list) {
return list[offset:], pages
}
return list[offset : offset+p.pageSize], pages
}
func FilterByProjectsAndNamespaces(list []unstructured.Unstructured, projectsOrNamespaces ProjectsOrNamespacesFilter, namespaceCache corecontrollers.NamespaceCache) []unstructured.Unstructured {
if len(projectsOrNamespaces.filter) == 0 {
return list
}
result := []unstructured.Unstructured{}
for _, obj := range list {
namespaceName := obj.GetNamespace()
if namespaceName == "" {
continue
}
namespace, err := namespaceCache.Get(namespaceName)
if namespace == nil || err != nil {
continue
}
projectLabel, _ := namespace.GetLabels()[projectIDFieldLabel]
_, matchesProject := projectsOrNamespaces.filter[projectLabel]
_, matchesNamespace := projectsOrNamespaces.filter[namespaceName]
matches := matchesProject || matchesNamespace
if projectsOrNamespaces.op == eq && matches {
result = append(result, obj)
}
if projectsOrNamespaces.op == notEq && !matches {
result = append(result, obj)
}
}
return result
}