@@ -30,13 +30,16 @@ import (
3030 "k8s.io/apimachinery/pkg/api/meta"
3131 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33+ "k8s.io/apimachinery/pkg/fields"
34+ "k8s.io/apimachinery/pkg/labels"
3335 "k8s.io/apimachinery/pkg/runtime"
3436 "k8s.io/apimachinery/pkg/runtime/schema"
3537 utilrand "k8s.io/apimachinery/pkg/util/rand"
3638 "k8s.io/apimachinery/pkg/util/validation/field"
3739 "k8s.io/apimachinery/pkg/watch"
3840 "k8s.io/client-go/kubernetes/scheme"
3941 "k8s.io/client-go/testing"
42+ "sigs.k8s.io/controller-runtime/pkg/internal/field/selector"
4043
4144 "sigs.k8s.io/controller-runtime/pkg/client"
4245 "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -49,9 +52,14 @@ type versionedTracker struct {
4952}
5053
5154type 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+
5563 schemeWriteLock sync.Mutex
5664}
5765
@@ -93,6 +101,10 @@ type ClientBuilder struct {
93101 initLists []client.ObjectList
94102 initRuntimeObjects []runtime.Object
95103 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
96108}
97109
98110// WithScheme sets this builder's internal scheme.
@@ -135,6 +147,31 @@ func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuild
135147 return f
136148}
137149
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+
138175// Build builds and returns a new fake client.
139176func (f * ClientBuilder ) Build () client.WithWatch {
140177 if f .scheme == nil {
@@ -171,6 +208,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
171208 tracker : tracker ,
172209 scheme : f .scheme ,
173210 restMapper : f .restMapper ,
211+ indexes : f .indexes ,
174212 }
175213}
176214
@@ -420,21 +458,92 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
420458 return err
421459 }
422460
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 )
425485 if err != nil {
426- return err
486+ return nil , err
427487 }
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 )
429494 if err != nil {
430- return err
495+ return nil , err
431496 }
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 )
435529 }
436530 }
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
438547}
439548
440549func (c * fakeClient ) Scheme () * runtime.Scheme {
0 commit comments