Kubernetes Operator series 5— controller-runtime component — Reconciler

Masato Naka
7 min readMay 15, 2023

Overview

In the previous two posts, I’ve shared insights about the Manager and Builder components of controller-runtime. Now let’s dive into the Reconciler component, which plays a vital role in controller-runtime.

What’s Reconciler?

The Reconciler is a crucial component within controller-runtime that encapsulates the core reconciliation logic. It requires developers to implement the Reconcile function, which serves as the entry point for performing the actual reconciliation process.

However, the Reconciler doesn’t operate independently. It is an integral part of a Controller, which we will delve into later in this series. The Controller determines when to invoke the Reconcile function for a specific object. To establish this relationship, the Reconciler is combined with the Controller using the Builder component, as we explored in the previous episode.

In practice, the Controller actively observes changes to designated resources, such as Kubernetes objects. When a change event occurs, it adds the event to a queue and subsequently calls the Reconcile function, providing the queued item as an input. If necessary, the Controller can requeue the item to allow for additional reconciliation attempts.

Simplified diagram

The Reconciler interface is defined with a single method called Reconcile. Here is the definition of the interface:

type Reconciler interface {
// Reconcile performs a full reconciliation for the object referred to by the Request.
// The Controller will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
Reconcile(context.Context, Request) (Result, error)
}

By implementing the Reconcile method with the specified signature, a type becomes a Reconciler. It's a straightforward interface, isn't it?

Now let’s take a look at the definition of Request and Result.

The Request struct is defined as follows:

type Request struct {
// NamespacedName is the name and namespace of the object to reconcile.
types.NamespacedName
}

Based on the provided comment, we can understand that the input for a Reconciler is the name and namespace of the object that needs to be reconciled.

The Result struct is defined as follows:

type Result struct {
// Requeue tells the Controller to requeue the reconcile key. Defaults to false.
Requeue bool

// RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration.
// Implies that Requeue is true, there is no need to set Requeue to true at the same time as RequeueAfter.
RequeueAfter time.Duration
}

The Result struct contains two fields. The Requeue field determines whether the Controller should requeue the same reconcile key for further processing. By default, it is set to false. The RequeueAfter field, when set to a duration greater than 0, instructs the Controller to requeue the reconcile key after the specified duration. It is important to note that if RequeueAfter is set, there is no need to also set Requeue to true.

How to implement Reconciler

There are two ways to implement a Reconciler:

  1. With your own struct (e.g. reconciler) with Reconcile function
  2. With reconcile.Func

Let’s explore each approach in detail.

With your own struct with Reconcile function

To implement a Reconciler using your own struct and the Reconcile function, follow these steps:

  1. Define your struct that will serve as the reconciler. This struct can have additional fields or dependencies as needed.
  2. In the Reconcile function, you can access the necessary context and the request object that contains the name and namespace of the object being reconciled. Use this information to perform your reconciliation logic.
type ReconcilerStruct struct {
// Add your fields or dependencies here
}

func (r *ReconcilerStruct) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
// Implement your reconciliation logic here
// You can read and write objects, perform desired actions, and return the result
return reconcile.Result{}, nil
}

Here’s an example from a previous episode of this series:

// ReplicaSetReconciler is a simple Controller example implementation.
type ReplicaSetReconciler struct {
client.Client
}

func (r *ReplicaSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

rs := &appsv1.ReplicaSet{}
err := r.Get(ctx, req.NamespacedName, rs)
if err != nil {
return ctrl.Result{}, err
}
...
return ctrl.Result{}, nil
}

In this example, the ReplicaSetReconciler struct implements the Reconciler function to handle the reconciliation logic for ReplicaSet objects. The Get method is used to retrieve the ReplicaSet object based on the provided name and namespace in the request.

With reconcile.Func

Another way to implement a Reconciler is by using the reconile.Func provided by the reconcile package. This approach simplifies the implementation process. Here’s how to use it:

reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
// Implement your reconciliation logic here
// You can read and write objects, perform desired actions, and return the result

return reconcile.Result{}, nil
})

Here’s the definition of reconcile.Func :

// Func is a function that implements the reconcile interface.
type Func func(context.Context, Request) (Result, error)

var _ Reconciler = Func(nil)

// Reconcile implements Reconciler.
func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) { return r(ctx, o) }

The reconcile.Func is a function that takes two paramenters: ctx of type context.Context and req of type reconcile.Request. Inside the function, you can implement your reconciliation logic using the provided input parameters.

The reconcile.Func function implements the Reconciler interface. It has Reconcile method that delegates the call to the wrapped function. By using this approach, you can directly pass your custom logic as a function to the reconcile.Func constructor.

Regardless of the implementation approach you choose (struct with Reconcile function or reconcile.Func), the key is to implement your reconciliation logic by taking a Request as an input and returning a Result and an error.

When to use which way?

The choice between using a custom struct with the Reconcile function or reconcile.Func depends on the complexity of your reconciliation logic and the need for additional dependencies.

If your reconciliation logic requires access to the Kubernetes API server using a client, it is common to use a custom struct that holds the client, as shown in the first example:

type ReplicaSetReconciler struct {
client.Client
}

Using a custom struct allows you to define additional fields and methods that are specific to your reconciliation logic. It provides more flexibility and allows you to encapsulate the necessary dependencies within the struct.

On the other hand, if you have a simple reconciliation logic without the need of additional dependencies or custom fields, using reconcile.Func is a great option. It simplifies the implementation by directly using a function to define your reconciliation logic.

The choice between the two approaches depends on your specific requirements and the complexity of your reconciliation logic. Consider the need for additional dependencies, flexibility, and the level of customization required when deciding which approach to use.

How the Reconciler is used

As we saw in the previous post about Builder, a Reconciler is passed to Builder to create a new Controller with the given reconciler. So the reconciler is called in the Controller component. We’ll check how the Reconciler is used internally in more details in a future post about Controller.

Reconciler implementation tips

1. Reconciler doesn’t know what action triggered

The Reconcile function is unaware of the specific action that triggered it, such as object creation, update, or deletion. Therefore, the reconciliation logic should not rely on the action taken on the triggered object. Instead, gather all necessary infromation about the object’s current state from the API server within the Reconcile function.

2. Returning errors to retry reconciliation

When implementing the Reconcile function, if an error occurs during the reconciliation process, return the error. By returning an error, the Reconcile function will be called again for the same Request, allow for retry attempts.

func (r *MyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
...
err := someFunc()
if err != nil {
return Result{}, err // Return error to trigger a retry
}
}

To control the requeue behavior, use the Result struct:

  • If reconciliation is successful and doesn’t require a retry, return ctrl.Result{}, nil.
  • If reconciliation fails due to an error and needs to be retried, return ctrl.Result{}, err.
  • If you need to requeue the object for any reason other than an error, use ctrl.Result{Requeue: true}, nil.
  • To specify the duration to wait before the next reconciliation, set RequeueAfter in the Result struct, such as ctrl.Result{RequeueAfter: 5 * time.Second}, nil.

These tips will help you handle errors and control the retry behavior in the Reconcile function effectively.

Summary

In this post, we explored the Reconciler, a vital component of controller-runtime. We covered its definition, two ways to implement it, and some implemetation tips. The Reconciler is responsible for handling the reconciliation of objects and can be implemented using a custom struct with Reconcile function or using reconcile.Func. We discussed important considerations such as handling errors and requeueing for retries.

In the next post, we’ll explore the Controller component in more detail. Stay tuned for further insights!

Series Index

  1. Kubernetes Operator series 1 — controller-runtime example controller
  2. Kubernetes Operator series 2 — Overview of controller-runtime
  3. Kubernetes Operator series 3 — controller-runtime component — Manager
  4. Kubernetes Operator series 4 — controller-runtime component — Builder
  5. Kubernetes Operator series 5 — controller-runtime component — Reconciler
  6. Kubernetes Operator series 6 — controller-runtime component — Controller

--

--

Masato Naka

An SRE engineer, mainly working on Kubernetes. CKA (Feb 2021). His Interests include Cloud-Native application development, and machine learning.