Skip to content

🐛 Modify multinamespaced cache to support cluster scoped resources #1418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions pkg/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,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
35 changes: 33 additions & 2 deletions pkg/cache/multi_namespace_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ import (
"k8s.io/client-go/rest"
toolscache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
)

// 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
const globalCache = "_cluster-scope"

// 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 +50,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 All @@ -54,7 +61,7 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
}
caches[ns] = c
}
return &multiNamespaceCache{namespaceToCache: caches, Scheme: opts.Scheme}, nil
return &multiNamespaceCache{namespaceToCache: caches, Scheme: opts.Scheme, RESTMapper: opts.Mapper}, nil
}
}

Expand All @@ -65,6 +72,7 @@ func MultiNamespacedCacheBuilder(namespaces []string) NewCacheFunc {
type multiNamespaceCache struct {
namespaceToCache map[string]Cache
Scheme *runtime.Scheme
RESTMapper meta.RESTMapper
}

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

func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
isNamespaced, err := objectutil.IsAPINamespaced(obj, c.Scheme, c.RESTMapper)
if err != nil {
return err
}

if !isNamespaced {
// Look into the global cache to fetch the object
cache := c.namespaceToCache[globalCache]
return cache.Get(ctx, key, obj)
}

cache, ok := c.namespaceToCache[key.Namespace]
if !ok {
return fmt.Errorf("unable to get: %v because of unknown namespace for the cache", key)
Expand All @@ -138,6 +157,18 @@ func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj
func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
listOpts := client.ListOptions{}
listOpts.ApplyOptions(opts)

isNamespaced, err := objectutil.IsAPINamespaced(list, c.Scheme, c.RESTMapper)
if err != nil {
return err
}

if !isNamespaced {
// Look at the global cache to get the objects with the specified GVK
cache := c.namespaceToCache[globalCache]
return cache.List(ctx, list, opts...)
}

if listOpts.Namespace != corev1.NamespaceAll {
cache, ok := c.namespaceToCache[listOpts.Namespace]
if !ok {
Expand Down
1 change: 1 addition & 0 deletions pkg/client/namespaced_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (n *namespacedClient) RESTMapper() meta.RESTMapper {

// isNamespaced returns true if the object is namespace scoped.
// For unstructured objects the gvk is found from the object itself.
// TODO: this is repetitive code. Remove this and use ojectutil.IsNamespaced.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR references a bug, will do refactoring in follow up.

func isNamespaced(c Client, obj runtime.Object) (bool, error) {
var gvk schema.GroupVersionKind
var err error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ limitations under the License.
package objectutil
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have renamed this file from filter.go to objectutil.go to make it generic


import (
"errors"
"fmt"

"k8s.io/apimachinery/pkg/api/meta"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

// FilterWithLabels returns a copy of the items in objs matching labelSel
Expand All @@ -40,3 +46,28 @@ func FilterWithLabels(objs []runtime.Object, labelSel labels.Selector) ([]runtim
}
return outItems, nil
}

// IsAPINamespaced returns true if the object is namespace scoped.
// For unstructured objects the gvk is found from the object itself.
func IsAPINamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper apimeta.RESTMapper) (bool, error) {
gvk, err := apiutil.GVKForObject(obj, scheme)
if err != nil {
return false, err
}

restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
if err != nil {
return false, fmt.Errorf("failed to get restmapping: %w", err)
}

scope := restmapping.Scope.Name()

if scope == "" {
return false, errors.New("Scope cannot be identified. Empty scope returned")
}

if scope != meta.RESTScopeNameRoot {
return true, nil
}
return false, nil
}