Skip to content

Commit c1c8833

Browse files
authored
Merge pull request #11195 from fabriziopandini/refactor-implementers-guide
📖 Refactor provider's implementers guide
2 parents 649c34d + fe7b6b4 commit c1c8833

File tree

14 files changed

+279
-240
lines changed

14 files changed

+279
-240
lines changed

docs/book/src/SUMMARY.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,24 @@
8787
- [Multi-tenancy](./developer/architecture/controllers/multi-tenancy.md)
8888
- [Support multiple instances](./developer/architecture/controllers/support-multiple-instances.md)
8989
- [Tuning controllers](./developer/architecture/controllers/tuning.md)
90-
- [Provider Implementers](./developer/providers/implementers.md)
91-
- [Version migration](./developer/providers/version-migration.md)
92-
- [v1.6 to v1.7](./developer/providers/migrations/v1.6-to-v1.7.md)
93-
- [v1.7 to v1.8](./developer/providers/migrations/v1.7-to-v1.8.md)
94-
- [v1.8 to v1.9](./developer/providers/migrations/v1.8-to-v1.9.md)
90+
- [Developing providers](./developer/providers/overview.md)
91+
- [Getting started](developer/providers/getting-started/overview.md)
92+
- [Naming](developer/providers/getting-started/naming.md)
93+
- [Initialize Repo and API types](developer/providers/getting-started/initialize-repo-and-api-types.md)
94+
- [Implement API types](developer/providers/getting-started/implement-api-types.md)
95+
- [Webhooks](developer/providers/getting-started/webhooks.md)
96+
- [Controllers and Reconciliation](developer/providers/getting-started/controllers-and-reconciliation.md)
97+
- [Configure the provider manifest](developer/providers/getting-started/configure-the-deployment.md)
98+
- [Building, Running, Testing](developer/providers/getting-started/building-running-and-testing.md)
9599
- [Provider contracts](./developer/providers/contracts.md)
96100
- [Cluster Infrastructure](./developer/providers/cluster-infrastructure.md)
97101
- [Control Plane](./developer/providers/control-plane.md)
98102
- [Machine Infrastructure](./developer/providers/machine-infrastructure.md)
99103
- [Bootstrap](./developer/providers/bootstrap.md)
100-
- [Implementer's Guide](./developer/providers/implementers-guide/overview.md)
101-
- [Naming](./developer/providers/implementers-guide/naming.md)
102-
- [Configure](./developer/providers/implementers-guide/configure.md)
103-
- [Create Repo and Generate CRDs](./developer/providers/implementers-guide/generate_crds.md)
104-
- [Create API](./developer/providers/implementers-guide/create_api.md)
105-
- [Webhooks](./developer/providers/webhooks.md)
106-
- [Controllers and Reconciliation](./developer/providers/implementers-guide/controllers_and_reconciliation.md)
107-
- [Building, Running, Testing](./developer/providers/implementers-guide/building_running_and_testing.md)
104+
- [Version migration](./developer/providers/version-migration.md)
105+
- [v1.6 to v1.7](./developer/providers/migrations/v1.6-to-v1.7.md)
106+
- [v1.7 to v1.8](./developer/providers/migrations/v1.7-to-v1.8.md)
107+
- [v1.8 to v1.9](./developer/providers/migrations/v1.8-to-v1.9.md)
108108
- [Troubleshooting](./user/troubleshooting.md)
109109
- [Reference](./reference/reference.md)
110110
- [API Reference](./reference/api/reference.md)

docs/book/src/developer/architecture/controllers.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Controllers
22

3-
Cluster API has a number of controllers, both in the core Cluster API and the reference providers, which move the state of the cluster toward some defined desired state through the process of [controller reconciliation].
3+
Cluster API has a number of controllers, both in the core Cluster API and the reference providers, which move the state of the cluster toward some defined desired state.
44

55
Documentation for the CAPI controllers can be found at:
66
- Bootstrap Provider
@@ -18,7 +18,3 @@ Documentation for the CAPI controllers can be found at:
1818
- [Cluster Topology](./controllers/cluster-topology.md)
1919
- AddOns
2020
- [ClusterResourceSet](./controllers/cluster-resource-set.md)
21-
22-
23-
<!-- links -->
24-
[controller reconciliation]: ../providers/implementers-guide/controllers_and_reconciliation.md

docs/book/src/developer/providers/implementers-guide/configure.md renamed to docs/book/src/developer/providers/getting-started/configure-the-deployment.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
# Configure
1+
# Configure the controller manifest
22

3-
## YAML
3+
`kubebuilder` generates most of the YAML you'll need to deploy your controller into Kubernetes by using a Deployment.
4+
You just need to modify it to add the `MAILGUN_DOMAIN`, `MAILGUN_API_KEY` and `MAIL_RECIPIENT` environment variables
5+
introduced in the previous steps.
46

5-
`kubebuilder` generates most of the YAML you'll need to deploy a container.
6-
We just need to modify it to add our new secrets.
7-
8-
First, let's add our secret as a [patch] to the manager yaml.
7+
First, let's add our environment variables as a [patch] to the manager yaml.
98

109
`config/manager/manager_config.yaml`:
1110

@@ -49,9 +48,9 @@ patches:
4948
[kustomizeyaml]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization
5049
[patch]: https://git.k8s.io/community/contributors/devel/sig-api-machinery/strategic-merge-patch.md
5150

52-
## Our configuration
51+
As you might have noticed, we are reading variable values from a `ConfigMap` and a `Secret`.
5352

54-
There's many ways to manage configuration in production.
53+
You now have to add those to the manifest, but how to inject configuration in production?
5554
The convention many Cluster-API projects use is environment variables.
5655

5756
`config/manager/configuration.yaml`
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Controllers and Reconciliation
22

3+
Right now, you can create objects with our API types, but those objects doesn't make any impact on your mailgun infrastrucrure.
4+
Let's fix that by implementing controllers and reconciliation for your API objects.
5+
36
From the [kubebuilder book][controller]:
47

58
> Controllers are the core of Kubernetes, and of any operator.
@@ -11,7 +14,8 @@ From the [kubebuilder book][controller]:
1114
1215
[controller]: https://book.kubebuilder.io/cronjob-tutorial/controller-overview.html#whats-in-a-controller
1316

14-
Right now, we can create objects in our API but we won't do anything about it. Let's fix that.
17+
Also in this case, controllers and reconcilers generated by Kubebuilder are just a shell.
18+
It is up to you to fill it with the actual implementation.
1519

1620
# Let's see the Code
1721

@@ -39,14 +43,16 @@ func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
3943

4044
## RBAC Roles
4145

46+
Before looking at `(add) your logic here`, lets focus for a moment on the markers before the Reconcile func.
47+
4248
The `// +kubebuilder...` lines tell kubebuilder to generate [RBAC] roles so the manager we're writing can access its own managed resources. These should already exist in `controllers/mailguncluster_controller.go`:
4349

4450
```go
4551
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters,verbs=get;list;watch;create;update;patch;delete
4652
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/status,verbs=get;update;patch
4753
```
4854

49-
We also need to add rules that will let it retrieve (but not modify) Cluster API objects.
55+
We also need to add rules that will let it retrieve (but not modify) `Cluster` objects.
5056
So we'll add another annotation for that, right below the other lines:
5157

5258
```go
@@ -55,7 +61,7 @@ So we'll add another annotation for that, right below the other lines:
5561

5662
Make sure to add this annotation to `MailgunClusterReconciler`.
5763

58-
For `MailgunMachineReconciler`, access to Cluster API `Machine` object is needed, so you must add this annotation in `controllers/mailgunmachine_controller.go`:
64+
Also, for our `MailgunMachineReconciler`, access to Cluster API `Machine` object is needed, so you must add this annotation in `controllers/mailgunmachine_controller.go`:
5965

6066
```go
6167
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machines/status,verbs=get;list;watch
@@ -69,9 +75,10 @@ make manifests
6975

7076
[RBAC]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
7177

72-
## State
78+
## Reconciliation
79+
80+
Let's focus on the `MailgunClusterReconciler` struct first.
7381

74-
Let's focus on that `struct` first.
7582
First, a word of warning: no guarantees are made about parallel access, both on one machine or multiple machines.
7683
That means you should not store any important state in memory: if you need it, write it into a Kubernetes object and store it.
7784

@@ -87,14 +94,12 @@ type MailgunClusterReconciler struct {
8794
}
8895
```
8996

90-
## Reconciliation
91-
9297
Now it's time for our Reconcile function.
9398
Reconcile is only passed a name, not an object, so let's retrieve ours.
9499

95100
Here's a naive example:
96101

97-
```
102+
```go
98103
func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
99104
ctx := context.Background()
100105
_ = r.Log.WithValues("mailguncluster", req.NamespacedName)
@@ -108,54 +113,41 @@ func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
108113
}
109114
```
110115

111-
By returning an error, we request that our controller will get `Reconcile()` called again.
112-
That may not always be what we want - what if the object's been deleted? So let's check that:
116+
By returning an error, you request that our controller will get `Reconcile()` called again.
117+
That may not always be what you want - what if the object's been deleted? So let's check that:
113118

114-
```
115-
var cluster infrav1.MailgunCluster
116-
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
117-
// import apierrors "k8s.io/apimachinery/pkg/api/errors"
118-
if apierrors.IsNotFound(err) {
119-
return ctrl.Result{}, nil
119+
```go
120+
var cluster infrav1.MailgunCluster
121+
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
122+
// import apierrors "k8s.io/apimachinery/pkg/api/errors"
123+
if apierrors.IsNotFound(err) {
124+
return ctrl.Result{}, nil
125+
}
126+
return ctrl.Result{}, err
120127
}
121-
return ctrl.Result{}, err
122-
}
123128
```
124129

125-
Now, if this were any old `kubebuilder` project we'd be done, but in our case we have one more object to retrieve.
130+
Now, if this were any old `kubebuilder` project you'd be done, but in our case you have one more object to retrieve.
126131
Cluster API splits a cluster into two objects: the [`Cluster` defined by Cluster API itself][cluster].
127132
We'll want to retrieve that as well.
128133
Luckily, cluster API [provides a helper for us][getowner].
129134

130135
```go
131-
cluster, err := util.GetOwnerCluster(ctx, r.Client, &mg)
132-
if err != nil {
133-
return ctrl.Result{}, err
134-
135-
}
136-
```
137-
138-
### client-go versions
139-
At the time this document was written, `kubebuilder` pulls `client-go` version `1.14.1` into `go.mod` (it looks like `k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible`).
140-
141-
If you encounter an error when compiling like:
142-
143-
```
144-
../pkg/mod/k8s.io/client-go@v11.0.1-0.20190409021438-1a26190bd76a+incompatible/rest/request.go:598:31: not enough arguments in call to watch.NewStreamWatcher
145-
have (*versioned.Decoder)
146-
want (watch.Decoder, watch.Reporter)`
136+
cluster, err := util.GetOwnerCluster(ctx, r.Client, &mg)
137+
if err != nil {
138+
return ctrl.Result{}, err
139+
140+
}
147141
```
148142

149-
You may need to bump `client-go`. At time of writing, that means `1.15`, which looks like: `k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible`.
150-
151-
## The fun part
143+
### The fun part
152144

153145
_More Documentation: [The Kubebuilder Book][book] has some excellent documentation on many things, including [how to write good controllers!][implement]_
154146

155147
[book]: https://book.kubebuilder.io/
156148
[implement]: https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html
157149

158-
Now that we have our objects, it's time to do something with them!
150+
Now that you have all the objects you care about, it's time to do something with them!
159151
This is where your provider really comes into its own.
160152
In our case, let's try sending some mail:
161153

@@ -170,7 +162,7 @@ if err != nil {
170162
}
171163
```
172164

173-
## Idempotency
165+
### Idempotency
174166

175167
But wait, this isn't quite right.
176168
`Reconcile()` gets called periodically for updates, and any time any updates are made.
@@ -180,37 +172,37 @@ This is an important thing about controllers: they need to be idempotent. This m
180172
So in our case, we'll store the result of sending a message, and then check to see if we've sent one before.
181173

182174
```go
183-
if mgCluster.Status.MessageID != nil {
184-
// We already sent a message, so skip reconciliation
175+
if mgCluster.Status.MessageID != nil {
176+
// We already sent a message, so skip reconciliation
177+
return ctrl.Result{}, nil
178+
}
179+
180+
subject := fmt.Sprintf("[%s] New Cluster %s requested", mgCluster.Spec.Priority, cluster.Name)
181+
body := fmt.Sprintf("Hello! One cluster please.\n\n%s\n", mgCluster.Spec.Request)
182+
183+
msg := mailgun.NewMessage(mgCluster.Spec.Requester, subject, body, r.Recipient)
184+
_, msgID, err := r.Mailgun.Send(msg)
185+
if err != nil {
186+
return ctrl.Result{}, err
187+
}
188+
189+
// patch from sigs.k8s.io/cluster-api/util/patch
190+
helper, err := patch.NewHelper(&mgCluster, r.Client)
191+
if err != nil {
192+
return ctrl.Result{}, err
193+
}
194+
mgCluster.Status.MessageID = &msgID
195+
if err := helper.Patch(ctx, &mgCluster); err != nil {
196+
return ctrl.Result{}, errors.Wrapf(err, "couldn't patch cluster %q", mgCluster.Name)
197+
}
198+
185199
return ctrl.Result{}, nil
186-
}
187-
188-
subject := fmt.Sprintf("[%s] New Cluster %s requested", mgCluster.Spec.Priority, cluster.Name)
189-
body := fmt.Sprintf("Hello! One cluster please.\n\n%s\n", mgCluster.Spec.Request)
190-
191-
msg := mailgun.NewMessage(mgCluster.Spec.Requester, subject, body, r.Recipient)
192-
_, msgID, err := r.Mailgun.Send(msg)
193-
if err != nil {
194-
return ctrl.Result{}, err
195-
}
196-
197-
// patch from sigs.k8s.io/cluster-api/util/patch
198-
helper, err := patch.NewHelper(&mgCluster, r.Client)
199-
if err != nil {
200-
return ctrl.Result{}, err
201-
}
202-
mgCluster.Status.MessageID = &msgID
203-
if err := helper.Patch(ctx, &mgCluster); err != nil {
204-
return ctrl.Result{}, errors.Wrapf(err, "couldn't patch cluster %q", mgCluster.Name)
205-
}
206-
207-
return ctrl.Result{}, nil
208200
```
209201

210202
[cluster]: https://godoc.org/sigs.k8s.io/cluster-api/api/v1beta1#Cluster
211203
[getowner]: https://godoc.org/sigs.k8s.io/cluster-api/util#GetOwnerMachine
212204

213-
#### A note about the status
205+
### A note about the status
214206

215207
Usually, the `Status` field should only be values that can be _computed from existing state_.
216208
Things like whether a machine is running can be retrieved from an API, and cluster status can be queried by a healthcheck.
@@ -221,55 +213,56 @@ If you have a backup of your cluster and you want to restore it, Kubernetes does
221213

222214
We use the MessageID as a `Status` here to illustrate how one might issue status updates in a real application.
223215

224-
## Update `main.go` with your new fields
216+
## Update `main.go`
225217

226-
If you added fields to your reconciler, you'll need to update `main.go`.
218+
Since you added fields to the `MailgunClusterReconciler`, it is now required to update `main.go` to set those fields when
219+
our reconciler is initialized.
227220

228221
Right now, it probably looks like this:
229222

230223
```go
231-
if err = (&controllers.MailgunClusterReconciler{
232-
Client: mgr.GetClient(),
233-
Log: ctrl.Log.WithName("controllers").WithName("MailgunCluster"),
234-
}).SetupWithManager(mgr); err != nil {
235-
setupLog.Error(err, "Unable to create controller", "controller", "MailgunCluster")
236-
os.Exit(1)
237-
}
224+
if err = (&controllers.MailgunClusterReconciler{
225+
Client: mgr.GetClient(),
226+
Log: ctrl.Log.WithName("controllers").WithName("MailgunCluster"),
227+
}).SetupWithManager(mgr); err != nil {
228+
setupLog.Error(err, "Unable to create controller", "controller", "MailgunCluster")
229+
os.Exit(1)
230+
}
238231
```
239232

240233
Let's add our configuration.
241234
We're going to use environment variables for this:
242235

243236
```go
244-
domain := os.Getenv("MAILGUN_DOMAIN")
245-
if domain == "" {
246-
setupLog.Info("missing required env MAILGUN_DOMAIN")
247-
os.Exit(1)
248-
}
249-
250-
apiKey := os.Getenv("MAILGUN_API_KEY")
251-
if apiKey == "" {
252-
setupLog.Info("missing required env MAILGUN_API_KEY")
253-
os.Exit(1)
254-
}
255-
256-
recipient := os.Getenv("MAIL_RECIPIENT")
257-
if recipient == "" {
258-
setupLog.Info("missing required env MAIL_RECIPIENT")
259-
os.Exit(1)
260-
}
261-
262-
mg := mailgun.NewMailgun(domain, apiKey)
263-
264-
if err = (&controllers.MailgunClusterReconciler{
265-
Client: mgr.GetClient(),
266-
Log: ctrl.Log.WithName("controllers").WithName("MailgunCluster"),
267-
Mailgun: mg,
268-
Recipient: recipient,
269-
}).SetupWithManager(mgr); err != nil {
270-
setupLog.Error(err, "Unable to create controller", "controller", "MailgunCluster")
271-
os.Exit(1)
272-
}
237+
domain := os.Getenv("MAILGUN_DOMAIN")
238+
if domain == "" {
239+
setupLog.Info("missing required env MAILGUN_DOMAIN")
240+
os.Exit(1)
241+
}
242+
243+
apiKey := os.Getenv("MAILGUN_API_KEY")
244+
if apiKey == "" {
245+
setupLog.Info("missing required env MAILGUN_API_KEY")
246+
os.Exit(1)
247+
}
248+
249+
recipient := os.Getenv("MAIL_RECIPIENT")
250+
if recipient == "" {
251+
setupLog.Info("missing required env MAIL_RECIPIENT")
252+
os.Exit(1)
253+
}
254+
255+
mg := mailgun.NewMailgun(domain, apiKey)
256+
257+
if err = (&controllers.MailgunClusterReconciler{
258+
Client: mgr.GetClient(),
259+
Log: ctrl.Log.WithName("controllers").WithName("MailgunCluster"),
260+
Mailgun: mg,
261+
Recipient: recipient,
262+
}).SetupWithManager(mgr); err != nil {
263+
setupLog.Error(err, "Unable to create controller", "controller", "MailgunCluster")
264+
os.Exit(1)
265+
}
273266
```
274267

275268
If you have some other state, you'll want to initialize it here!

0 commit comments

Comments
 (0)