Skip to content

Commit 414b86e

Browse files
committed
✨ Cache: Allow defining options that apply to all namespaces that themselves have no explicit config
This change allows to define a cache selector config that applies to all namespaces that themselves do not have an explicit config. An example would be "Cache all namespaces without selector, except for namespace foo, there use labelSelector bar=baz". More as a side effect than intentionally, this also makes it valid to use the empty string as a key in the `Namespaces` and `byObject.Namespaces` config of the cache. This is very useful to for example have a `namespace` CLI flag that might be empty. This change is the last missing bit to finish the implementation of the [cache options design doc](./designs/cache_options.md).
1 parent 5771399 commit 414b86e

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

pkg/cache/cache.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"net/http"
2323
"time"
2424

25+
"golang.org/x/exp/maps"
2526
corev1 "k8s.io/api/core/v1"
2627
"k8s.io/apimachinery/pkg/api/meta"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2729
"k8s.io/apimachinery/pkg/fields"
2830
"k8s.io/apimachinery/pkg/labels"
2931
"k8s.io/apimachinery/pkg/runtime"
@@ -121,6 +123,10 @@ type Informer interface {
121123
HasSynced() bool
122124
}
123125

126+
// AllNamespaces should be used as the map key to deliminate namespace settings
127+
// that apply to all namespaces that themselves do not have explicit settings.
128+
const AllNamespaces = metav1.NamespaceAll
129+
124130
// Options are the optional arguments for creating a new Cache object.
125131
type Options struct {
126132
// HTTPClient is the http client to use for the REST client
@@ -172,6 +178,11 @@ type Options struct {
172178
// the namespaces in here will be watched and it will by used to default
173179
// ByObject.Namespaces for all objects if that is nil.
174180
//
181+
// It is possible to have specific Config for just some namespaces
182+
// but cache all namespaces by using the AllNamespaces const as the map key.
183+
// This will then include all namespaces that do not have a more specific
184+
// setting.
185+
//
175186
// The options in the Config that are nil will be defaulted from
176187
// the respective Default* settings.
177188
DefaultNamespaces map[string]Config
@@ -220,6 +231,11 @@ type ByObject struct {
220231
// Settings in the map value that are unset will be defaulted.
221232
// Use an empty value for the specific setting to prevent that.
222233
//
234+
// It is possible to have specific Config for just some namespaces
235+
// but cache all namespaces by using the AllNamespaces const as the map key.
236+
// This will then include all namespaces that do not have a more specific
237+
// setting.
238+
//
223239
// A nil map allows to default this to the cache's DefaultNamespaces setting.
224240
// An empty map prevents this and means that all namespaces will be cached.
225241
//
@@ -399,6 +415,9 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
399415

400416
for namespace, cfg := range opts.DefaultNamespaces {
401417
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
418+
if namespace == metav1.NamespaceAll {
419+
cfg.FieldSelector = fields.AndSelectors(appendIfNotNil(namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)), cfg.FieldSelector)...)
420+
}
402421
opts.DefaultNamespaces[namespace] = cfg
403422
}
404423

@@ -425,6 +444,15 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
425444
// 3. Default from the global defaults
426445
config = defaultConfig(config, optionDefaultsToConfig(&opts))
427446

447+
if namespace == metav1.NamespaceAll {
448+
config.FieldSelector = fields.AndSelectors(
449+
appendIfNotNil(
450+
namespaceAllSelector(maps.Keys(byObject.Namespaces)),
451+
config.FieldSelector,
452+
)...,
453+
)
454+
}
455+
428456
byObject.Namespaces[namespace] = config
429457
}
430458

@@ -464,3 +492,21 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
464492

465493
return toDefault
466494
}
495+
496+
func namespaceAllSelector(namespaces []string) fields.Selector {
497+
selectors := make([]fields.Selector, 0, len(namespaces)-1)
498+
for _, namespace := range namespaces {
499+
if namespace != metav1.NamespaceAll {
500+
selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace))
501+
}
502+
}
503+
504+
return fields.AndSelectors(selectors...)
505+
}
506+
507+
func appendIfNotNil[T comparable](a, b T) []T {
508+
if b != *new(T) {
509+
return []T{a, b}
510+
}
511+
return []T{a}
512+
}

pkg/cache/cache_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
15431543
}
15441544
return obtainedPodNames
15451545
}, ConsistOf(tc.expectedPods)))
1546+
for _, pod := range obtainedStructuredPodList.Items {
1547+
Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) //nolint:gosec // We don't retain the pointer
1548+
}
15461549

15471550
By("Checking with unstructured")
15481551
obtainedUnstructuredPodList := unstructured.UnstructuredList{}
@@ -1560,6 +1563,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
15601563
}
15611564
return obtainedPodNames
15621565
}, ConsistOf(tc.expectedPods)))
1566+
for _, pod := range obtainedUnstructuredPodList.Items {
1567+
Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) //nolint:gosec // We don't retain the pointer
1568+
}
15631569

15641570
By("Checking with metadata")
15651571
obtainedMetadataPodList := metav1.PartialObjectMetadataList{}
@@ -1577,6 +1583,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
15771583
}
15781584
return obtainedPodNames
15791585
}, ConsistOf(tc.expectedPods)))
1586+
for _, pod := range obtainedMetadataPodList.Items {
1587+
Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) //nolint:gosec // We don't retain the pointer
1588+
}
15801589
},
15811590
Entry("when selectors are empty it has to inform about all the pods", selectorsTestCase{
15821591
expectedPods: []string{"test-pod-1", "test-pod-2", "test-pod-3", "test-pod-4", "test-pod-5", "test-pod-6"},
@@ -1789,6 +1798,54 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
17891798
},
17901799
expectedPods: []string{},
17911800
}),
1801+
Entry("Only NamespaceAll in DefaultNamespaces returns all pods", selectorsTestCase{
1802+
options: cache.Options{
1803+
DefaultNamespaces: map[string]cache.Config{
1804+
metav1.NamespaceAll: {},
1805+
},
1806+
},
1807+
expectedPods: []string{"test-pod-1", "test-pod-2", "test-pod-3", "test-pod-4", "test-pod-5", "test-pod-6"},
1808+
}),
1809+
Entry("Only NamespaceAll in ByObject.Namespaces returns all pods", selectorsTestCase{
1810+
options: cache.Options{
1811+
ByObject: map[client.Object]cache.ByObject{
1812+
&corev1.Pod{}: {
1813+
Namespaces: map[string]cache.Config{
1814+
metav1.NamespaceAll: {},
1815+
},
1816+
},
1817+
},
1818+
},
1819+
expectedPods: []string{"test-pod-1", "test-pod-2", "test-pod-3", "test-pod-4", "test-pod-5", "test-pod-6"},
1820+
}),
1821+
Entry("NamespaceAll in DefaultNamespaces creates a cache for all Namespaces that are not in DefaultNamespaces", selectorsTestCase{
1822+
options: cache.Options{
1823+
DefaultNamespaces: map[string]cache.Config{
1824+
metav1.NamespaceAll: {},
1825+
testNamespaceOne: {
1826+
// labels.Nothing when serialized matches everything, so we have to construct our own "match nothing" selector
1827+
LabelSelector: labels.SelectorFromSet(labels.Set{"no-present": "not-present"})},
1828+
},
1829+
},
1830+
// All pods that are not in NamespaceOne
1831+
expectedPods: []string{"test-pod-2", "test-pod-3", "test-pod-4", "test-pod-6"},
1832+
}),
1833+
Entry("NamespaceAll in ByObject.Namespaces creates a cache for all Namespaces that are not in ByObject.Namespaces", selectorsTestCase{
1834+
options: cache.Options{
1835+
ByObject: map[client.Object]cache.ByObject{
1836+
&corev1.Pod{}: {
1837+
Namespaces: map[string]cache.Config{
1838+
metav1.NamespaceAll: {},
1839+
testNamespaceOne: {
1840+
// labels.Nothing when serialized matches everything, so we have to construct our own "match nothing" selector
1841+
LabelSelector: labels.SelectorFromSet(labels.Set{"no-present": "not-present"})},
1842+
},
1843+
},
1844+
},
1845+
},
1846+
// All pods that are not in NamespaceOne
1847+
expectedPods: []string{"test-pod-2", "test-pod-3", "test-pod-4", "test-pod-6"},
1848+
}),
17921849
)
17931850
})
17941851
Describe("as an Informer", func() {

pkg/cache/multi_namespace_cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
corev1 "k8s.io/api/core/v1"
2525
apimeta "k8s.io/apimachinery/pkg/api/meta"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apimachinery/pkg/runtime"
2728
"k8s.io/apimachinery/pkg/runtime/schema"
2829
toolscache "k8s.io/client-go/tools/cache"
@@ -210,6 +211,9 @@ func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj
210211

211212
cache, ok := c.namespaceToCache[key.Namespace]
212213
if !ok {
214+
if global, hasGlobal := c.namespaceToCache[metav1.NamespaceAll]; hasGlobal {
215+
return global.Get(ctx, key, obj, opts...)
216+
}
213217
return fmt.Errorf("unable to get: %v because of unknown namespace for the cache", key)
214218
}
215219
return cache.Get(ctx, key, obj, opts...)

0 commit comments

Comments
 (0)