Skip to content

Commit bf3e187

Browse files
committed
🏃 Proposal to extract cluster-specifics out of the Manager
1 parent e50c7b8 commit bf3e187

File tree

1 file changed

+209
-0
lines changed

1 file changed

+209
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
Move cluster-specific code out of the manager
2+
===================
3+
4+
Currently, the `./pkg/manager.Manager` has two purposes:
5+
6+
* Handle running controllers/other runnables and managing their lifecycle
7+
* Setting up various things to interact with the Kubernetes cluster,
8+
for example a `Client` and a `Cache`
9+
10+
This works very well when building controllers that talk to a single cluster,
11+
however some use-cases require controllers that interact with more than
12+
one cluster. This multi-cluster usecase is very awkward today, because it
13+
requires to construct one manager per cluster and adding all subsequent
14+
managers to the first one.
15+
16+
This document proposes to move all cluster-specific code out of the manager
17+
and into a new package and interface, that then gets embedded into the manager.
18+
This allows to keep the usage for single-cluster cases the same and introduce
19+
this change in a backwards-compatible manner.
20+
21+
Furthermore, the manager gets extended to start all caches before any other
22+
`runnables` are started.
23+
24+
25+
The new `ClusterConnector` interface will look like this:
26+
27+
```go
28+
type ClusterConnector interface {
29+
// SetFields will set cluster-specific dependencies on an object for which the object has implemented the inject
30+
// interface, specifically inject.Client, inject.Cache, inject.Scheme, inject.Config and inject.APIReader
31+
SetFields(interface{}) error
32+
33+
// GetConfig returns an initialized Config
34+
GetConfig() *rest.Config
35+
36+
// GetClient returns a client configured with the Config. This client may
37+
// not be a fully "direct" client -- it may read from a cache, for
38+
// instance. See Options.NewClient for more information on how the default
39+
// implementation works.
40+
GetClient() client.Client
41+
42+
// GetFieldIndexer returns a client.FieldIndexer configured with the client
43+
GetFieldIndexer() client.FieldIndexer
44+
45+
// GetCache returns a cache.Cache
46+
GetCache() cache.Cache
47+
48+
// GetEventRecorderFor returns a new EventRecorder for the provided name
49+
GetEventRecorderFor(name string) record.EventRecorder
50+
51+
// GetRESTMapper returns a RESTMapper
52+
GetRESTMapper() meta.RESTMapper
53+
54+
// GetAPIReader returns a reader that will be configured to use the API server.
55+
// This should be used sparingly and only when the client does not fit your
56+
// use case.
57+
GetAPIReader() client.Reader
58+
59+
// GetScheme returns an initialized Scheme
60+
GetScheme() *runtime.Scheme
61+
62+
// Start starts the ClusterConnector
63+
Start(<-chan struct{}) error
64+
}
65+
```
66+
67+
And the current `Manager` interface will change to look like this:
68+
69+
```go
70+
type Manager interface {
71+
// ClusterConnector holds objects to connect to a cluster
72+
cluserconnector.ClusterConnector
73+
74+
// Add will set requested dependencies on the component, and cause the component to be
75+
// started when Start is called. Add will inject any dependencies for which the argument
76+
// implements the inject interface - e.g. inject.Client.
77+
// Depending on if a Runnable implements LeaderElectionRunnable interface, a Runnable can be run in either
78+
// non-leaderelection mode (always running) or leader election mode (managed by leader election if enabled).
79+
Add(Runnable) error
80+
81+
// Elected is closed when this manager is elected leader of a group of
82+
// managers, either because it won a leader election or because no leader
83+
// election was configured.
84+
Elected() <-chan struct{}
85+
86+
// SetFields will set any dependencies on an object for which the object has implemented the inject
87+
// interface - e.g. inject.Client.
88+
SetFields(interface{}) error
89+
90+
// AddMetricsExtraHandler adds an extra handler served on path to the http server that serves metrics.
91+
// Might be useful to register some diagnostic endpoints e.g. pprof. Note that these endpoints meant to be
92+
// sensitive and shouldn't be exposed publicly.
93+
// If the simple path -> handler mapping offered here is not enough, a new http server/listener should be added as
94+
// Runnable to the manager via Add method.
95+
AddMetricsExtraHandler(path string, handler http.Handler) error
96+
97+
// AddHealthzCheck allows you to add Healthz checker
98+
AddHealthzCheck(name string, check healthz.Checker) error
99+
100+
// AddReadyzCheck allows you to add Readyz checker
101+
AddReadyzCheck(name string, check healthz.Checker) error
102+
103+
// Start starts all registered Controllers and blocks until the Stop channel is closed.
104+
// Returns an error if there is an error starting any controller.
105+
// If LeaderElection is used, the binary must be exited immediately after this returns,
106+
// otherwise components that need leader election might continue to run after the leader
107+
// lock was lost.
108+
Start(<-chan struct{}) error
109+
110+
// GetWebhookServer returns a webhook.Server
111+
GetWebhookServer() *webhook.Server
112+
}
113+
```
114+
115+
Furthermore, during startup, the `Manager` will use type assertion to find `ClusterConnector`s
116+
to be able to start their caches before anything else:
117+
118+
```go
119+
if cc, isClusterConnector:= runnable.(clusterconnector.ClusterConnector); isClusterConnector {
120+
m.caches = append(m.caches, cc.GetCache())
121+
}
122+
```
123+
124+
```go
125+
for idx := range cm.caches {
126+
go func(idx int) {cm.caches[idx].Start(cm.internalStop)}
127+
}
128+
129+
for _, cache := range cm.caches {
130+
cache.WaitForCacheSync(cm.internalStop)
131+
}
132+
133+
// Start all other runnables
134+
```
135+
136+
## Example
137+
138+
Below is a sample `reconciler` that will create a secret in a `mirrorCluster` for each
139+
secret found in `referenceCluster` if none of that name already exists. To keep the sample
140+
short, it won't compare the contents of the secrets.
141+
142+
```go
143+
type secretMirrorReconciler struct {
144+
referenceClusterClient, mirrorClusterClient client.Client
145+
}
146+
147+
func (r *secretMirrorReconciler) Reconcile(r reconcile.Request)(reconcile.Result, error){
148+
s := &corev1.Secret{}
149+
if err := r.referenceClusterClient.Get(context.TODO(), r.NamespacedName, s); err != nil {
150+
if kerrors.IsNotFound{ return reconcile.Result{}, nil }
151+
return reconcile.Result, err
152+
}
153+
154+
if err := r.mirrorClusterClient.Get(context.TODO(), r.NamespacedName, &corev1.Secret); err != nil {
155+
if !kerrors.IsNotFound(err) {
156+
return reconcile.Result{}, err
157+
}
158+
159+
mirrorSecret := &corev1.Secret{
160+
ObjectMeta: metav1.ObjectMeta{Namespace: s.Namespace, Name: s.Name},
161+
Data: s.Data,
162+
}
163+
return reconcile.Result{}, r.mirrorClusterClient.Create(context.TODO(), mirrorSecret)
164+
}
165+
166+
return nil
167+
}
168+
169+
func NewSecretMirrorReconciler(mgr manager.Manager, mirrorConnector clusterconnector.ClusterConnector) error {
170+
return ctrl.NewControllerManagedBy(mgr).
171+
// Watch Secrets in the reference cluster
172+
For(&corev1.Secret{}).
173+
// Watch pods in the mirror cluster
174+
Watches(
175+
source.NewKindWithCache(&corev1.Secret{}, mirrorCluster.GetCache()),
176+
&handler.EnqueueRequestForObject{},
177+
).
178+
Complete(&secretMirrorReconciler{
179+
referenceClusterClient: mgr.GetClient(),
180+
mirrorClusterClient: mirrorCluster.GetClient(),
181+
})
182+
}
183+
}
184+
185+
func main(){
186+
187+
mgr, err := manager.New(cfg1, manager.Options{})
188+
if err != nil {
189+
panic(err)
190+
}
191+
192+
mirrorClusterConnector, err := clusterconnector.New(cfg2)
193+
if err != nil {
194+
panic(err)
195+
}
196+
197+
if err := mgr.Add(mirrorConnector); err != nil {
198+
panic(err)
199+
}
200+
201+
if err := NewSecretMirrorReconciler(mgr, mirrorClusterConnector); err != nil {
202+
panic(err)
203+
}
204+
205+
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
206+
panic(err)
207+
}
208+
}
209+
```

0 commit comments

Comments
 (0)