Skip to content

Commit 78b3ce6

Browse files
authored
client: client.MatchingFields support multiple indexes (#2512)
add support for multiple indexes when using client.MatchingFields
1 parent e39539c commit 78b3ce6

File tree

8 files changed

+149
-65
lines changed

8 files changed

+149
-65
lines changed

pkg/cache/cache_test.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,13 +1998,13 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
19981998
Expect(err).NotTo(HaveOccurred())
19991999

20002000
By("indexing the Namespace objects with fixed values before starting")
2001-
pod := &corev1.Namespace{}
2001+
ns := &corev1.Namespace{}
20022002
indexerValues := []string{"a", "b", "c"}
20032003
fieldName := "fixedValues"
20042004
indexFunc := func(obj client.Object) []string {
20052005
return indexerValues
20062006
}
2007-
Expect(informer.IndexField(context.TODO(), pod, fieldName, indexFunc)).To(Succeed())
2007+
Expect(informer.IndexField(context.TODO(), ns, fieldName, indexFunc)).To(Succeed())
20082008

20092009
By("running the cache and waiting for it to sync")
20102010
go func() {
@@ -2025,6 +2025,51 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
20252025
Expect(indexerValues[1]).To(Equal("b"))
20262026
Expect(indexerValues[2]).To(Equal("c"))
20272027
})
2028+
2029+
It("should be able to matching fields with multiple indexes", func() {
2030+
By("creating the cache")
2031+
informer, err := cache.New(cfg, cache.Options{})
2032+
Expect(err).NotTo(HaveOccurred())
2033+
2034+
pod := &corev1.Pod{}
2035+
By("indexing pods with label before starting")
2036+
fieldName1 := "indexByLabel"
2037+
indexFunc1 := func(obj client.Object) []string {
2038+
return []string{obj.(*corev1.Pod).Labels["common-label"]}
2039+
}
2040+
Expect(informer.IndexField(context.TODO(), pod, fieldName1, indexFunc1)).To(Succeed())
2041+
By("indexing pods with restart policy before starting")
2042+
fieldName2 := "indexByPolicy"
2043+
indexFunc2 := func(obj client.Object) []string {
2044+
return []string{string(obj.(*corev1.Pod).Spec.RestartPolicy)}
2045+
}
2046+
Expect(informer.IndexField(context.TODO(), pod, fieldName2, indexFunc2)).To(Succeed())
2047+
2048+
By("running the cache and waiting for it to sync")
2049+
go func() {
2050+
defer GinkgoRecover()
2051+
Expect(informer.Start(informerCacheCtx)).To(Succeed())
2052+
}()
2053+
Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue())
2054+
2055+
By("listing pods with label index")
2056+
listObj := &corev1.PodList{}
2057+
Expect(informer.List(context.Background(), listObj,
2058+
client.MatchingFields{fieldName1: "common"})).To(Succeed())
2059+
Expect(listObj.Items).To(HaveLen(2))
2060+
2061+
By("listing pods with restart policy index")
2062+
listObj = &corev1.PodList{}
2063+
Expect(informer.List(context.Background(), listObj,
2064+
client.MatchingFields{fieldName2: string(corev1.RestartPolicyNever)})).To(Succeed())
2065+
Expect(listObj.Items).To(HaveLen(3))
2066+
2067+
By("listing pods with both fixed indexers 1 and 2")
2068+
listObj = &corev1.PodList{}
2069+
Expect(informer.List(context.Background(), listObj,
2070+
client.MatchingFields{fieldName1: "common", fieldName2: string(corev1.RestartPolicyNever)})).To(Succeed())
2071+
Expect(listObj.Items).To(HaveLen(1))
2072+
})
20282073
})
20292074
Context("with unstructured objects", func() {
20302075
It("should be able to get informer for the object", func() {

pkg/cache/internal/cache_reader.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
apierrors "k8s.io/apimachinery/pkg/api/errors"
2525
apimeta "k8s.io/apimachinery/pkg/api/meta"
26+
"k8s.io/apimachinery/pkg/fields"
2627
"k8s.io/apimachinery/pkg/labels"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -117,16 +118,14 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
117118

118119
switch {
119120
case listOpts.FieldSelector != nil:
120-
// TODO(directxman12): support more complicated field selectors by
121-
// combining multiple indices, GetIndexers, etc
122-
field, val, requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
121+
requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
123122
if !requiresExact {
124123
return fmt.Errorf("non-exact field matches are not supported by the cache")
125124
}
126125
// list all objects by the field selector. If this is namespaced and we have one, ask for the
127126
// namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces"
128127
// namespace.
129-
objs, err = c.indexer.ByIndex(FieldIndexName(field), KeyToNamespacedKey(listOpts.Namespace, val))
128+
objs, err = byIndexes(c.indexer, listOpts.FieldSelector.Requirements(), listOpts.Namespace)
130129
case listOpts.Namespace != "":
131130
objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace)
132131
default:
@@ -178,6 +177,54 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
178177
return apimeta.SetList(out, runtimeObjs)
179178
}
180179

180+
func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]interface{}, error) {
181+
var (
182+
err error
183+
objs []interface{}
184+
vals []string
185+
)
186+
indexers := indexer.GetIndexers()
187+
for idx, req := range requires {
188+
indexName := FieldIndexName(req.Field)
189+
indexedValue := KeyToNamespacedKey(namespace, req.Value)
190+
if idx == 0 {
191+
// we use first require to get snapshot data
192+
// TODO(halfcrazy): use complicated index when client-go provides byIndexes
193+
// https://github.com/kubernetes/kubernetes/issues/109329
194+
objs, err = indexer.ByIndex(indexName, indexedValue)
195+
if err != nil {
196+
return nil, err
197+
}
198+
if len(objs) == 0 {
199+
return nil, nil
200+
}
201+
continue
202+
}
203+
fn, exist := indexers[indexName]
204+
if !exist {
205+
return nil, fmt.Errorf("index with name %s does not exist", indexName)
206+
}
207+
filteredObjects := make([]interface{}, 0, len(objs))
208+
for _, obj := range objs {
209+
vals, err = fn(obj)
210+
if err != nil {
211+
return nil, err
212+
}
213+
for _, val := range vals {
214+
if val == indexedValue {
215+
filteredObjects = append(filteredObjects, obj)
216+
break
217+
}
218+
}
219+
}
220+
if len(filteredObjects) == 0 {
221+
return nil, nil
222+
}
223+
objs = filteredObjects
224+
}
225+
return objs, nil
226+
}
227+
181228
// objectKeyToStorageKey converts an object key to store key.
182229
// It's akin to MetaNamespaceKeyFunc. It's separate from
183230
// String to allow keeping the key format easily in sync with

pkg/client/example_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func ExampleClient_deleteAllOf() {
262262
}
263263

264264
// This example shows how to set up and consume a field selector over a pod's volumes' secretName field.
265-
func ExampleFieldIndexer_secretName() {
265+
func ExampleFieldIndexer_secretNameNode() {
266266
// someIndexer is a FieldIndexer over a Cache
267267
_ = someIndexer.IndexField(context.TODO(), &corev1.Pod{}, "spec.volumes.secret.secretName", func(o client.Object) []string {
268268
var res []string
@@ -276,8 +276,20 @@ func ExampleFieldIndexer_secretName() {
276276
return res
277277
})
278278

279+
_ = someIndexer.IndexField(context.TODO(), &corev1.Pod{}, "spec.NodeName", func(o client.Object) []string {
280+
nodeName := o.(*corev1.Pod).Spec.NodeName
281+
if nodeName != "" {
282+
return []string{nodeName}
283+
}
284+
return nil
285+
})
286+
279287
// elsewhere (e.g. in your reconciler)
280288
mySecretName := "someSecret" // derived from the reconcile.Request, for instance
289+
myNode := "master-0"
281290
var podsWithSecrets corev1.PodList
282-
_ = c.List(context.Background(), &podsWithSecrets, client.MatchingFields{"spec.volumes.secret.secretName": mySecretName})
291+
_ = c.List(context.Background(), &podsWithSecrets, client.MatchingFields{
292+
"spec.volumes.secret.secretName": mySecretName,
293+
"spec.NodeName": myNode,
294+
})
283295
}

pkg/client/fake/client.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -588,9 +588,7 @@ func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKi
588588
}
589589

590590
func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) {
591-
// We only allow filtering on the basis of a single field to ensure consistency with the
592-
// behavior of the cache reader (which we're faking here).
593-
fieldKey, fieldVal, requiresExact := selector.RequiresExactMatch(fs)
591+
requiresExact := selector.RequiresExactMatch(fs)
594592
if !requiresExact {
595593
return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"",
596594
fs)
@@ -599,15 +597,24 @@ func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVer
599597
// Field selection is mimicked via indexes, so there's no sane answer this function can give
600598
// if there are no indexes registered for the GroupVersionKind of the objects in the list.
601599
indexes := c.indexes[gvk]
602-
if len(indexes) == 0 || indexes[fieldKey] == nil {
603-
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
604-
"index with name %s has been registered for GroupVersionKind %v", gvk, fieldKey, fieldKey, gvk)
600+
for _, req := range fs.Requirements() {
601+
if len(indexes) == 0 || indexes[req.Field] == nil {
602+
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
603+
"index with name %s has been registered for GroupVersionKind %v", gvk, req.Field, req.Field, gvk)
604+
}
605605
}
606606

607-
indexExtractor := indexes[fieldKey]
608607
filteredList := make([]runtime.Object, 0, len(list))
609608
for _, obj := range list {
610-
if c.objMatchesFieldSelector(obj, indexExtractor, fieldVal) {
609+
matches := true
610+
for _, req := range fs.Requirements() {
611+
indexExtractor := indexes[req.Field]
612+
if !c.objMatchesFieldSelector(obj, indexExtractor, req.Value) {
613+
matches = false
614+
break
615+
}
616+
}
617+
if matches {
611618
filteredList = append(filteredList, obj)
612619
}
613620
}

pkg/client/fake/client_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,14 +1333,15 @@ var _ = Describe("Fake client", func() {
13331333
Expect(list.Items).To(BeEmpty())
13341334
})
13351335

1336-
It("errors when field selector uses two requirements", func() {
1336+
It("no error when field selector uses two requirements", func() {
13371337
listOpts := &client.ListOptions{
13381338
FieldSelector: fields.AndSelectors(
13391339
fields.OneTermEqualSelector("spec.replicas", "1"),
13401340
fields.OneTermEqualSelector("spec.strategy.type", string(appsv1.RecreateDeploymentStrategyType)),
13411341
)}
1342-
err := cl.List(context.Background(), &appsv1.DeploymentList{}, listOpts)
1343-
Expect(err).To(HaveOccurred())
1342+
list := &appsv1.DeploymentList{}
1343+
Expect(cl.List(context.Background(), list, listOpts)).To(Succeed())
1344+
Expect(list.Items).To(ConsistOf(*dep))
13441345
})
13451346
})
13461347
})

pkg/client/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ type ListOptions struct {
419419
LabelSelector labels.Selector
420420
// FieldSelector filters results by a particular field. In order
421421
// to use this with cache-based implementations, restrict usage to
422-
// a single field-value pair that's been added to the indexers.
422+
// exact match field-value pair that's been added to the indexers.
423423
FieldSelector fields.Selector
424424

425425
// Namespace represents the namespace to list for, or empty for

pkg/internal/field/selector/utils.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ import (
2222
)
2323

2424
// RequiresExactMatch checks if the given field selector is of the form `k=v` or `k==v`.
25-
func RequiresExactMatch(sel fields.Selector) (field, val string, required bool) {
25+
func RequiresExactMatch(sel fields.Selector) bool {
2626
reqs := sel.Requirements()
27-
if len(reqs) != 1 {
28-
return "", "", false
27+
if len(reqs) == 0 {
28+
return false
2929
}
30-
req := reqs[0]
31-
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
32-
return "", "", false
30+
31+
for _, req := range reqs {
32+
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
33+
return false
34+
}
3335
}
34-
return req.Field, req.Value, true
36+
return true
3537
}

pkg/internal/field/selector/utils_test.go

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,62 +27,32 @@ import (
2727
var _ = Describe("RequiresExactMatch function", func() {
2828

2929
It("Returns false when the selector matches everything", func() {
30-
_, _, requiresExactMatch := RequiresExactMatch(fields.Everything())
30+
requiresExactMatch := RequiresExactMatch(fields.Everything())
3131
Expect(requiresExactMatch).To(BeFalse())
3232
})
3333

3434
It("Returns false when the selector matches nothing", func() {
35-
_, _, requiresExactMatch := RequiresExactMatch(fields.Nothing())
35+
requiresExactMatch := RequiresExactMatch(fields.Nothing())
3636
Expect(requiresExactMatch).To(BeFalse())
3737
})
3838

3939
It("Returns false when the selector has the form key!=val", func() {
40-
_, _, requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key!=val"))
40+
requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key!=val"))
4141
Expect(requiresExactMatch).To(BeFalse())
4242
})
4343

44-
It("Returns false when the selector has the form key1==val1,key2==val2", func() {
45-
_, _, requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key1==val1,key2==val2"))
46-
Expect(requiresExactMatch).To(BeFalse())
44+
It("Returns true when the selector has the form key1==val1,key2==val2", func() {
45+
requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key1==val1,key2==val2"))
46+
Expect(requiresExactMatch).To(BeTrue())
4747
})
4848

4949
It("Returns true when the selector has the form key==val", func() {
50-
_, _, requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key==val"))
50+
requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key==val"))
5151
Expect(requiresExactMatch).To(BeTrue())
5252
})
5353

5454
It("Returns true when the selector has the form key=val", func() {
55-
_, _, requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key=val"))
55+
requiresExactMatch := RequiresExactMatch(fields.ParseSelectorOrDie("key=val"))
5656
Expect(requiresExactMatch).To(BeTrue())
5757
})
58-
59-
It("Returns empty key and value when the selector matches everything", func() {
60-
key, val, _ := RequiresExactMatch(fields.Everything())
61-
Expect(key).To(Equal(""))
62-
Expect(val).To(Equal(""))
63-
})
64-
65-
It("Returns empty key and value when the selector matches nothing", func() {
66-
key, val, _ := RequiresExactMatch(fields.Nothing())
67-
Expect(key).To(Equal(""))
68-
Expect(val).To(Equal(""))
69-
})
70-
71-
It("Returns empty key and value when the selector has the form key!=val", func() {
72-
key, val, _ := RequiresExactMatch(fields.ParseSelectorOrDie("key!=val"))
73-
Expect(key).To(Equal(""))
74-
Expect(val).To(Equal(""))
75-
})
76-
77-
It("Returns key and value when the selector has the form key==val", func() {
78-
key, val, _ := RequiresExactMatch(fields.ParseSelectorOrDie("key==val"))
79-
Expect(key).To(Equal("key"))
80-
Expect(val).To(Equal("val"))
81-
})
82-
83-
It("Returns key and value when the selector has the form key=val", func() {
84-
key, val, _ := RequiresExactMatch(fields.ParseSelectorOrDie("key=val"))
85-
Expect(key).To(Equal("key"))
86-
Expect(val).To(Equal("val"))
87-
})
8858
})

0 commit comments

Comments
 (0)