1
1
# Controllers and Reconciliation
2
2
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
+
3
6
From the [ kubebuilder book] [ controller ] :
4
7
5
8
> Controllers are the core of Kubernetes, and of any operator.
@@ -11,7 +14,8 @@ From the [kubebuilder book][controller]:
11
14
12
15
[ controller ] : https://book.kubebuilder.io/cronjob-tutorial/controller-overview.html#whats-in-a-controller
13
16
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.
15
19
16
20
# Let's see the Code
17
21
@@ -39,14 +43,16 @@ func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
39
43
40
44
## RBAC Roles
41
45
46
+ Before looking at ` (add) your logic here ` , lets focus for a moment on the markers before the Reconcile func.
47
+
42
48
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 ` :
43
49
44
50
``` go
45
51
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters,verbs=get;list;watch;create;update;patch;delete
46
52
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/status,verbs=get;update;patch
47
53
```
48
54
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.
50
56
So we'll add another annotation for that, right below the other lines:
51
57
52
58
``` go
@@ -55,7 +61,7 @@ So we'll add another annotation for that, right below the other lines:
55
61
56
62
Make sure to add this annotation to ` MailgunClusterReconciler ` .
57
63
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 ` :
59
65
60
66
``` go
61
67
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machines/status,verbs=get;list;watch
@@ -69,9 +75,10 @@ make manifests
69
75
70
76
[ RBAC ] : https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
71
77
72
- ## State
78
+ ## Reconciliation
79
+
80
+ Let's focus on the ` MailgunClusterReconciler ` struct first.
73
81
74
- Let's focus on that ` struct ` first.
75
82
First, a word of warning: no guarantees are made about parallel access, both on one machine or multiple machines.
76
83
That means you should not store any important state in memory: if you need it, write it into a Kubernetes object and store it.
77
84
@@ -87,14 +94,12 @@ type MailgunClusterReconciler struct {
87
94
}
88
95
```
89
96
90
- ## Reconciliation
91
-
92
97
Now it's time for our Reconcile function.
93
98
Reconcile is only passed a name, not an object, so let's retrieve ours.
94
99
95
100
Here's a naive example:
96
101
97
- ```
102
+ ``` go
98
103
func (r *MailgunClusterReconciler ) Reconcile (ctx context .Context , req ctrl .Request ) (ctrl .Result , error ) {
99
104
ctx := context.Background ()
100
105
_ = r.Log .WithValues (" mailguncluster" , req.NamespacedName )
@@ -108,54 +113,41 @@ func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
108
113
}
109
114
```
110
115
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:
113
118
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
120
127
}
121
- return ctrl.Result{}, err
122
- }
123
128
```
124
129
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.
126
131
Cluster API splits a cluster into two objects: the [ ` Cluster ` defined by Cluster API itself] [ cluster ] .
127
132
We'll want to retrieve that as well.
128
133
Luckily, cluster API [ provides a helper for us] [ getowner ] .
129
134
130
135
``` 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
+ }
147
141
```
148
142
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
152
144
153
145
_ More Documentation: [ The Kubebuilder Book] [ book ] has some excellent documentation on many things, including [ how to write good controllers!] [ implement ] _
154
146
155
147
[ book ] : https://book.kubebuilder.io/
156
148
[ implement ] : https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html
157
149
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!
159
151
This is where your provider really comes into its own.
160
152
In our case, let's try sending some mail:
161
153
@@ -170,7 +162,7 @@ if err != nil {
170
162
}
171
163
```
172
164
173
- ## Idempotency
165
+ ### Idempotency
174
166
175
167
But wait, this isn't quite right.
176
168
` 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
180
172
So in our case, we'll store the result of sending a message, and then check to see if we've sent one before.
181
173
182
174
``` 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
+
185
199
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
208
200
```
209
201
210
202
[ cluster ] : https://godoc.org/sigs.k8s.io/cluster-api/api/v1beta1#Cluster
211
203
[ getowner ] : https://godoc.org/sigs.k8s.io/cluster-api/util#GetOwnerMachine
212
204
213
- #### A note about the status
205
+ ### A note about the status
214
206
215
207
Usually, the ` Status ` field should only be values that can be _ computed from existing state_ .
216
208
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
221
213
222
214
We use the MessageID as a ` Status ` here to illustrate how one might issue status updates in a real application.
223
215
224
- ## Update ` main.go ` with your new fields
216
+ ## Update ` main.go `
225
217
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.
227
220
228
221
Right now, it probably looks like this:
229
222
230
223
``` 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
+ }
238
231
```
239
232
240
233
Let's add our configuration.
241
234
We're going to use environment variables for this:
242
235
243
236
``` 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
+ }
273
266
```
274
267
275
268
If you have some other state, you'll want to initialize it here!
0 commit comments