|
| 1 | +Filter cache ListWatch using selectors |
| 2 | +=================== |
| 3 | +## Motivation |
| 4 | + |
| 5 | +Controller-Runtime controllers use a cache to subscribe to events from |
| 6 | +Kubernetes objects and to read those objects more efficiently by avoiding |
| 7 | +to call out to the API. This cache is backed by Kubernetes informers. |
| 8 | + |
| 9 | +The only way to filter this cache is by namespace and resource type. |
| 10 | +In cases where a controller is only interested in a small subset of objects |
| 11 | +(for example all pods on a node), this might end up not being efficient enough. |
| 12 | + |
| 13 | +Requests to a client backed by a filtered cache for objects that do not match |
| 14 | +the filter will never return anything, so we need to make sure that we properly |
| 15 | +warn users to only use this when they are sure they know what they are doing. |
| 16 | + |
| 17 | +This proposal sidesteps the issue of "How to we plug this into the cache-backed |
| 18 | +client so that users get feedback when they request something that is |
| 19 | +not matching the caches filter" by only implementing the filter logic in the |
| 20 | +cache package. This allows advanced users to combine a filtered cache with the |
| 21 | +already existing `NewCacheFunc` option in the manager and cluster package, |
| 22 | +while simultaneously hiding it from newer users that might not be aware of the |
| 23 | +implications and the associated foot-shoot potential. |
| 24 | + |
| 25 | +The only alternative today to get a filtered cache with controller-runtime is |
| 26 | +to build it out-of tree. Because such a cache would mostly copy the existing |
| 27 | +cache and add just some options, this is not great for consumers. |
| 28 | + |
| 29 | +This proposal is related to the following issue [2] |
| 30 | + |
| 31 | +## Proposal |
| 32 | + |
| 33 | +Add a new selector code at `pkg/cache/internal/selector.go` with common structs |
| 34 | +and helpers |
| 35 | +```golang |
| 36 | +package internal |
| 37 | + |
| 38 | +... |
| 39 | + |
| 40 | +// SelectorsByObject associate a runtime.Object to a field/label selector |
| 41 | +type SelectorsByObject map[client.Object]Selector |
| 42 | + |
| 43 | +// SelectorsByGVK associate a GroupVersionResource to a field/label selector |
| 44 | +type SelectorsByGVK map[schema.GroupVersionKind]Selector |
| 45 | + |
| 46 | +// Selector specify the label/field selector to fill in ListOptions |
| 47 | +type Selector struct { |
| 48 | + Label labels.Selector |
| 49 | + Field fields.Selector |
| 50 | +} |
| 51 | + |
| 52 | +// ApplyToList fill in ListOptions LabelSelector and FieldSelector if needed |
| 53 | +func (s Selector) ApplyToList(listOpts *metav1.ListOptions) { |
| 54 | +... |
| 55 | +} |
| 56 | + |
| 57 | + |
| 58 | +Add a type alias to `pkg/cache/cache.go` to internal |
| 59 | + |
| 60 | +```golang |
| 61 | +
|
| 62 | +type SelectorsByObject internal.SelectorsByObject |
| 63 | +
|
| 64 | +``` |
| 65 | + |
| 66 | +Extend `cache.Options` as follows: |
| 67 | + |
| 68 | +```golang |
| 69 | +type Options struct { |
| 70 | + Scheme *runtime.Scheme |
| 71 | + Mapper meta.RESTMapper |
| 72 | + Resync *time.Duration |
| 73 | + Namespace string |
| 74 | + SelectorsByObject SelectorsByObject |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +Add new builder function that will return a cache constructor using the passed |
| 79 | +cache.Options, users can set SelectorsByObject there to filter out cache, it |
| 80 | +will convert SelectorByObject to SelectorsByGVK |
| 81 | + |
| 82 | + |
| 83 | +```golang |
| 84 | +func BuilderWithOptions(options cache.Options) NewCacheFunc { |
| 85 | +... |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +is passed to informer's ListWatch and add the filtering option: |
| 90 | +
|
| 91 | +```golang |
| 92 | +
|
| 93 | +# At pkg/cache/internal/informers_map.go |
| 94 | +
|
| 95 | +ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { |
| 96 | + ip.selectors[gvk].ApplyToList(&opts) |
| 97 | +... |
| 98 | +``` |
| 99 | +
|
| 100 | +Here is a PR with the implementatin at the `pkg/cache` part [3] |
| 101 | +
|
| 102 | +## Example |
| 103 | +
|
| 104 | +User will override `NewCache` function to make clear that they know exactly the |
| 105 | +implications of using a different cache than the default one |
| 106 | +
|
| 107 | +```golang |
| 108 | + ctrl.Options.NewCache = cache.BuilderWithOptions(cache.Options{ |
| 109 | + SelectorsByObject: cache.SelectorsByObject{ |
| 110 | + &corev1.Node{}: { |
| 111 | + Field: fields.SelectorFromSet(fields.Set{"metadata.name": "node01"}), |
| 112 | + } |
| 113 | + &v1beta1.NodeNetworkState{}: { |
| 114 | + Field: fields.SelectorFromSet(fields.Set{"metadata.name": "node01"}), |
| 115 | + Label: labels.SelectorFromSet(labels.Set{"app": "kubernetes-nmstate})", |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + ) |
| 120 | +``` |
| 121 | +
|
| 122 | +
|
| 123 | +[1] https://github.com/nmstate/kubernetes-nmstate/pull/687 |
| 124 | +[2] https://github.com/kubernetes-sigs/controller-runtime/issues/244 |
| 125 | +[3] https://github.com/kubernetes-sigs/controller-runtime/pull/1404 |
0 commit comments