From 9ee06039ad34e12db4dcdfdbbafc009232ee255c Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Mon, 1 Nov 2021 11:22:48 -0400 Subject: [PATCH] :sparkles: Allow configuring a default cache selector It is already possible to configure cache selectors per gvk, but it is not possible to default this selector for all types. This change adds that. --- pkg/cache/cache.go | 11 +++- pkg/cache/cache_test.go | 98 +++++++++++++++++++++++++++++ pkg/cache/internal/informers_map.go | 8 +-- pkg/cache/internal/selector.go | 11 ++++ 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index f08983335d..4179182516 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -90,6 +90,8 @@ type Informer interface { type ObjectSelector internal.Selector // SelectorsByObject associate a client.Object's GVK to a field/label selector. +// There is also `DefaultSelector` to set a global default (which will be overriden by +// a more specific setting here, if any). type SelectorsByObject map[client.Object]ObjectSelector // Options are the optional arguments for creating a new InformersMap object. @@ -117,6 +119,10 @@ type Options struct { // [2] https://pkg.go.dev/k8s.io/apimachinery/pkg/fields#Set SelectorsByObject SelectorsByObject + // DefaultSelector will be used as selectors for all object types + // that do not have a selector in SelectorsByObject defined. + DefaultSelector ObjectSelector + // UnsafeDisableDeepCopyByObject indicates not to deep copy objects during get or // list objects per GVK at the specified object. // Be very careful with this, when enabled you must DeepCopy any object before mutating it, @@ -132,7 +138,7 @@ func New(config *rest.Config, opts Options) (Cache, error) { if err != nil { return nil, err } - selectorsByGVK, err := convertToSelectorsByGVK(opts.SelectorsByObject, opts.Scheme) + selectorsByGVK, err := convertToSelectorsByGVK(opts.SelectorsByObject, opts.DefaultSelector, opts.Scheme) if err != nil { return nil, err } @@ -194,7 +200,7 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { return opts, nil } -func convertToSelectorsByGVK(selectorsByObject SelectorsByObject, scheme *runtime.Scheme) (internal.SelectorsByGVK, error) { +func convertToSelectorsByGVK(selectorsByObject SelectorsByObject, defaultSelector ObjectSelector, scheme *runtime.Scheme) (internal.SelectorsByGVK, error) { selectorsByGVK := internal.SelectorsByGVK{} for object, selector := range selectorsByObject { gvk, err := apiutil.GVKForObject(object, scheme) @@ -203,6 +209,7 @@ func convertToSelectorsByGVK(selectorsByObject SelectorsByObject, scheme *runtim } selectorsByGVK[gvk] = internal.Selector(selector) } + selectorsByGVK[schema.GroupVersionKind{}] = internal.Selector(defaultSelector) return selectorsByGVK, nil } diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 9ae290636c..4593cd17f5 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "sort" + "strconv" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" @@ -73,6 +74,33 @@ func createPodWithLabels(name, namespace string, restartPolicy corev1.RestartPol return pod } +func createSvc(name, namespace string, cl client.Client) client.Object { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 1}}, + }, + } + err := cl.Create(context.Background(), svc) + Expect(err).NotTo(HaveOccurred()) + return svc +} + +func createSA(name, namespace string, cl client.Client) client.Object { + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + err := cl.Create(context.Background(), sa) + Expect(err).NotTo(HaveOccurred()) + return sa +} + func createPod(name, namespace string, restartPolicy corev1.RestartPolicy) client.Object { return createPodWithLabels(name, namespace, restartPolicy, nil) } @@ -93,6 +121,76 @@ var _ = Describe("Multi-Namespace Informer Cache", func() { var _ = Describe("Informer Cache without DeepCopy", func() { CacheTest(cache.New, cache.Options{UnsafeDisableDeepCopyByObject: cache.DisableDeepCopyByObject{cache.ObjectAll{}: true}}) }) +var _ = Describe("Cache with selectors", func() { + defer GinkgoRecover() + var ( + informerCache cache.Cache + informerCacheCtx context.Context + informerCacheCancel context.CancelFunc + ) + + BeforeEach(func() { + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + Expect(cfg).NotTo(BeNil()) + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + err = ensureNamespace(testNamespaceOne, cl) + Expect(err).NotTo(HaveOccurred()) + err = ensureNamespace(testNamespaceTwo, cl) + Expect(err).NotTo(HaveOccurred()) + for idx, namespace := range []string{testNamespaceOne, testNamespaceTwo} { + _ = createSA("test-sa-"+strconv.Itoa(idx), namespace, cl) + _ = createSvc("test-svc-"+strconv.Itoa(idx), namespace, cl) + } + + opts := cache.Options{ + SelectorsByObject: cache.SelectorsByObject{ + &corev1.ServiceAccount{}: {Field: fields.OneTermEqualSelector("metadata.namespace", testNamespaceOne)}, + }, + DefaultSelector: cache.ObjectSelector{Field: fields.OneTermEqualSelector("metadata.namespace", testNamespaceTwo)}, + } + + By("creating the informer cache") + informerCache, err = cache.New(cfg, opts) + Expect(err).NotTo(HaveOccurred()) + By("running the cache and waiting for it to sync") + // pass as an arg so that we don't race between close and re-assign + go func(ctx context.Context) { + defer GinkgoRecover() + Expect(informerCache.Start(ctx)).To(Succeed()) + }(informerCacheCtx) + Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + }) + + AfterEach(func() { + ctx := context.Background() + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + for idx, namespace := range []string{testNamespaceOne, testNamespaceTwo} { + err = cl.Delete(ctx, &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "test-sa-" + strconv.Itoa(idx)}}) + Expect(err).NotTo(HaveOccurred()) + err = cl.Delete(ctx, &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "test-svc-" + strconv.Itoa(idx)}}) + Expect(err).NotTo(HaveOccurred()) + } + informerCacheCancel() + }) + + It("Should list serviceaccounts and find exactly one in namespace "+testNamespaceOne, func() { + var sas corev1.ServiceAccountList + err := informerCache.List(informerCacheCtx, &sas) + Expect(err).NotTo(HaveOccurred()) + Expect(len(sas.Items)).To(Equal(1)) + Expect(sas.Items[0].Namespace).To(Equal(testNamespaceOne)) + }) + + It("Should list services and find exactly one in namespace "+testNamespaceTwo, func() { + var svcs corev1.ServiceList + err := informerCache.List(informerCacheCtx, &svcs) + Expect(err).NotTo(HaveOccurred()) + Expect(len(svcs.Items)).To(Equal(1)) + Expect(svcs.Items[0].Namespace).To(Equal(testNamespaceTwo)) + }) +}) func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (cache.Cache, error), opts cache.Options) { Describe("Cache test", func() { diff --git a/pkg/cache/internal/informers_map.go b/pkg/cache/internal/informers_map.go index f8e957343f..3366742a77 100644 --- a/pkg/cache/internal/informers_map.go +++ b/pkg/cache/internal/informers_map.go @@ -277,19 +277,19 @@ func createStructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformer // Create a new ListWatch for the obj return &cache.ListWatch{ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { - ip.selectors[gvk].ApplyToList(&opts) + ip.selectors.forGVK(gvk).ApplyToList(&opts) res := listObj.DeepCopyObject() - namespace := restrictNamespaceBySelector(ip.namespace, ip.selectors[gvk]) + namespace := restrictNamespaceBySelector(ip.namespace, ip.selectors.forGVK(gvk)) isNamespaceScoped := namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot err := client.Get().NamespaceIfScoped(namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Do(ctx).Into(res) return res, err }, // Setup the watch function WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { - ip.selectors[gvk].ApplyToList(&opts) + ip.selectors.forGVK(gvk).ApplyToList(&opts) // Watch needs to be set to true separately opts.Watch = true - namespace := restrictNamespaceBySelector(ip.namespace, ip.selectors[gvk]) + namespace := restrictNamespaceBySelector(ip.namespace, ip.selectors.forGVK(gvk)) isNamespaceScoped := namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot return client.Get().NamespaceIfScoped(namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Watch(ctx) }, diff --git a/pkg/cache/internal/selector.go b/pkg/cache/internal/selector.go index cd9c580008..4eff32fb35 100644 --- a/pkg/cache/internal/selector.go +++ b/pkg/cache/internal/selector.go @@ -26,6 +26,17 @@ import ( // SelectorsByGVK associate a GroupVersionKind to a field/label selector. type SelectorsByGVK map[schema.GroupVersionKind]Selector +func (s SelectorsByGVK) forGVK(gvk schema.GroupVersionKind) Selector { + if specific, found := s[gvk]; found { + return specific + } + if defaultSelector, found := s[schema.GroupVersionKind{}]; found { + return defaultSelector + } + + return Selector{} +} + // Selector specify the label/field selector to fill in ListOptions. type Selector struct { Label labels.Selector