Skip to content

Commit 5ce3c59

Browse files
authoredJan 19, 2024
Introduce scope configuration for internal caches (#133)
This patch enhances the runtime internal caches by introducing a configuration mechanism to tailor their behavior. The caches can now be configured to watch a specific set of namespaces (instead of the previous "all or nothing" style). This is intended to be complete the multi-namspace-watch mode feature. Now when a user configures a controller to watch a set of namespaces the ACK runtime caches will also respect that scope. In addition this commit adds `kube-node-lease` to the set of namespace ignored by default (This namespace is dedicated to node Leases objects). Signed-off-by: Amine Hilaly <hilalyamine@gmail.com> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 585594d commit 5ce3c59

File tree

4 files changed

+202
-21
lines changed

4 files changed

+202
-21
lines changed
 

‎pkg/runtime/cache/cache.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ func init() {
5858
)
5959
}
6060

61+
// Config is used to configure the caches.
62+
type Config struct {
63+
// WatchScope is a list of namespaces to watch for resources
64+
WatchScope []string
65+
// Ignored is a list of namespaces to ignore
66+
Ignored []string
67+
}
68+
6169
// Caches is used to interact with the different caches
6270
type Caches struct {
6371
// stopCh is a channel use to stop all the
@@ -72,10 +80,10 @@ type Caches struct {
7280
}
7381

7482
// New instantiate a new Caches object.
75-
func New(log logr.Logger) Caches {
83+
func New(log logr.Logger, config Config) Caches {
7684
return Caches{
7785
Accounts: NewAccountCache(log),
78-
Namespaces: NewNamespaceCache(log),
86+
Namespaces: NewNamespaceCache(log, config.WatchScope, config.Ignored),
7987
}
8088
}
8189

‎pkg/runtime/cache/namespace.go

+44-16
Original file line numberDiff line numberDiff line change
@@ -80,51 +80,79 @@ type NamespaceCache struct {
8080
log logr.Logger
8181
// namespaceInfos maps namespaces names to their known namespaceInfo
8282
namespaceInfos map[string]*namespaceInfo
83+
// watchScope is the list of namespaces we are watching
84+
watchScope []string
85+
// ignored is the list of namespaces we are ignoring
86+
ignored []string
8387
}
8488

8589
// NewNamespaceCache instanciate a new NamespaceCache.
86-
func NewNamespaceCache(log logr.Logger) *NamespaceCache {
90+
func NewNamespaceCache(log logr.Logger, watchScope []string, ignored []string) *NamespaceCache {
8791
return &NamespaceCache{
8892
log: log.WithName("cache.namespace"),
8993
namespaceInfos: make(map[string]*namespaceInfo),
94+
ignored: ignored,
95+
watchScope: watchScope,
9096
}
9197
}
9298

93-
// isIgnoredNamespace returns true if an object is of type corev1.Namespace and
94-
// its metadata name is the ACK system namespace, 'kube-system' or
95-
// 'kube-public'
96-
func isIgnoredNamespace(raw interface{}) bool {
97-
object, ok := raw.(*corev1.Namespace)
98-
return ok &&
99-
(object.ObjectMeta.Name == ackSystemNamespace ||
100-
object.ObjectMeta.Name == "kube-system" ||
101-
object.ObjectMeta.Name == "kube-public")
99+
// isIgnoredNamespace returns true if the namespace is ignored
100+
func (c *NamespaceCache) isIgnoredNamespace(namespace string) bool {
101+
for _, ns := range c.ignored {
102+
if namespace == ns {
103+
return true
104+
}
105+
}
106+
return false
107+
}
108+
109+
// inWatchScope returns true if the namespace is in the watch scope
110+
func (c *NamespaceCache) inWatchScope(namespace string) bool {
111+
if len(c.watchScope) == 0 {
112+
return true
113+
}
114+
for _, ns := range c.watchScope {
115+
if namespace == ns {
116+
return true
117+
}
118+
}
119+
return false
120+
}
121+
122+
// approvedNamespace returns true if the namespace is not ignored and is in the watch scope
123+
func (c *NamespaceCache) approvedNamespace(namespace string) bool {
124+
return !c.isIgnoredNamespace(namespace) && c.inWatchScope(namespace)
102125
}
103126

104127
// Run instantiate a new shared informer for namespaces and runs it to begin processing items.
105128
func (c *NamespaceCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
129+
c.log.V(1).Info("Starting namespace cache", "watchScope", c.watchScope, "ignored", c.ignored)
106130
informer := informersv1.NewNamespaceInformer(
107131
clientSet,
108132
informerResyncPeriod,
109-
k8scache.Indexers{},
133+
k8scache.Indexers{
134+
k8scache.NamespaceIndex: k8scache.MetaNamespaceIndexFunc,
135+
},
110136
)
111137
informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
112138
AddFunc: func(obj interface{}) {
113-
if !isIgnoredNamespace(obj) {
114-
ns := obj.(*corev1.Namespace)
139+
// It is guaranteed that the object is of type corev1.Namespace
140+
ns := obj.(*corev1.Namespace)
141+
if c.approvedNamespace(ns.ObjectMeta.Name) {
115142
c.setNamespaceInfoFromK8sObject(ns)
116143
c.log.V(1).Info("created namespace", "name", ns.ObjectMeta.Name)
117144
}
118145
},
119146
UpdateFunc: func(orig, desired interface{}) {
120-
if !isIgnoredNamespace(desired) {
121-
ns := desired.(*corev1.Namespace)
147+
ns := desired.(*corev1.Namespace)
148+
if c.approvedNamespace(ns.ObjectMeta.Name) {
122149
c.setNamespaceInfoFromK8sObject(ns)
123150
c.log.V(1).Info("updated namespace", "name", ns.ObjectMeta.Name)
124151
}
125152
},
126153
DeleteFunc: func(obj interface{}) {
127-
if !isIgnoredNamespace(obj) {
154+
ns := obj.(*corev1.Namespace)
155+
if c.approvedNamespace(ns.ObjectMeta.Name) {
128156
ns := obj.(*corev1.Namespace)
129157
c.deleteNamespaceInfo(ns.ObjectMeta.Name)
130158
c.log.V(1).Info("deleted namespace", "name", ns.ObjectMeta.Name)

‎pkg/runtime/cache/namespace_test.go

+108-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package cache_test
1515

1616
import (
1717
"context"
18+
"io"
1819
"testing"
1920
"time"
2021

@@ -49,7 +50,7 @@ func TestNamespaceCache(t *testing.T) {
4950
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))
5051

5152
// initlizing account cache
52-
namespaceCache := ackrtcache.NewNamespaceCache(fakeLogger)
53+
namespaceCache := ackrtcache.NewNamespaceCache(fakeLogger, []string{}, []string{})
5354
stopCh := make(chan struct{})
5455

5556
namespaceCache.Run(k8sClient, stopCh)
@@ -129,3 +130,109 @@ func TestNamespaceCache(t *testing.T) {
129130
_, ok = namespaceCache.GetDefaultRegion(testNamespace1)
130131
require.False(t, ok)
131132
}
133+
134+
func TestScopedNamespaceCache(t *testing.T) {
135+
defaultConfig := ackrtcache.Config{
136+
WatchScope: []string{"watch-scope", "watch-scope-2"},
137+
Ignored: []string{"ignored", "ignored-2"},
138+
}
139+
140+
testCases := []struct {
141+
name string
142+
createNamespace string
143+
expectCacheHit bool
144+
cacheConfig ackrtcache.Config
145+
}{
146+
{
147+
name: "namespace in scope",
148+
createNamespace: "watch-scope",
149+
expectCacheHit: true,
150+
cacheConfig: defaultConfig,
151+
},
152+
{
153+
name: "namespace not in scope",
154+
createNamespace: "watch-scope-3",
155+
expectCacheHit: false,
156+
cacheConfig: defaultConfig,
157+
},
158+
{
159+
name: "namespace in ignored",
160+
createNamespace: "ignored",
161+
expectCacheHit: false,
162+
cacheConfig: defaultConfig,
163+
},
164+
{
165+
name: "namespace is nor in scope or ignored",
166+
createNamespace: "random-penguin",
167+
expectCacheHit: false,
168+
cacheConfig: defaultConfig,
169+
},
170+
{
171+
name: "namespace is in scope and ignored",
172+
createNamespace: "watch-scope-2",
173+
expectCacheHit: true,
174+
cacheConfig: defaultConfig,
175+
},
176+
{
177+
name: "cache watching all namespaces - namespace in scope",
178+
cacheConfig: ackrtcache.Config{},
179+
createNamespace: "watch-scope",
180+
expectCacheHit: true,
181+
},
182+
{
183+
name: "cache watching all namespaces - namespace is ignored",
184+
cacheConfig: ackrtcache.Config{Ignored: []string{"kube-system"}},
185+
createNamespace: "kube-system",
186+
expectCacheHit: false,
187+
},
188+
}
189+
190+
for _, tc := range testCases {
191+
t.Run(tc.name, func(t *testing.T) {
192+
// create a fake k8s client and fake watcher
193+
k8sClient := k8sfake.NewSimpleClientset()
194+
watcher := watch.NewFake()
195+
k8sClient.PrependWatchReactor("random-penguin", k8stesting.DefaultWatchReactor(watcher, nil))
196+
197+
// New logger writing to specific buffer
198+
zapOptions := ctrlrtzap.Options{
199+
Development: true,
200+
Level: zapcore.InfoLevel,
201+
DestWriter: io.Discard,
202+
}
203+
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))
204+
205+
// initlizing account cache
206+
namespaceCache := ackrtcache.NewNamespaceCache(fakeLogger, tc.cacheConfig.WatchScope, tc.cacheConfig.Ignored)
207+
stopCh := make(chan struct{})
208+
209+
namespaceCache.Run(k8sClient, stopCh)
210+
211+
// Create namespace with name testNamespace1
212+
_, err := k8sClient.CoreV1().Namespaces().Create(
213+
context.Background(),
214+
newNamespace(tc.createNamespace),
215+
metav1.CreateOptions{},
216+
)
217+
require.Nil(t, err)
218+
219+
// Need a better way to wait for the cache to be updated
220+
// Thinking informer.WaitForCacheSync() ~ but it's not exported
221+
time.Sleep(time.Millisecond * 50)
222+
223+
_, found := namespaceCache.GetDefaultRegion(tc.createNamespace)
224+
require.Equal(t, tc.expectCacheHit, found)
225+
})
226+
}
227+
}
228+
229+
func newNamespace(name string) *corev1.Namespace {
230+
return &corev1.Namespace{
231+
ObjectMeta: metav1.ObjectMeta{
232+
Name: name,
233+
Annotations: map[string]string{
234+
ackv1alpha1.AnnotationDefaultRegion: "us-west-2",
235+
},
236+
},
237+
}
238+
}

‎pkg/runtime/service_controller.go

+40-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package runtime
1515

1616
import (
17+
"fmt"
1718
"strings"
1819
"sync"
1920

@@ -34,6 +35,18 @@ import (
3435
acktypes "github.com/aws-controllers-k8s/runtime/pkg/types"
3536
)
3637

38+
const (
39+
// NamespaceKubeNodeLease is the name of the Kubernetes namespace that
40+
// contains the kube-node-lease resources (used for node hearthbeats)
41+
NamespaceKubeNodeLease = "kube-node-lease"
42+
// NamespacePublic is the name of the Kubernetes namespace that contains
43+
// the public info (ConfigMaps)
44+
NamespaceKubePublic = "kube-public"
45+
// NamespaceSystem is the name of the Kubernetes namespace where we place
46+
// system components.
47+
NamespaceKubeSystem = "kube-system"
48+
)
49+
3750
// serviceController wraps a number of `controller-runtime.Reconciler` that are
3851
// related to a specific AWS service API.
3952
type serviceController struct {
@@ -187,13 +200,38 @@ func (c *serviceController) BindControllerManager(mgr ctrlrt.Manager, cfg ackcfg
187200
c.metaLock.Lock()
188201
defer c.metaLock.Unlock()
189202

190-
cache := ackrtcache.New(c.log)
191-
if cfg.WatchNamespace == "" {
203+
namespaces, err := cfg.GetWatchNamespaces()
204+
if err != nil {
205+
return fmt.Errorf("unable to get watch namespaces: %v", err)
206+
}
207+
208+
cache := ackrtcache.New(c.log, ackrtcache.Config{
209+
WatchScope: namespaces,
210+
// Default to ignoring the kube-system, kube-public, and
211+
// kube-node-lease namespaces.
212+
// NOTE: Maybe we should make this configurable? It's not clear that
213+
// we'd ever want to watch these namespaces.
214+
Ignored: []string{
215+
NamespaceKubeSystem,
216+
NamespaceKubePublic,
217+
NamespaceKubeNodeLease,
218+
}},
219+
)
220+
// We want to run the caches if the length of the namespaces slice is
221+
// either 0 (watching all namespaces) or greater than 1 (watching multiple
222+
// namespaces).
223+
//
224+
// The caches are only used for cross account resource management. If the
225+
// controller is not configured to watch multiple namespaces, then we don't
226+
// need to run the caches.
227+
if len(namespaces) == 0 || len(namespaces) >= 2 {
192228
clusterConfig := mgr.GetConfig()
193229
clientSet, err := kubernetes.NewForConfig(clusterConfig)
194230
if err != nil {
195231
return err
196232
}
233+
// Run the caches. This will not block as the caches are run in
234+
// separate goroutines.
197235
cache.Run(clientSet)
198236
}
199237

0 commit comments

Comments
 (0)
Please sign in to comment.