Skip to content

Commit 8b5b1a2

Browse files
authored
Merge pull request #2539 from k8s-infra-cherrypick-robot/cherry-pick-2528-to-release-0.16
[release-0.16] ✨ Cache: Allow defining options that apply to all namespaces that themselves have no explicit config
2 parents 8361246 + b9c691d commit 8b5b1a2

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
@@ -214,6 +225,11 @@ type ByObject struct {
214225
// Settings in the map value that are unset will be defaulted.
215226
// Use an empty value for the specific setting to prevent that.
216227
//
228+
// It is possible to have specific Config for just some namespaces
229+
// but cache all namespaces by using the AllNamespaces const as the map key.
230+
// This will then include all namespaces that do not have a more specific
231+
// setting.
232+
//
217233
// A nil map allows to default this to the cache's DefaultNamespaces setting.
218234
// An empty map prevents this and means that all namespaces will be cached.
219235
//
@@ -392,6 +408,9 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
392408

393409
for namespace, cfg := range opts.DefaultNamespaces {
394410
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
411+
if namespace == metav1.NamespaceAll {
412+
cfg.FieldSelector = fields.AndSelectors(appendIfNotNil(namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)), cfg.FieldSelector)...)
413+
}
395414
opts.DefaultNamespaces[namespace] = cfg
396415
}
397416

@@ -418,6 +437,15 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
418437
// 3. Default from the global defaults
419438
config = defaultConfig(config, optionDefaultsToConfig(&opts))
420439

440+
if namespace == metav1.NamespaceAll {
441+
config.FieldSelector = fields.AndSelectors(
442+
appendIfNotNil(
443+
namespaceAllSelector(maps.Keys(byObject.Namespaces)),
444+
config.FieldSelector,
445+
)...,
446+
)
447+
}
448+
421449
byObject.Namespaces[namespace] = config
422450
}
423451

@@ -457,3 +485,21 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
457485

458486
return toDefault
459487
}
488+
489+
func namespaceAllSelector(namespaces []string) fields.Selector {
490+
selectors := make([]fields.Selector, 0, len(namespaces)-1)
491+
for _, namespace := range namespaces {
492+
if namespace != metav1.NamespaceAll {
493+
selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace))
494+
}
495+
}
496+
497+
return fields.AndSelectors(selectors...)
498+
}
499+
500+
func appendIfNotNil[T comparable](a, b T) []T {
501+
if b != *new(T) {
502+
return []T{a, b}
503+
}
504+
return []T{a}
505+
}

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)