Kubebuilder Tips and Tricks
Tags: kubernetes, go, kubebuilder
Published: 2023-08-22 14:23:58

---
title: Kubebuilder Tips and Tricks
published: true
description: A few examples of how to make your life easier when writing Kubernetes operators with Kubebuilder
tags: kubernetes,go,golang,kubebuilder
# published_at: 2023-08-15 16:16 +0000
---


Recently, I've been spending a lot of time writing a Kubernetes operator using the [go operator-sdk](https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/), which is built on top of the [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) framework.  This is a list of a few tips and tricks that I've compiled over the past few months working with these frameworks.

## Log Formatting

Kubebuilder, like much of the k8s ecosystem, utilizes [zap](https://github.com/uber-go/zap) for logging.  Out of the box, the Kubebuilder zap configuration outputs a timestamp for each log, which gets formatted using scientific notation.  This makes it difficult for me to read the time of an event just by glancing at it.  Personally, I prefer ISO 8601, so let's change it!

In your scaffolding's `main.go`, you can configure your current logger format by modifying the `zap.Options` struct and calling `ctrl.SetLogger`.

```go
opts := zap.Options{
    Development: true,
    TimeEncoder: zapcore.ISO8601TimeEncoder,
}

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
```

In this case, I added the `zapcore.ISO8601TimeEncoder`, which encodes timestamps to human-readable ISO 8601-formatted strings.  It took a bit of digging, along with a bit of help from the Kubernetes Slack org, to figure this one out.  But it's been a huge quality-of-life improvement when debugging complex reconcile loops, especially in a multithreaded environment.

## MaxConcurrentReconciles

Speaking of multithreaded environments, by default, an operator will only run a single reconcile loop per-controller.  However, in practice, especially when running a globally-scoped controller, it's useful to run multiple concurrent reconcile loops to simultaneously handle many resource changes at once.  Luckily, the Operator SDK makes this incredibly easy with the `MaxConcurrentReconciles` setting.  We can set this up in a new controller's `SetupWithManager` func:

```go
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {

    return ctrl.NewControllerManagedBy(mgr).
		WithOptions(controller.Options{MaxConcurrentReconciles: 10}).
        ...
        Complete(r)
}
```

I've created a command line arg in my `main.go` file that allows the user to set this value to any integer value, since this will likely be a tweaked over time depending on how the controller performs in a production cluster.

## Parent-Child Relationships

One of the basic functions of a controller is to act as a parent to Kubernetes resources.  This allows the controller to "own"
these objects such that when it is deleted, all child objects are automatically garbage collected by the Kubernetes runtime.

I like this small function that can be called for any `client.Object` to add a parent reference to the controller
that you're writing.

```go
func (r *CustomReconciler) ownObject(ctx context.Context, cr *myapiv1alpha1.CustomResource, obj client.Object) error {

	err := ctrl.SetControllerReference(cr, obj, r.Scheme)
	if err != nil {
		return err
	}
	return r.Update(ctx, obj)
}
```
You can then add `Owns` watches for these resources in your `SetupWithManager` func.  These will instruct your controller to listen for changes in child resources of the specified types, triggering a reconcile loop on each change.

```go
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {

    return ctrl.NewControllerManagedBy(mgr).
        Owns(&v1apps.Deployment{}).
		Owns(&v1core.ConfigMap{}).
		Owns(&v1core.Service{}).
        Complete(r)
}

```

## Watches

Your controller can also watch resources that it _doesn't_ own.  This is useful for when you need to watch for changes in globally-scoped resources like PersistentVolumes or Nodes.  Here's an example of how you would register this watch in your `SetupWithManager` func.

```go
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {

    return ctrl.NewControllerManagedBy(mgr).
        Watches(
            &source.Kind{Type: &v1core.Node{}},
            handler.EnqueueRequestsFromMapFunc(myNodeFilterFunc),
            builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
        ).
        Complete(r)
}
```

In this case, you need to implement `myNodeFilterFunc` to accept
an `obj client.Object` and return `[]reconcile.Request`.  Using the
`ResourceVersionChangedPredicate` triggers the filter function for every change on that resource type, so it's important to
write your filter function to be as efficient as possible, since there is a chance that it could be called quite a bit, especially
if your controller is globally-scoped.

## Field Indexers

One gotcha that I encountered happened when trying to query for a list of Pods that are running on a particular Node.  This query uses a FieldSelector filter, as
seen here:

```go
// Get a list of all pods on the node
err := c.List(ctx, &pods, &client.ListOptions{
    Namespace:     "",
    FieldSelector: fields.ParseSelectorOrDie(fmt.Sprintf("spec.nodeName=%s", node.Name)),
})
```

This codepath led to the following error: `Index with name field:spec.nodeName does not exist`.  After some googling around, I
found [this GitHub issue](https://github.com/kubernetes-sigs/kubebuilder/issues/547) that referenced
a [Kubebuilder docs page](https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup) which contained the answer.

Controllers created using operator-sdk and Kubebuilder use a built-in caching mechanism to store results of API requests.  This is to prevent
spamming the K8s API, as well as improve reconciliation performance.

When performing resource lookups using FieldSelectors, you first need to add your desired search field to an index
that the cache can use for lookups.  Here's an example that will build this index for a Pod's `nodeName`:

```go
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1core.Pod{}, "spec.nodeName", func(rawObj client.Object) []string {
    pod := rawObj.(*v1core.Pod)
    return []string{pod.Spec.NodeName}
}); err != nil {
    return err
}
```

Now, we can run the `List` function from above with the FieldSelector with no issues.

## Retries on Conflicts

If you've ever written controllers, you're probably very familiar with the error `Operation cannot be fulfilled on ...: the object has been modified; please apply your changes to the latest version and try again`

This occurs when the version of the resource that you're currently reconciling in your controller is out-of-date with what's in latest version of the K8s cluster state.  If you're retrying your reconciliation loop on any errors, your controller will eventually reconcile the resource, but this can really pollute your logs and make it difficult to spot more important errors.

After reading through the k8s source, I found the solution to this: `RetryOnConflict`.  It's a [utility function](https://github.com/kubernetes/client-go/blob/648d82831a4db2e9d07c0dad53603aa15c45a968/util/retry/util.go#L103) in the `client-go` package that runs a function and automatically retries on conflict, up to a certain point.

Now, you can just wrap your logic inside this function argument, and never have to worry about this issue again!  And the added benefit is that you just get to `return err` instead of `return ctrl.Result{}, err`, which makes your code that much easier to read.

## Useful Kubebuilder Markers

Here are some useful code markers that I've found while developing my operator.

To add custom columns to your custom resource's description (when running `kubectl get`), you can add annotations to your API object like these:

```go
//+kubebuilder:printcolumn:name="NodeReady",type="boolean",JSONPath=".status.nodeReady"
//+kubebuilder:printcolumn:name="NodeIp",type="string",JSONPath=".status.nodeIp"
```
To add a shortname to your custom resource (like `pvc` for `PersistentVolumeClaim` for example), you can add this annotation:

```go
//+kubebuilder:resource:shortName=mycr;mycrs
```

More docs on kubebuilder markers can be found here:

<https://book.kubebuilder.io/reference/markers/crd.html>


Originally published on my blog: https://sklar.rocks/kubebuilder-tips/