@@ -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 GroupVersionResource (GVR) to the indexes registered for that GVR.
60
+ // The inner map maps from index name to IndexerFunc.
61
+ indexes map [schema.GroupVersionResource ]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 GroupVersionResource (GVR) to the indexes registered for that GVR.
106
+ // The inner map maps from index name to IndexerFunc.
107
+ indexes map [schema.GroupVersionResource ]map [string ]client.IndexerFunc
96
108
}
97
109
98
110
// WithScheme sets this builder's internal scheme.
@@ -135,6 +147,31 @@ 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 `name` and indexer `indexer` for
151
+ // API objects of GroupVersionResource `gvr` in the fake client.
152
+ // It can be invoked multiple times, both with different GroupVersionResource or the same one.
153
+ // Invoking WithIndex twice with the same `name` and `gvr` will panic.
154
+ func (f * ClientBuilder ) WithIndex (gvr schema.GroupVersionResource , name string , indexer client.IndexerFunc ) * ClientBuilder {
155
+ // If this is the first index being registered, we initialize the map storing all the indexes.
156
+ if f .indexes == nil {
157
+ f .indexes = make (map [schema.GroupVersionResource ]map [string ]client.IndexerFunc )
158
+ }
159
+
160
+ // If this is the first index being registered for the input GroupVersionResource, we initialize
161
+ // the map storing the indexes for that GroupVersionResource.
162
+ if f .indexes [gvr ] == nil {
163
+ f .indexes [gvr ] = make (map [string ]client.IndexerFunc )
164
+ }
165
+
166
+ if _ , nameAlreadyTaken := f.indexes [gvr ][name ]; nameAlreadyTaken {
167
+ panic (fmt .Errorf ("indexer conflict: index name %s is already registered for GroupVersionResource %v" , name , gvr ))
168
+ }
169
+
170
+ f.indexes [gvr ][name ] = indexer
171
+
172
+ return f
173
+ }
174
+
138
175
// Build builds and returns a new fake client.
139
176
func (f * ClientBuilder ) Build () client.WithWatch {
140
177
if f .scheme == nil {
@@ -171,6 +208,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
171
208
tracker : tracker ,
172
209
scheme : f .scheme ,
173
210
restMapper : f .restMapper ,
211
+ indexes : f .indexes ,
174
212
}
175
213
}
176
214
@@ -420,21 +458,92 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
420
458
return err
421
459
}
422
460
423
- if listOpts .LabelSelector != nil {
424
- objs , err := meta .ExtractList (obj )
461
+ if listOpts .LabelSelector == nil && listOpts .FieldSelector == nil {
462
+ return nil
463
+ }
464
+
465
+ // If we're here, either a label or field selector are specified (or both), so before we return
466
+ // the list we must filter it. If both selectors are set, they are ANDed.
467
+ objs , err := meta .ExtractList (obj )
468
+ if err != nil {
469
+ return err
470
+ }
471
+
472
+ filteredList , err := c .filterList (objs , gvr , listOpts .LabelSelector , listOpts .FieldSelector )
473
+ if err != nil {
474
+ return err
475
+ }
476
+
477
+ return meta .SetList (obj , filteredList )
478
+ }
479
+
480
+ func (c * fakeClient ) filterList (list []runtime.Object , gvr schema.GroupVersionResource , ls labels.Selector , fs fields.Selector ) ([]runtime.Object , error ) {
481
+ // Filter the objects with the label selector
482
+ filteredList := list
483
+ if ls != nil {
484
+ objsFilteredByLabel , err := objectutil .FilterWithLabels (list , ls )
425
485
if err != nil {
426
- return err
486
+ return nil , err
427
487
}
428
- filteredObjs , err := objectutil .FilterWithLabels (objs , listOpts .LabelSelector )
488
+ filteredList = objsFilteredByLabel
489
+ }
490
+
491
+ // Filter the result of the previous pass with the field selector
492
+ if fs != nil {
493
+ objsFilteredByField , err := c .filterWithFields (filteredList , gvr , fs )
429
494
if err != nil {
430
- return err
495
+ return nil , err
431
496
}
432
- err = meta .SetList (obj , filteredObjs )
433
- if err != nil {
434
- return err
497
+ filteredList = objsFilteredByField
498
+ }
499
+
500
+ return filteredList , nil
501
+ }
502
+
503
+ func (c * fakeClient ) filterWithFields (list []runtime.Object , gvr schema.GroupVersionResource , fs fields.Selector ) ([]runtime.Object , error ) {
504
+ // We only allow filtering on the basis of a single field to ensure consistency with the
505
+ // behavior of the cache reader (which we're faking here).
506
+ fieldKey , fieldVal , requiresExact := selector .RequiresExactMatch (fs )
507
+ if ! requiresExact {
508
+ return nil , fmt .Errorf ("field selector %s is not in one of the two supported forms \" key==val\" or \" key=val\" " ,
509
+ fs )
510
+ }
511
+
512
+ // Field selection is mimicked via indexes, so there's no sane answer this function can give
513
+ // if there are no indexes registered for the GroupVersionResource of the objects in the list.
514
+ indexes , listGVRHasIndexes := c .indexes [gvr ]
515
+ if ! listGVRHasIndexes {
516
+ return nil , fmt .Errorf ("List on GroupVersionResource %v specifies field selector, but no " +
517
+ "indexes for that GroupResourceVersion are defined" , gvr )
518
+ }
519
+
520
+ indexExtractor , found := indexes [fieldKey ]
521
+ if ! found {
522
+ return nil , fmt .Errorf ("no index with name %s was registered" , fieldKey )
523
+ }
524
+
525
+ filteredList := make ([]runtime.Object , 0 , len (list ))
526
+ for _ , obj := range list {
527
+ if c .objMatchesFieldSelector (obj , indexExtractor , fieldVal ) {
528
+ filteredList = append (filteredList , obj )
435
529
}
436
530
}
437
- return nil
531
+ return filteredList , nil
532
+ }
533
+
534
+ func (c * fakeClient ) objMatchesFieldSelector (o runtime.Object , extractIndex client.IndexerFunc , val string ) bool {
535
+ obj , isClientObject := o .(client.Object )
536
+ if ! isClientObject {
537
+ panic (fmt .Errorf ("expected object %v to be of type client.Object, but it's not" , o ))
538
+ }
539
+
540
+ for _ , extractedVal := range extractIndex (obj ) {
541
+ if extractedVal == val {
542
+ return true
543
+ }
544
+ }
545
+
546
+ return false
438
547
}
439
548
440
549
func (c * fakeClient ) Scheme () * runtime.Scheme {
0 commit comments