Skip to content

Commit fd22dea

Browse files
✨ Modify multinamespaced cache to support cluster scoped resources
This PR modifies the multinamespacedcache implementation to: - create a global cache mapping for an empty namespace, so that when cluster scoped resources are fetched, namespace is not required. - deduplicate the objects in the `List` call, based on unique combination of resource name and namespace. Signed-off-by: varshaprasad96 <[email protected]>
1 parent 774f9d4 commit fd22dea

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

pkg/cache/cache_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,39 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
494494
err := informerCache.Get(context.Background(), svcKey, svc)
495495
Expect(err).To(HaveOccurred())
496496
})
497+
It("test multinamespaced cache for cluster scoped resources", func() {
498+
By("creating a multinamespaced cache to watch specific namespaces")
499+
multi := cache.MultiNamespacedCacheBuilder([]string{"default", testNamespaceOne})
500+
m, err := multi(cfg, cache.Options{})
501+
Expect(err).NotTo(HaveOccurred())
502+
503+
By("running the cache and waiting it for sync")
504+
go func() {
505+
defer GinkgoRecover()
506+
Expect(m.Start(informerCacheCtx)).To(Succeed())
507+
}()
508+
Expect(m.WaitForCacheSync(informerCacheCtx)).NotTo(BeFalse())
509+
510+
By("should be able to fetch cluster scoped resource")
511+
node := &kcorev1.Node{}
512+
513+
By("verifying that getting the node works with an empty namespace")
514+
key1 := client.ObjectKey{Namespace: "", Name: testNodeOne}
515+
Expect(m.Get(context.Background(), key1, node)).To(Succeed())
516+
517+
By("verifying if the cluster scoped resources are not duplicated")
518+
nodeList := &unstructured.UnstructuredList{}
519+
nodeList.SetGroupVersionKind(schema.GroupVersionKind{
520+
Group: "",
521+
Version: "v1",
522+
Kind: "NodeList",
523+
})
524+
Expect(m.List(context.Background(), nodeList)).To(Succeed())
525+
526+
By("verifying the node list is not empty")
527+
Expect(nodeList.Items).NotTo(BeEmpty())
528+
Expect(len(nodeList.Items)).To(BeEquivalentTo(1))
529+
})
497530
})
498531
Context("with metadata-only objects", func() {
499532
It("should be able to list objects that haven't been watched previously", func() {

pkg/cache/multi_namespace_cache.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ import (
3434
// NewCacheFunc - Function for creating a new cache from the options and a rest config
3535
type NewCacheFunc func(config *rest.Config, opts Options) (Cache, error)
3636

37+
// a new global namespaced cache to handle cluster scoped resources
38+
var globalCache = ""
39+
3740
// MultiNamespacedCacheBuilder - Builder function to create a new multi-namespaced cache.
3841
// This will scope the cache to a list of namespaces. Listing for all namespaces
39-
// will list for all the namespaces that this knows about. Note that this is not intended
42+
// will list for all the namespaces that this knows about. By default this will create
43+
// a global cache for cluster scoped resource (having empty namespace). Note that this is not intended
4044
// to be used for excluding namespaces, this is better done via a Predicate. Also note that
4145
// you may face performance issues when using this with a high number of namespaces.
4246
func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
@@ -45,6 +49,8 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
4549
if err != nil {
4650
return nil, err
4751
}
52+
// create a cache for cluster scoped resources
53+
namespaces = append(namespaces, globalCache)
4854
caches := map[string]Cache{}
4955
for _, ns := range namespaces {
5056
opts.Namespace = ns
@@ -143,7 +149,16 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList,
143149
if !ok {
144150
return fmt.Errorf("unable to get: %v because of unknown namespace for the cache", listOpts.Namespace)
145151
}
146-
return cache.List(ctx, list, opts...)
152+
err := cache.List(ctx, list, opts...)
153+
if err != nil {
154+
return err
155+
}
156+
items, err := apimeta.ExtractList(list)
157+
if err != nil {
158+
return err
159+
}
160+
uniqueItems := removeDuplicates(items)
161+
return apimeta.SetList(list, uniqueItems)
147162
}
148163

149164
listAccessor, err := meta.ListAccessor(list)
@@ -174,9 +189,28 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList,
174189
// The last list call should have the most correct resource version.
175190
resourceVersion = accessor.GetResourceVersion()
176191
}
192+
193+
uniqueItems := removeDuplicates(allItems)
177194
listAccessor.SetResourceVersion(resourceVersion)
178195

179-
return apimeta.SetList(list, allItems)
196+
return apimeta.SetList(list, uniqueItems)
197+
}
198+
199+
// removeDuplicates removes the duplicate objects obtained from all namespaces so that
200+
// the resulting list has objects with unique name and namespace.
201+
func removeDuplicates(items []runtime.Object) []runtime.Object {
202+
objects := make(map[string]bool)
203+
unique := []runtime.Object{}
204+
205+
for _, obj := range items {
206+
metaObj, _ := meta.Accessor(obj)
207+
key := metaObj.GetName() + " " + metaObj.GetNamespace()
208+
if _, value := objects[key]; !value {
209+
objects[key] = true
210+
unique = append(unique, obj)
211+
}
212+
}
213+
return unique
180214
}
181215

182216
// multiNamespaceInformer knows how to handle interacting with the underlying informer across multiple namespaces

0 commit comments

Comments
 (0)