Skip to content

Commit c8aad1d

Browse files
matteooliviPaul Eichler
and
Paul Eichler
committed
Add indexes to fake client
Allow registration of indexes into the fake client to allow usage of field selectors when doing a List. The indexing is done lazily by doing a normal list and then filtering the results by computing the index value for each list item. To enable the main change for this commit, some refactorings are performed: an internal and unexported function that checks whether a field selector is in the form key=val or key==val is made internal and exported to be shared between real and faked code to ensure loyalty. Unit tests for it are added. Co-authored-by: Paul Eichler <peichler@anynines.com>
1 parent 8ad090e commit c8aad1d

File tree

6 files changed

+469
-31
lines changed

6 files changed

+469
-31
lines changed

pkg/cache/internal/cache_reader.go

+2-16
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ import (
2323

2424
apierrors "k8s.io/apimachinery/pkg/api/errors"
2525
apimeta "k8s.io/apimachinery/pkg/api/meta"
26-
"k8s.io/apimachinery/pkg/fields"
2726
"k8s.io/apimachinery/pkg/labels"
2827
"k8s.io/apimachinery/pkg/runtime"
2928
"k8s.io/apimachinery/pkg/runtime/schema"
30-
"k8s.io/apimachinery/pkg/selection"
3129
"k8s.io/client-go/tools/cache"
30+
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector"
3231

3332
"sigs.k8s.io/controller-runtime/pkg/client"
3433
)
@@ -116,7 +115,7 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
116115
case listOpts.FieldSelector != nil:
117116
// TODO(directxman12): support more complicated field selectors by
118117
// combining multiple indices, GetIndexers, etc
119-
field, val, requiresExact := requiresExactMatch(listOpts.FieldSelector)
118+
field, val, requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
120119
if !requiresExact {
121120
return fmt.Errorf("non-exact field matches are not supported by the cache")
122121
}
@@ -186,19 +185,6 @@ func objectKeyToStoreKey(k client.ObjectKey) string {
186185
return k.Namespace + "/" + k.Name
187186
}
188187

189-
// requiresExactMatch checks if the given field selector is of the form `k=v` or `k==v`.
190-
func requiresExactMatch(sel fields.Selector) (field, val string, required bool) {
191-
reqs := sel.Requirements()
192-
if len(reqs) != 1 {
193-
return "", "", false
194-
}
195-
req := reqs[0]
196-
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
197-
return "", "", false
198-
}
199-
return req.Field, req.Value, true
200-
}
201-
202188
// FieldIndexName constructs the name of the index over the given field,
203189
// for use with an indexer.
204190
func FieldIndexName(field string) string {

pkg/client/fake/client.go

+121-12
Original file line numberDiff line numberDiff line change
@@ -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

5154
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+
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.
139176
func (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

440549
func (c *fakeClient) Scheme() *runtime.Scheme {

0 commit comments

Comments
 (0)