Kubernetes Operator series 4— controller-runtime component — Builder
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:
- The target resources, to which the reconciliation logic is executed
- 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:
For
can be configured only once for eachReconciler
- Multiple
Owns
can be set for aReconciler
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:
doController(Reconciler)
: build a controller with the specified Reconciler and Manager.doWatch()
: set up Source, EventHandler, and Predicates based on the configuredFor
,Owns
, andWatches
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:
- Builder is initialized with the given manager.
- The target resources are configured with
For
andOwns
.For
is used for the main resource.Owns
is used for the resources owned by the controller. - 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
- Kubernetes Operator series 1 — controller-runtime example controller
- Kubernetes Operator series 2 — Overview of controller-runtime
- Kubernetes Operator series 3 — controller-runtime component — Manager
- Kubernetes Operator series 4 — controller-runtime component — Builder
- Kubernetes Operator series 5 — controller-runtime component — Reconciler
- Kubernetes Operator series 6 — controller-runtime component — Controller
- Kubernetes Operator series 7 — controller-runtime component — Source