Skip to content

Commit

Permalink
✨ Modify multinamespaced cache to support cluster scoped resources
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
varshaprasad96 committed Mar 8, 2021
1 parent 774f9d4 commit fd22dea
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 3 deletions.
33 changes: 33 additions & 0 deletions pkg/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,39 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
err := informerCache.Get(context.Background(), svcKey, svc)
Expect(err).To(HaveOccurred())
})
It("test multinamespaced cache for cluster scoped resources", func() {
By("creating a multinamespaced cache to watch specific namespaces")
multi := cache.MultiNamespacedCacheBuilder([]string{"default", testNamespaceOne})
m, err := multi(cfg, cache.Options{})
Expect(err).NotTo(HaveOccurred())

By("running the cache and waiting it for sync")
go func() {
defer GinkgoRecover()
Expect(m.Start(informerCacheCtx)).To(Succeed())
}()
Expect(m.WaitForCacheSync(informerCacheCtx)).NotTo(BeFalse())

By("should be able to fetch cluster scoped resource")
node := &kcorev1.Node{}

By("verifying that getting the node works with an empty namespace")
key1 := client.ObjectKey{Namespace: "", Name: testNodeOne}
Expect(m.Get(context.Background(), key1, node)).To(Succeed())

By("verifying if the cluster scoped resources are not duplicated")
nodeList := &unstructured.UnstructuredList{}
nodeList.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "NodeList",
})
Expect(m.List(context.Background(), nodeList)).To(Succeed())

By("verifying the node list is not empty")
Expect(nodeList.Items).NotTo(BeEmpty())
Expect(len(nodeList.Items)).To(BeEquivalentTo(1))
})
})
Context("with metadata-only objects", func() {
It("should be able to list objects that haven't been watched previously", func() {
Expand Down
40 changes: 37 additions & 3 deletions pkg/cache/multi_namespace_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ import (
// NewCacheFunc - Function for creating a new cache from the options and a rest config
type NewCacheFunc func(config *rest.Config, opts Options) (Cache, error)

// a new global namespaced cache to handle cluster scoped resources
var globalCache = ""

// MultiNamespacedCacheBuilder - Builder function to create a new multi-namespaced cache.
// This will scope the cache to a list of namespaces. Listing for all namespaces
// will list for all the namespaces that this knows about. Note that this is not intended
// will list for all the namespaces that this knows about. By default this will create
// a global cache for cluster scoped resource (having empty namespace). Note that this is not intended
// to be used for excluding namespaces, this is better done via a Predicate. Also note that
// you may face performance issues when using this with a high number of namespaces.
func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
Expand All @@ -45,6 +49,8 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
if err != nil {
return nil, err
}
// create a cache for cluster scoped resources
namespaces = append(namespaces, globalCache)
caches := map[string]Cache{}
for _, ns := range namespaces {
opts.Namespace = ns
Expand Down Expand Up @@ -143,7 +149,16 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList,
if !ok {
return fmt.Errorf("unable to get: %v because of unknown namespace for the cache", listOpts.Namespace)
}
return cache.List(ctx, list, opts...)
err := cache.List(ctx, list, opts...)
if err != nil {
return err
}
items, err := apimeta.ExtractList(list)
if err != nil {
return err
}
uniqueItems := removeDuplicates(items)
return apimeta.SetList(list, uniqueItems)
}

listAccessor, err := meta.ListAccessor(list)
Expand Down Expand Up @@ -174,9 +189,28 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList,
// The last list call should have the most correct resource version.
resourceVersion = accessor.GetResourceVersion()
}

uniqueItems := removeDuplicates(allItems)
listAccessor.SetResourceVersion(resourceVersion)

return apimeta.SetList(list, allItems)
return apimeta.SetList(list, uniqueItems)
}

// removeDuplicates removes the duplicate objects obtained from all namespaces so that
// the resulting list has objects with unique name and namespace.
func removeDuplicates(items []runtime.Object) []runtime.Object {
objects := make(map[string]bool)
unique := []runtime.Object{}

for _, obj := range items {
metaObj, _ := meta.Accessor(obj)
key := metaObj.GetName() + " " + metaObj.GetNamespace()
if _, value := objects[key]; !value {
objects[key] = true
unique = append(unique, obj)
}
}
return unique
}

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

0 comments on commit fd22dea

Please sign in to comment.