@@ -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 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+
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 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
96108}
97109
98110// WithScheme sets this builder's internal scheme.
@@ -135,6 +147,44 @@ 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 `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+
138188// Build builds and returns a new fake client.
139189func (f * ClientBuilder ) Build () client.WithWatch {
140190 if f .scheme == nil {
@@ -171,6 +221,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
171221 tracker : tracker ,
172222 scheme : f .scheme ,
173223 restMapper : f .restMapper ,
224+ indexes : f .indexes ,
174225 }
175226}
176227
@@ -420,21 +471,88 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
420471 return err
421472 }
422473
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 )
425498 if err != nil {
426- return err
499+ return nil , err
427500 }
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 )
429507 if err != nil {
430- return err
508+ return nil , err
431509 }
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 )
435538 }
436539 }
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
438556}
439557
440558func (c * fakeClient ) Scheme () * runtime.Scheme {
0 commit comments