Skip to content

Commit 5673ada

Browse files
🐛 Modify multinamespaced cache to support cluster scoped resources (#1418)
* 🐛 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 <varshaprasad96@gmail.com> * Add restmapper to multinamespaced cache * Use restmapper to identify scope of the object Modify multinamespaced cache to accept restmapper, which can be used to identify the scope of the object and handle the cluster scoped objects accordingly. * Rename fileter.go to objectutil.go Signed-off-by: varshaprasad96 <varshaprasad96@gmail.com>
1 parent 9e04ba9 commit 5673ada

File tree

4 files changed

+98
-2
lines changed

4 files changed

+98
-2
lines changed

pkg/cache/cache_test.go

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

pkg/cache/multi_namespace_cache.go

+33-2
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ import (
2929
"k8s.io/client-go/rest"
3030
toolscache "k8s.io/client-go/tools/cache"
3131
"sigs.k8s.io/controller-runtime/pkg/client"
32+
"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
3233
)
3334

3435
// NewCacheFunc - Function for creating a new cache from the options and a rest config
3536
type NewCacheFunc func(config *rest.Config, opts Options) (Cache, error)
3637

38+
// a new global namespaced cache to handle cluster scoped resources
39+
const globalCache = "_cluster-scope"
40+
3741
// MultiNamespacedCacheBuilder - Builder function to create a new multi-namespaced cache.
3842
// 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
43+
// will list for all the namespaces that this knows about. By default this will create
44+
// a global cache for cluster scoped resource (having empty namespace). Note that this is not intended
4045
// to be used for excluding namespaces, this is better done via a Predicate. Also note that
4146
// you may face performance issues when using this with a high number of namespaces.
4247
func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
@@ -45,6 +50,8 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
4550
if err != nil {
4651
return nil, err
4752
}
53+
// create a cache for cluster scoped resources
54+
namespaces = append(namespaces, globalCache)
4855
caches := map[string]Cache{}
4956
for _, ns := range namespaces {
5057
opts.Namespace = ns
@@ -54,7 +61,7 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
5461
}
5562
caches[ns] = c
5663
}
57-
return &multiNamespaceCache{namespaceToCache: caches, Scheme: opts.Scheme}, nil
64+
return &multiNamespaceCache{namespaceToCache: caches, Scheme: opts.Scheme, RESTMapper: opts.Mapper}, nil
5865
}
5966
}
6067

@@ -65,6 +72,7 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
6572
type multiNamespaceCache struct {
6673
namespaceToCache map[string]Cache
6774
Scheme *runtime.Scheme
75+
RESTMapper meta.RESTMapper
6876
}
6977

7078
var _ Cache = &multiNamespaceCache{}
@@ -127,6 +135,17 @@ func (c *multiNamespaceCache) IndexField(ctx context.Context, obj client.Object,
127135
}
128136

129137
func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
138+
isNamespaced, err := objectutil.IsAPINamespaced(obj, c.Scheme, c.RESTMapper)
139+
if err != nil {
140+
return err
141+
}
142+
143+
if !isNamespaced {
144+
// Look into the global cache to fetch the object
145+
cache := c.namespaceToCache[globalCache]
146+
return cache.Get(ctx, key, obj)
147+
}
148+
130149
cache, ok := c.namespaceToCache[key.Namespace]
131150
if !ok {
132151
return fmt.Errorf("unable to get: %v because of unknown namespace for the cache", key)
@@ -138,6 +157,18 @@ func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj
138157
func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
139158
listOpts := client.ListOptions{}
140159
listOpts.ApplyOptions(opts)
160+
161+
isNamespaced, err := objectutil.IsAPINamespaced(list, c.Scheme, c.RESTMapper)
162+
if err != nil {
163+
return err
164+
}
165+
166+
if !isNamespaced {
167+
// Look at the global cache to get the objects with the specified GVK
168+
cache := c.namespaceToCache[globalCache]
169+
return cache.List(ctx, list, opts...)
170+
}
171+
141172
if listOpts.Namespace != corev1.NamespaceAll {
142173
cache, ok := c.namespaceToCache[listOpts.Namespace]
143174
if !ok {

pkg/client/namespaced_client.go

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func (n *namespacedClient) RESTMapper() meta.RESTMapper {
5757

5858
// isNamespaced returns true if the object is namespace scoped.
5959
// For unstructured objects the gvk is found from the object itself.
60+
// TODO: this is repetitive code. Remove this and use ojectutil.IsNamespaced.
6061
func isNamespaced(c Client, obj runtime.Object) (bool, error) {
6162
var gvk schema.GroupVersionKind
6263
var err error

pkg/internal/objectutil/filter.go pkg/internal/objectutil/objectutil.go

+31
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ limitations under the License.
1717
package objectutil
1818

1919
import (
20+
"errors"
21+
"fmt"
22+
23+
"k8s.io/apimachinery/pkg/api/meta"
2024
apimeta "k8s.io/apimachinery/pkg/api/meta"
2125
"k8s.io/apimachinery/pkg/labels"
2226
"k8s.io/apimachinery/pkg/runtime"
27+
"k8s.io/apimachinery/pkg/runtime/schema"
28+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2329
)
2430

2531
// FilterWithLabels returns a copy of the items in objs matching labelSel
@@ -40,3 +46,28 @@ func FilterWithLabels(objs []runtime.Object, labelSel labels.Selector) ([]runtim
4046
}
4147
return outItems, nil
4248
}
49+
50+
// IsAPINamespaced returns true if the object is namespace scoped.
51+
// For unstructured objects the gvk is found from the object itself.
52+
func IsAPINamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper apimeta.RESTMapper) (bool, error) {
53+
gvk, err := apiutil.GVKForObject(obj, scheme)
54+
if err != nil {
55+
return false, err
56+
}
57+
58+
restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
59+
if err != nil {
60+
return false, fmt.Errorf("failed to get restmapping: %w", err)
61+
}
62+
63+
scope := restmapping.Scope.Name()
64+
65+
if scope == "" {
66+
return false, errors.New("Scope cannot be identified. Empty scope returned")
67+
}
68+
69+
if scope != meta.RESTScopeNameRoot {
70+
return true, nil
71+
}
72+
return false, nil
73+
}

0 commit comments

Comments
 (0)