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