Skip to content

Commit 968daa8

Browse files
authored
Merge pull request #2528 from kubernetes-sigs/allow-all-but
✨ Cache: Allow defining options that apply to all namespaces that themselves have no explicit config
2 parents d94cf60 + 414b86e commit 968daa8

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)