sigs.k8s.io/cluster-api@v1.7.1/docs/book/src/developer/providers/implementers-guide/controllers_and_reconciliation.md (about) 1 # Controllers and Reconciliation 2 3 From the [kubebuilder book][controller]: 4 5 > Controllers are the core of Kubernetes, and of any operator. 6 > 7 > It’s a controller’s job to ensure that, for any given object, the actual state of the world (both the cluster state, and potentially external state like running containers for Kubelet or loadbalancers for a cloud provider) matches the desired state in the object. 8 > Each controller focuses on one root Kind, but may interact with other Kinds. 9 > 10 > We call this process reconciling. 11 12 [controller]: https://book.kubebuilder.io/cronjob-tutorial/controller-overview.html#whats-in-a-controller 13 14 Right now, we can create objects in our API but we won't do anything about it. Let's fix that. 15 16 # Let's see the Code 17 18 Kubebuilder has created our first controller in `controllers/mailguncluster_controller.go`. Let's take a look at what got generated: 19 20 ```go 21 // MailgunClusterReconciler reconciles a MailgunCluster object 22 type MailgunClusterReconciler struct { 23 client.Client 24 Log logr.Logger 25 } 26 27 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters,verbs=get;list;watch;create;update;patch;delete 28 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/status,verbs=get;update;patch 29 30 func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 31 _ = context.Background() 32 _ = r.Log.WithValues("mailguncluster", req.NamespacedName) 33 34 // your logic here 35 36 return ctrl.Result{}, nil 37 } 38 ``` 39 40 ## RBAC Roles 41 42 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 44 ```go 45 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters,verbs=get;list;watch;create;update;patch;delete 46 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=mailgunclusters/status,verbs=get;update;patch 47 ``` 48 49 We also need to add rules that will let it retrieve (but not modify) Cluster API objects. 50 So we'll add another annotation for that, right below the other lines: 51 52 ```go 53 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch 54 ``` 55 56 Make sure to add this annotation to `MailgunClusterReconciler`. 57 58 For `MailgunMachineReconciler`, access to Cluster API `Machine` object is needed, so you must add this annotation in `controllers/mailgunmachine_controller.go`: 59 60 ```go 61 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machines/status,verbs=get;list;watch 62 ``` 63 64 Regenerate the RBAC roles after you are done: 65 66 ```bash 67 make manifests 68 ``` 69 70 [RBAC]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole 71 72 ## State 73 74 Let's focus on that `struct` first. 75 First, a word of warning: no guarantees are made about parallel access, both on one machine or multiple machines. 76 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 78 We're going to be sending mail, so let's add a few extra fields: 79 80 ```go 81 // MailgunClusterReconciler reconciles a MailgunCluster object 82 type MailgunClusterReconciler struct { 83 client.Client 84 Log logr.Logger 85 Mailgun mailgun.Mailgun 86 Recipient string 87 } 88 ``` 89 90 ## Reconciliation 91 92 Now it's time for our Reconcile function. 93 Reconcile is only passed a name, not an object, so let's retrieve ours. 94 95 Here's a naive example: 96 97 ``` 98 func (r *MailgunClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 99 ctx := context.Background() 100 _ = r.Log.WithValues("mailguncluster", req.NamespacedName) 101 102 var cluster infrav1.MailgunCluster 103 if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil { 104 return ctrl.Result{}, err 105 } 106 107 return ctrl.Result{}, nil 108 } 109 ``` 110 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: 113 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 120 } 121 return ctrl.Result{}, err 122 } 123 ``` 124 125 Now, if this were any old `kubebuilder` project we'd be done, but in our case we have one more object to retrieve. 126 Cluster API splits a cluster into two objects: the [`Cluster` defined by Cluster API itself][cluster]. 127 We'll want to retrieve that as well. 128 Luckily, cluster API [provides a helper for us][getowner]. 129 130 ```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)` 147 ``` 148 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 152 153 _More Documentation: [The Kubebuilder Book][book] has some excellent documentation on many things, including [how to write good controllers!][implement]_ 154 155 [book]: https://book.kubebuilder.io/ 156 [implement]: https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html 157 158 Now that we have our objects, it's time to do something with them! 159 This is where your provider really comes into its own. 160 In our case, let's try sending some mail: 161 162 ```go 163 subject := fmt.Sprintf("[%s] New Cluster %s requested", mgCluster.Spec.Priority, cluster.Name) 164 body := fmt.Sprint("Hello! One cluster please.\n\n%s\n", mgCluster.Spec.Request) 165 166 msg := mailgun.NewMessage(mgCluster.Spec.Requester, subject, body, r.Recipient) 167 _, _, err = r.Mailgun.Send(msg) 168 if err != nil { 169 return ctrl.Result{}, err 170 } 171 ``` 172 173 ## Idempotency 174 175 But wait, this isn't quite right. 176 `Reconcile()` gets called periodically for updates, and any time any updates are made. 177 That would mean we're potentially sending an email every few minutes! 178 This is an important thing about controllers: they need to be idempotent. This means a controller must be able to repeat actions on the same inputs without changing the effect of those actions. 179 180 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 182 ```go 183 if mgCluster.Status.MessageID != nil { 184 // We already sent a message, so skip reconciliation 185 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 ``` 209 210 [cluster]: https://godoc.org/sigs.k8s.io/cluster-api/api/v1beta1#Cluster 211 [getowner]: https://godoc.org/sigs.k8s.io/cluster-api/util#GetOwnerMachine 212 213 #### A note about the status 214 215 Usually, the `Status` field should only be values that can be _computed from existing state_. 216 Things like whether a machine is running can be retrieved from an API, and cluster status can be queried by a healthcheck. 217 The message ID is ephemeral, so it should properly go in the `Spec` part of the object. 218 Anything that can't be recreated, either with some sort of deterministic generation method or by querying/observing actual state, needs to be in Spec. 219 This is to support proper disaster recovery of resources. 220 If you have a backup of your cluster and you want to restore it, Kubernetes doesn't let you restore both spec & status together. 221 222 We use the MessageID as a `Status` here to illustrate how one might issue status updates in a real application. 223 224 ## Update `main.go` with your new fields 225 226 If you added fields to your reconciler, you'll need to update `main.go`. 227 228 Right now, it probably looks like this: 229 230 ```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 } 238 ``` 239 240 Let's add our configuration. 241 We're going to use environment variables for this: 242 243 ```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 } 273 ``` 274 275 If you have some other state, you'll want to initialize it here!