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!