Skip to content

Commit 120cef5

Browse files
committed
Update multi-cluster proposal with new implementation details
Signed-off-by: Marvin Beckers <[email protected]>
1 parent eb207cb commit 120cef5

File tree

1 file changed

+147
-73
lines changed

1 file changed

+147
-73
lines changed

designs/multi-cluster.md

+147-73
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Multi-Cluster Support
2-
Author: @sttts
2+
3+
Author: @sttts @embik
4+
35
Initial implementation: @vincepri
46

5-
Last Updated on: 03/26/2024
7+
Last Updated on: 12/04/2024
68

79
## Table of Contents
810

@@ -35,6 +37,10 @@ Multi-cluster use-cases require the creation of multiple managers and/or cluster
3537
objects. This proposal is about adding native support for multi-cluster use-cases
3638
to controller-runtime.
3739

40+
With this change, it will be possible to implement pluggable cluster providers
41+
that automatically start and stop watches (and thus, cluster-aware reconcilers) when
42+
the cluster provider adds ("engages") or removes ("disengages") a cluster.
43+
3844
## Motivation
3945

4046
This change is important because:
@@ -50,6 +56,7 @@ This change is important because:
5056

5157
### Goals
5258

59+
- Provide an interface for plugging in a "cluster provider", which provides a dynamic set of clusters that should be reconciled by registered controllers.
5360
- Provide a way to natively write controllers that
5461
1. (UNIFORM MULTI-CLUSTER CONTROLLER) operate on multiple clusters in a uniform way,
5562
i.e. reconciling the same resources on multiple clusters, **optionally**
@@ -59,12 +66,8 @@ This change is important because:
5966
Example: distributed `ReplicaSet` controller, reconciling `ReplicaSets` on multiple clusters.
6067
2. (AGGREGATING MULTI-CLUSTER CONTROLLER) operate on one central hub cluster aggregating information from multiple clusters.
6168

62-
Example: distributed `Deployment` controller, aggregating `ReplicaSets` back into the `Deployment` object.
69+
Example: distributed `Deployment` controller, aggregating `ReplicaSets` across multiple clusters back into a central `Deployment` object.
6370
- Allow clusters to dynamically join and leave the set of clusters a controller operates on.
64-
- Allow event sources to be cross-cluster:
65-
1. Multi-cluster events that trigger reconciliation in the one central hub cluster.
66-
2. Central hub cluster events to trigger reconciliation on multiple clusters.
67-
- Allow (informer) indexes that span multiple clusters.
6871
- Allow logical clusters where a set of clusters is actually backed by one physical informer store.
6972
- Allow 3rd-parties to plug in their multi-cluster adapter (in source code) into
7073
an existing multi-cluster-compatible code-base.
@@ -80,7 +83,7 @@ logic.
8083
### Examples
8184

8285
- Run a controller-runtime controller against a kubeconfig with arbitrary many contexts, all being reconciled.
83-
- Run a controller-runtime controller against cluster-managers like kind, Cluster-API, Open-Cluster-Manager or Hypershift.
86+
- Run a controller-runtime controller against cluster managers like kind, Cluster API, Open-Cluster-Manager or Hypershift.
8487
- Run a controller-runtime controller against a kcp shard with a wildcard watch.
8588

8689
### Non-Goals/Future Work
@@ -94,17 +97,32 @@ logic.
9497
## Proposal
9598

9699
The `ctrl.Manager` _SHOULD_ be extended to get an optional `cluster.Provider` via
97-
`ctrl.Options` implementing
100+
`ctrl.Options`, implementing:
98101

99102
```golang
100103
// pkg/cluster
104+
105+
// Provider defines methods to retrieve clusters by name. The provider is
106+
// responsible for discovering and managing the lifecycle of each cluster.
107+
//
108+
// Example: A Cluster API provider would be responsible for discovering and
109+
// managing clusters that are backed by Cluster API resources, which can live
110+
// in multiple namespaces in a single management cluster.
101111
type Provider interface {
102-
Get(ctx context.Context, clusterName string, opts ...Option) (Cluster, error)
103-
List(ctx context.Context) ([]string, error)
104-
Watch(ctx context.Context) (Watcher, error)
112+
// Get returns a cluster for the given identifying cluster name. Get
113+
// returns an existing cluster if it has been created before.
114+
Get(ctx context.Context, clusterName string) (Cluster, error)
105115
}
106116
```
117+
118+
A cluster provider is responsible for constructing a `cluster.Cluster` instance
119+
upon calls to `Get(ctx, clusterName)` and returning it. Providers should keep track
120+
of created clusters and return them again if the same name is requested. Since
121+
providers are responsible for constructing the `cluster.Cluster` instance, they
122+
can make decisions about e.g. reusing existing informers.
123+
107124
The `cluster.Cluster` _SHOULD_ be extended with a unique name identifier:
125+
108126
```golang
109127
// pkg/cluster:
110128
type Cluster interface {
@@ -113,104 +131,172 @@ type Cluster interface {
113131
}
114132
```
115133

116-
The `ctrl.Manager` will use the provider to watch clusters coming and going, and
117-
will inform runnables implementing the `cluster.AwareRunnable` interface:
134+
A new interface for cluster-aware runnables will be provided:
118135

119136
```golang
120137
// pkg/cluster
121-
type AwareRunnable interface {
138+
type Aware interface {
139+
// Engage gets called when the component should start operations for the given Cluster.
140+
// The given context is tied to the Cluster's lifecycle and will be cancelled when the
141+
// Cluster is removed or an error occurs.
142+
//
143+
// Implementers should return an error if they cannot start operations for the given Cluster,
144+
// and should ensure this operation is re-entrant and non-blocking.
145+
//
146+
// \_________________|)____.---'--`---.____
147+
// || \----.________.----/
148+
// || / / `--'
149+
// __||____/ /_
150+
// |___ \
151+
// `--------'
122152
Engage(context.Context, Cluster) error
153+
154+
// Disengage gets called when the component should stop operations for the given Cluster.
123155
Disengage(context.Context, Cluster) error
124156
}
125157
```
126-
In particular, controllers implement the `AwareRunnable` interface. They react
127-
to engaged clusters by duplicating and starting their registered `source.Source`s
128-
and `handler.EventHandler`s for each cluster through implementation of
158+
159+
`ctrl.Manager` will implement `cluster.Aware`. It is the cluster provider's responsibility
160+
to call `Engage` and `Disengage` on a `ctrl.Manager` instance when clusters join or leave
161+
the set of target clusters that should be reconciled.
162+
163+
The internal `ctrl.Manager` implementation in turn will call `Engage` and `Disengage` on all
164+
its runnables that are cluster-aware (i.e. that implement the `cluster.Aware` interface).
165+
166+
In particular, cluster-aware controllers implement the `cluster.Aware` interface and are
167+
responsible for starting watches on clusters when they are engaged. This is expressed through
168+
the interface below:
169+
129170
```golang
130-
// pkg/source
131-
type DeepCopyableSyncingSource interface {
132-
SyncingSource
133-
DeepCopyFor(cluster cluster.Cluster) DeepCopyableSyncingSource
171+
// pkg/controller
172+
type TypedMultiClusterController[request comparable] interface {
173+
cluster.Aware
174+
TypedController[request]
134175
}
176+
```
177+
178+
The multi-cluster controller implementation reacts to engaged clusters by starting
179+
a new `TypedSyncingSource` that also wraps the context passed down from the call to `Engage`,
180+
which _MUST_ be canceled by the cluster provider at the end of a cluster's lifecycle.
181+
182+
Instead of extending `reconcile.Request`, implementations _SHOULD_ bring their
183+
own request type through the generics support in `Typed*` types (`request comparable`).
135184

185+
Optionally, a passed `TypedEventHandler` will be duplicated per engaged cluster if they
186+
fullfil the following interface:
187+
188+
```golang
136189
// pkg/handler
137-
type DeepCopyableEventHandler interface {
138-
EventHandler
139-
DeepCopyFor(c cluster.Cluster) DeepCopyableEventHandler
190+
type TypedDeepCopyableEventHandler[object any, request comparable] interface {
191+
TypedEventHandler[object, request]
192+
DeepCopyFor(c cluster.Cluster) TypedDeepCopyableEventHandler[object, request]
140193
}
141194
```
142-
The standard implementing types, in particular `internal.Kind` will adhere to
143-
these interfaces.
195+
196+
This might be necessary if a BYO `TypedEventHandler` needs to store information about
197+
the engaged cluster (e.g. because the events do not supply information about the cluster in
198+
object annotations) that it has been started for.
144199

145200
The `ctrl.Manager` _SHOULD_ be extended by a `cluster.Cluster` getter:
201+
146202
```golang
147203
// pkg/manager
148204
type Manager interface {
149205
// ...
150206
GetCluster(ctx context.Context, clusterName string) (cluster.Cluster, error)
151207
}
152208
```
209+
153210
The embedded `cluster.Cluster` corresponds to `GetCluster(ctx, "")`. We call the
154211
clusters with non-empty name "provider clusters" or "enganged clusters", while
155212
the embedded cluster of the manager is called the "default cluster" or "hub
156213
cluster".
157214

158-
The `reconcile.Request` _SHOULD_ be extended by an optional `ClusterName` field:
215+
216+
### Multi-Cluster-Compatible Reconcilers
217+
218+
Reconcilers can be made multi-cluster-compatible by changing client and cache
219+
accessing code from directly accessing `mgr.GetClient()` and `mgr.GetCache()` to
220+
going through `mgr.GetCluster(clusterName).GetClient()` and
221+
`mgr.GetCluster(clusterName).GetCache()`. `clusterName` needs to be extracted from
222+
the BYO `request` type (e.g. a `clusterName` field in the type itself).
223+
224+
A typical snippet at the beginning of a reconciler to fetch the client could look like this:
225+
159226
```golang
160-
// pkg/reconile
161-
type Request struct {
162-
ClusterName string
163-
types.NamespacedName
227+
cl, err := mgr.GetCluster(ctx, req.ClusterName)
228+
if err != nil {
229+
return reconcile.Result{}, err
164230
}
231+
client := cl.GetClient()
165232
```
166233

167-
With these changes, the behaviour of controller-runtime without a set cluster
168-
provider will be unchanged.
234+
Due to the BYO `request` type, controllers need to be built like this:
169235

170-
### Multi-Cluster-Compatible Reconcilers
236+
```golang
237+
builder.TypedControllerManagedBy[ClusterRequest](mgr).
238+
Named("multi-cluster-controller").
239+
Watches(&corev1.Pod{}, &ClusterRequestEventHandler{}).
240+
Complete(reconciler)
241+
```
171242

172-
Reconcilers can be made multi-cluster-compatible by changing client and cache
173-
accessing code from directly accessing `mgr.GetClient()` and `mgr.GetCache()` to
174-
going through `mgr.GetCluster(req.ClusterName).GetClient()` and
175-
`mgr.GetCluster(req.ClusterName).GetCache()`.
243+
With `ClusterRequest` and `ClusterRequestEventHandler` being BYO types. `reconciler`
244+
can be e.g. of type `reconcile.TypedFunc[ClusterRequest]`.
245+
246+
`ClusterRequest` will likely often look like this:
176247

177-
When building a controller like
178248
```golang
179-
builder.NewControllerManagedBy(mgr).
180-
For(&appsv1.ReplicaSet{}).
181-
Owns(&v1.Pod{}).
182-
Complete(reconciler)
249+
type ClusterRequest struct {
250+
reconcile.Request
251+
ClusterName string
252+
}
183253
```
184-
with the described change to use `GetCluster(ctx, req.ClusterName)` will automatically
185-
act as *uniform multi-cluster controller*. It will reconcile resources from cluster `X`
254+
255+
Controllers that use `For` or `Owns` cannot be converted to multi-cluster controllers
256+
without changing to `Watches` as the BYO `request` type cannot be used with them:
257+
258+
```golang
259+
// pkg/builder/controller.go
260+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
261+
return fmt.Errorf("For() can only be used with reconcile.Request, got %T", *new(request))
262+
}
263+
```
264+
265+
With the described changes (use `GetCluster(ctx, clusterName)` and making `reconciler`
266+
a `TypedFunc[ClusterRequest`) an existing controller will automatically act as
267+
*uniform multi-cluster controller*. It will reconcile resources from cluster `X`
186268
in cluster `X`.
187269

188270
For a manager with `cluster.Provider`, the builder _SHOULD_ create a controller
189271
that sources events **ONLY** from the provider clusters that got engaged with
190272
the controller.
191273

192-
Controllers that should be triggered by events on the hub cluster will have to
193-
opt-in like in this example:
274+
Controllers that should be triggered by events on the hub cluster can continue
275+
to use `For` and `Owns` and explicitly pass the intention to engage only with the
276+
"default" cluster:
194277

195278
```golang
196279
builder.NewControllerManagedBy(mgr).
197-
For(&appsv1.Deployment{}, builder.InDefaultCluster).
280+
WithOptions(controller.TypedOptions{
281+
EngageWithDefaultCluster: ptr.To(true),
282+
EngageWithProviderClusters: ptr.To(false),
283+
}).
284+
For(&appsv1.Deployment{}).
198285
Owns(&v1.ReplicaSet{}).
199286
Complete(reconciler)
200287
```
201-
A mixed set of sources is possible as shown here in the example.
202288

203289
## User Stories
204290

205291
### Controller Author with no interest in multi-cluster wanting to old behaviour.
206292

207293
- Do nothing. Controller-runtime behaviour is unchanged.
208294

209-
### Multi-Cluster Integrator wanting to support cluster managers like Cluster-API or kind
295+
### Multi-Cluster Integrator wanting to support cluster managers like Cluster API or kind
210296

211297
- Implement the `cluster.Provider` interface, either via polling of the cluster registry
212298
or by watching objects in the hub cluster.
213-
- For every new cluster create an instance of `cluster.Cluster`.
299+
- For every new cluster create an instance of `cluster.Cluster` and call `mgr.Engage`.
214300

215301
### Multi-Cluster Integrator wanting to support apiservers with logical cluster (like kcp)
216302

@@ -223,7 +309,8 @@ A mixed set of sources is possible as shown here in the example.
223309
### Controller Author without self-interest in multi-cluster, but open for adoption in multi-cluster setups
224310

225311
- Replace `mgr.GetClient()` and `mgr.GetCache` with `mgr.GetCluster(req.ClusterName).GetClient()` and `mgr.GetCluster(req.ClusterName).GetCache()`.
226-
- Make manager and controller plumbing vendor'able to allow plugging in multi-cluster provider.
312+
- Switch from `For` and `Owns` builder calls to `watches`
313+
- Make manager and controller plumbing vendor'able to allow plugging in multi-cluster provider and BYO request type.
227314

228315
### Controller Author who wants to support certain multi-cluster setups
229316

@@ -234,12 +321,11 @@ A mixed set of sources is possible as shown here in the example.
234321

235322
- The standard behaviour of controller-runtime is unchanged for single-cluster controllers.
236323
- The activation of the multi-cluster mode is through attaching the `cluster.Provider` to the manager.
237-
To make it clear that the semantics are experimental, we make the `Options.provider` field private
238-
and adds `Options.WithExperimentalClusterProvider` method.
324+
To make it clear that the semantics are experimental, we name the `manager.Options` field
325+
`ExperimentalClusterProvider`.
239326
- We only extend these interfaces and structs:
240-
- `ctrl.Manager` with `GetCluster(ctx, clusterName string) (cluster.Cluster, error)`
241-
- `cluster.Cluster` with `Name() string`
242-
- `reconcile.Request` with `ClusterName string`
327+
- `ctrl.Manager` with `GetCluster(ctx, clusterName string) (cluster.Cluster, error)` and `cluster.Aware`.
328+
- `cluster.Cluster` with `Name() string`.
243329
We think that the behaviour of these extensions is well understood and hence low risk.
244330
Everything else behind the scenes is an implementation detail that can be changed
245331
at any time.
@@ -258,24 +344,12 @@ A mixed set of sources is possible as shown here in the example.
258344
- We could deepcopy the builder instead of the sources and handlers. This would
259345
lead to one controller and one workqueue per cluster. For the reason outlined
260346
in the previous alternative, this is not desireable.
261-
- We could skip adding `ClusterName` to `reconcile.Request` and instead pass the
262-
cluster through in the context. On the one hand, this looks attractive as it
263-
would avoid having to touch reconcilers at all to make them multi-cluster-compatible.
264-
On the other hand, with `cluster.Cluster` embedded into `manager.Manager`, not
265-
every method of `cluster.Cluster` carries a context. So virtualizing the cluster
266-
in the manager leads to contradictions in the semantics.
267-
268-
For example, it can well be that every cluster has different REST mapping because
269-
installed CRDs are different. Without a context, we cannot return the right
270-
REST mapper.
271-
272-
An alternative would be to add a context to every method of `cluster.Cluster`,
273-
which is a much bigger and uglier change than what is proposed here.
274-
275347

276348
## Implementation History
277349

278350
- [PR #2207 by @vincepri : WIP: ✨ Cluster Provider and cluster-aware controllers](https://github.com/kubernetes-sigs/controller-runtime/pull/2207) – with extensive review
279-
- [PR #2208 by @sttts replace #2207: WIP: ✨ Cluster Provider and cluster-aware controllers](https://github.com/kubernetes-sigs/controller-runtime/pull/2726)
351+
- [PR #2726 by @sttts replacing #2207: WIP: ✨ Cluster Provider and cluster-aware controllers](https://github.com/kubernetes-sigs/controller-runtime/pull/2726)
280352
picking up #2207, addressing lots of comments and extending the approach to what kcp needs, with a `fleet-namespace` example that demonstrates a similar setup as kcp with real logical clusters.
353+
- [PR #3019 by @embik, replacing #2726: ✨ WIP: Cluster provider and cluster-aware controllers](https://github.com/kubernetes-sigs/controller-runtime/pull/3019) -
354+
picking up #2726, reworking existing code to support the recent `Typed*` generic changes of the codebase.
281355
- [github.com/kcp-dev/controller-runtime](https://github.com/kcp-dev/controller-runtime) – the kcp controller-runtime fork

0 commit comments

Comments
 (0)