@@ -30,13 +30,16 @@ import (
30
30
"k8s.io/apimachinery/pkg/api/meta"
31
31
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32
32
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33
+ "k8s.io/apimachinery/pkg/fields"
34
+ "k8s.io/apimachinery/pkg/labels"
33
35
"k8s.io/apimachinery/pkg/runtime"
34
36
"k8s.io/apimachinery/pkg/runtime/schema"
35
37
utilrand "k8s.io/apimachinery/pkg/util/rand"
36
38
"k8s.io/apimachinery/pkg/util/validation/field"
37
39
"k8s.io/apimachinery/pkg/watch"
38
40
"k8s.io/client-go/kubernetes/scheme"
39
41
"k8s.io/client-go/testing"
42
+ "sigs.k8s.io/controller-runtime/pkg/internal/field/selector"
40
43
41
44
"sigs.k8s.io/controller-runtime/pkg/client"
42
45
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -49,9 +52,14 @@ type versionedTracker struct {
49
52
}
50
53
51
54
type fakeClient struct {
52
- tracker versionedTracker
53
- scheme * runtime.Scheme
54
- restMapper meta.RESTMapper
55
+ tracker versionedTracker
56
+ scheme * runtime.Scheme
57
+ restMapper meta.RESTMapper
58
+
59
+ // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
60
+ // The inner map maps from index name to IndexerFunc.
61
+ indexes map [schema.GroupVersionKind ]map [string ]client.IndexerFunc
62
+
55
63
schemeWriteLock sync.Mutex
56
64
}
57
65
@@ -93,6 +101,10 @@ type ClientBuilder struct {
93
101
initLists []client.ObjectList
94
102
initRuntimeObjects []runtime.Object
95
103
objectTracker testing.ObjectTracker
104
+
105
+ // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
106
+ // The inner map maps from index name to IndexerFunc.
107
+ indexes map [schema.GroupVersionKind ]map [string ]client.IndexerFunc
96
108
}
97
109
98
110
// WithScheme sets this builder's internal scheme.
@@ -135,6 +147,44 @@ func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuild
135
147
return f
136
148
}
137
149
150
+ // WithIndex can be optionally used to register an index with name `field` and indexer `extractValue`
151
+ // for API objects of the same GroupVersionKind (GVK) as `obj` in the fake client.
152
+ // It can be invoked multiple times, both with objects of the same GVK or different ones.
153
+ // Invoking WithIndex twice with the same `field` and GVK (via `obj`) arguments will panic.
154
+ // WithIndex retrieves the GVK of `obj` using the scheme registered via WithScheme if
155
+ // WithScheme was previously invoked, the default scheme otherwise.
156
+ func (f * ClientBuilder ) WithIndex (obj runtime.Object , field string , extractValue client.IndexerFunc ) * ClientBuilder {
157
+ objScheme := f .scheme
158
+ if objScheme == nil {
159
+ objScheme = scheme .Scheme
160
+ }
161
+
162
+ gvk , err := apiutil .GVKForObject (obj , objScheme )
163
+ if err != nil {
164
+ panic (err )
165
+ }
166
+
167
+ // If this is the first index being registered, we initialize the map storing all the indexes.
168
+ if f .indexes == nil {
169
+ f .indexes = make (map [schema.GroupVersionKind ]map [string ]client.IndexerFunc )
170
+ }
171
+
172
+ // If this is the first index being registered for the GroupVersionKind of `obj`, we initialize
173
+ // the map storing the indexes for that GroupVersionKind.
174
+ if f .indexes [gvk ] == nil {
175
+ f .indexes [gvk ] = make (map [string ]client.IndexerFunc )
176
+ }
177
+
178
+ if _ , fieldAlreadyIndexed := f.indexes [gvk ][field ]; fieldAlreadyIndexed {
179
+ panic (fmt .Errorf ("indexer conflict: field %s for GroupVersionKind %v is already indexed" ,
180
+ field , gvk ))
181
+ }
182
+
183
+ f.indexes [gvk ][field ] = extractValue
184
+
185
+ return f
186
+ }
187
+
138
188
// Build builds and returns a new fake client.
139
189
func (f * ClientBuilder ) Build () client.WithWatch {
140
190
if f .scheme == nil {
@@ -171,6 +221,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
171
221
tracker : tracker ,
172
222
scheme : f .scheme ,
173
223
restMapper : f .restMapper ,
224
+ indexes : f .indexes ,
174
225
}
175
226
}
176
227
@@ -420,21 +471,88 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
420
471
return err
421
472
}
422
473
423
- if listOpts .LabelSelector != nil {
424
- objs , err := meta .ExtractList (obj )
474
+ if listOpts .LabelSelector == nil && listOpts .FieldSelector == nil {
475
+ return nil
476
+ }
477
+
478
+ // If we're here, either a label or field selector are specified (or both), so before we return
479
+ // the list we must filter it. If both selectors are set, they are ANDed.
480
+ objs , err := meta .ExtractList (obj )
481
+ if err != nil {
482
+ return err
483
+ }
484
+
485
+ filteredList , err := c .filterList (objs , gvk , listOpts .LabelSelector , listOpts .FieldSelector )
486
+ if err != nil {
487
+ return err
488
+ }
489
+
490
+ return meta .SetList (obj , filteredList )
491
+ }
492
+
493
+ func (c * fakeClient ) filterList (list []runtime.Object , gvk schema.GroupVersionKind , ls labels.Selector , fs fields.Selector ) ([]runtime.Object , error ) {
494
+ // Filter the objects with the label selector
495
+ filteredList := list
496
+ if ls != nil {
497
+ objsFilteredByLabel , err := objectutil .FilterWithLabels (list , ls )
425
498
if err != nil {
426
- return err
499
+ return nil , err
427
500
}
428
- filteredObjs , err := objectutil .FilterWithLabels (objs , listOpts .LabelSelector )
501
+ filteredList = objsFilteredByLabel
502
+ }
503
+
504
+ // Filter the result of the previous pass with the field selector
505
+ if fs != nil {
506
+ objsFilteredByField , err := c .filterWithFields (filteredList , gvk , fs )
429
507
if err != nil {
430
- return err
508
+ return nil , err
431
509
}
432
- err = meta .SetList (obj , filteredObjs )
433
- if err != nil {
434
- return err
510
+ filteredList = objsFilteredByField
511
+ }
512
+
513
+ return filteredList , nil
514
+ }
515
+
516
+ func (c * fakeClient ) filterWithFields (list []runtime.Object , gvk schema.GroupVersionKind , fs fields.Selector ) ([]runtime.Object , error ) {
517
+ // We only allow filtering on the basis of a single field to ensure consistency with the
518
+ // behavior of the cache reader (which we're faking here).
519
+ fieldKey , fieldVal , requiresExact := selector .RequiresExactMatch (fs )
520
+ if ! requiresExact {
521
+ return nil , fmt .Errorf ("field selector %s is not in one of the two supported forms \" key==val\" or \" key=val\" " ,
522
+ fs )
523
+ }
524
+
525
+ // Field selection is mimicked via indexes, so there's no sane answer this function can give
526
+ // if there are no indexes registered for the GroupVersionKind of the objects in the list.
527
+ indexes := c .indexes [gvk ]
528
+ if len (indexes ) == 0 || indexes [fieldKey ] == nil {
529
+ return nil , fmt .Errorf ("List on GroupVersionKind %v specifies selector on field %s, but no " +
530
+ "index with name %s has been registered for GroupVersionKind %v" , gvk , fieldKey , fieldKey , gvk )
531
+ }
532
+
533
+ indexExtractor := indexes [fieldKey ]
534
+ filteredList := make ([]runtime.Object , 0 , len (list ))
535
+ for _ , obj := range list {
536
+ if c .objMatchesFieldSelector (obj , indexExtractor , fieldVal ) {
537
+ filteredList = append (filteredList , obj )
435
538
}
436
539
}
437
- return nil
540
+ return filteredList , nil
541
+ }
542
+
543
+ func (c * fakeClient ) objMatchesFieldSelector (o runtime.Object , extractIndex client.IndexerFunc , val string ) bool {
544
+ obj , isClientObject := o .(client.Object )
545
+ if ! isClientObject {
546
+ panic (fmt .Errorf ("expected object %v to be of type client.Object, but it's not" , o ))
547
+ }
548
+
549
+ for _ , extractedVal := range extractIndex (obj ) {
550
+ if extractedVal == val {
551
+ return true
552
+ }
553
+ }
554
+
555
+ return false
438
556
}
439
557
440
558
func (c * fakeClient ) Scheme () * runtime.Scheme {
0 commit comments