Skip to content

Commit 4c9c956

Browse files
matteooliviPaul Eichler
and
Paul Eichler
authored
✨ Add indexes to fake client to mimic field selectors (#2025)
* 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 <[email protected]> * Address review comments Co-authored-by: Paul Eichler <[email protected]>
1 parent a0e1772 commit 4c9c956

File tree

6 files changed

+470
-31
lines changed

6 files changed

+470
-31
lines changed

pkg/cache/internal/cache_reader.go

Lines changed: 2 additions & 16 deletions
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

Lines changed: 130 additions & 12 deletions
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 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.
139189
func (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

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

0 commit comments

Comments
 (0)