Kubernetes Operator series 4— controller-runtime component — Builder

Masato Naka
5 min readApr 28, 2023

--

From the previous post

In the previous post, we began our exploration of the controller-runtime component Manager. The Manager component cannot work independently, it’s necessary to use with Builder to combine a controller with a manager. In this post, we’ll dive deep into the Builder component.

What’s Builder?

Package builder wraps other controller-runtime libraries and exposes simple patterns for building common Controllers.

Projects built with the builder package can trivially be rebased on top of the underlying packages if the project requires more customized behavior in the future. (from Builder)

The primary purpose of the Builder component is to create a controller from a given Reconciler, bind it with the specified manager and target resources.

How Builder is used

Here’s an example of the usage of Builder:

err = builder.
ControllerManagedBy(mgr). // Initialize the builder with the given manager
For(&appsv1.ReplicaSet{}). // Specify the resource type to be watched
Owns(&corev1.Pod{}). // Specify the resource type that is owned by the ReplicaSet
Complete(&ReplicaSetReconciler{})

ControllerManagedBy initializes a Builder with the given manager, which will be used to bind with the controller.

The For, Owns, or Watches functions are used to specify the target resources, changes of which trigger the reconciliation logic. We’ll see the difference between them later in this post.

Note that most functions provided by Builder return the Builder itself to apply a series of functions against the same builder object.

The Complete or Build function is called in the final step to complete the building process. It takes a Reconciler as an argument to create a controller from the given Reconciler internally and bind it with the configured manager and target resources.

What the Builder exactly does internally?

ControllerManagedBy

ControllerManagedBy returns a new controller builder that will be started by the provided Manager.

As we saw in the previous post, what ControllerManagedBy does is just to initialize a Builder object with the specified manager. We always start with this function to initialize a Builder.

func ControllerManagedBy(m manager.Manager) *Builder {
return &Builder{mgr: m}
}

For, Owns, Watches

After initializing a builder with a manager, we set the target resources. There are several options provided to configure the target resources.

What we need to configure is the following two things:

  1. The target resources, to which the reconciliation logic is executed
  2. The resources to monitor changes to trigger the reconciliation logic

There are two commonly used functions:

For: configure the target resource to monitor changes and apply the reconciliation logic. This function can be used for only one resource.

Owns: configure the target resource to monitor changes and to apply the reconciliation logic to the owner object. This might be not easy to understand. Let’s see an example:

err = builder.
ControllerManagedBy(mgr). // Create the ControllerManagedBy
For(&alpha1v1.Foo{}). // Foo is the Application API
Owns(&corev1.Pod{}). // Foo owns Pods created by it
Complete(&FooReconciler{})

If a controller create another resource from the target resource in the reconciliation logic. Let’s consider Foo resource as the target resource to reconcile against and the reconciliation logic of Foo creating/updating a Pod.

In Kubernetes, there’s a mechanism called ownerReferences that allows to make an owner relationship between two objects: owner and controlled object.

The reconciliation logic in a controller also can create this owner references between the target objects and other managing objects.

In this case, if we want to run the reconciliation logic when the owned Pod is changed, we can use Owns:

Owns(&corev1.Pod{})

Changes of the owned Pods will trigger the reconciler but changes of a normal Pod (not owned by Foo) will not trigger.

Note that with this configuration, when the managed Pod is changed, the reconciliation logic will be called against the owner object, not against the Pod object.

You can configure multiple types with Owns if the target resource owns multiple resource types.

The main differences between For and Owns are:

  1. For can be configured only once for each Reconciler
  2. Multiple Owns can be set for a Reconciler

For beginners, For and Owns are enough to start with. However, if you need more complex logic to decide whether the reconciliation logic is called, you can use Watches.

Complete / Build

Finally, we can complete building a controller with the specified manager and the target resource by calling Complete(Reconciler), which internally calls Build(Reconciler).

Inside the Build function, there are two important steps:

  1. doController(Reconciler): build a controller with the specified Reconciler and Manager.
  2. doWatch(): set up Source, EventHandler, and Predicates based on the configured For, Owns, and Watches to start monitoring changes of the target resources in the Kubernetes API server.

doController

In doController function, most of the code is preparation and you can see a new controller is created by newController and set to blder.ctrl. We’ll study what’s exactly done in the function when studying Controller component.

var newController = controller.New
...

func (blder *Builder) doController(r reconcile.Reconciler) error {
...
// Retrieve the GVK from the object we're reconciling
...
// Setup concurrency.

// Setup cache sync timeout.

// Setup the logger.

// Build the controller and return.
blder.ctrl, err = newController(controllerName, blder.mgr, ctrlOptions)
return err
}

doWatch

func (blder *Builder) doWatch() error {
// Reconcile type
if blder.forInput.object != nil {
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
...
// Watches the managed types
for _, own := range blder.ownsInput {
typeForSrc, err := blder.project(own.object, own.objectProjection)
...
// Do the watch requests
for _, w := range blder.watchesInput {
..
if err := blder.ctrl.Watch(w.src, w.eventhandler, allPredicates...); err != nil {
}
}

In this function, convert the configured For , Owns, Watches into the necessary objects to monitor the target resources with blder.ctrl.Watch function. blder.ctrl is the controller set in doController function.

There appear several components that we haven’t studied yet, it might be difficult to understand properly, but for now you can think of a preparation for the input events for the controller. We’ll study how a controller receive changes of the target resources later in this series.

Summary

We’ve studied Builder component in this post. The main points are as follows:

  1. Builder is initialized with the given manager.
  2. The target resources are configured with For and Owns. For is used for the main resource. Owns is used for the resources owned by the controller.
  3. To complete building a controller, Complete is called with a Reconciler, which internally sets up a controller using the manager and prepares for watching changes of the target resources.

You might be more interested in the controller after studying Manager and Builder. The controller component is the main dish for custom controller development. With controller-runtime, we can focus on the core reconciliation logic instead of directly creating a controller that requires to implement a lot of functionality. In the next episode, we will dive into the Reconciler component.

Stay tuned!

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.