controller-runtime (Go package)

Package sigs.k8s.io/controller-runtime is used to build Kubernetes controllers and operators.

Why is it used?

You could write all of that yourself with the raw Kubernetes client-go library. controller-runtime provides boiler plate correct code.
controller-runtime is the framework for “watch K8s objects and react.” You only write the business logic inside Reconcile().

Without controller-runtime With controller-runtime
Write your own watch loops and retries Built-in Reconcile loop — your function runs when something changes
Call the API server on every read Local cache of cluster objects
Two copies of the controller both “fix” the same thing Leader election — only one instance runs the loop
Wire up HTTP health checks by hand Manager starts controllers, metrics, and probes together

Very simple example

Goal: whenever a Pod is created or changed, print its name and make sure it has a label managed-by=demo-controller.

Step 1 — define the reconciler

package main

import (
    "context"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/types"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// PodReconciler watches Pods and adds a label if missing.
type PodReconciler struct {
    client client.Client // reads/writes objects via the local cache
}

func (r *PodReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
    // 1. READ — fetch the Pod that triggered this run
    pod := &corev1.Pod{}
    if err := r.client.Get(ctx, types.NamespacedName{
        Name:      req.Name,
        Namespace: req.Namespace,
    }, pod); err != nil {
        return reconcile.Result{}, client.IgnoreNotFound(err) // Pod was deleted — ignore
    }

    // 2. COMPARE — do we already have the label?
    if pod.Labels["managed-by"] == "demo-controller" {
        return reconcile.Result{}, nil // nothing to do
    }

    // 3. ACT — add the label and write back to the cluster
    if pod.Labels == nil {
        pod.Labels = map[string]string{}
    }
    pod.Labels["managed-by"] = "demo-controller"
    return reconcile.Result{}, r.client.Update(ctx, pod)
}

Step 2 — start the manager

import (
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/log/zap"
)

func main() {
    ctrl.SetLogger(zap.New()) // optional: structured logs

    // Manager = connection to cluster + cache + leader election
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{})
    if err != nil {
        panic(err)
    }

    // Register our PodReconciler and tell it to watch Pod objects
    if err := ctrl.NewControllerManagedBy(mgr).
        For(&corev1.Pod{}).                    // watch Pods
        Complete(&PodReconciler{client: mgr.GetClient()}); err != nil {
        panic(err)
    }

    // Blocks forever — runs Reconcile whenever a Pod changes
    if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
        panic(err)
    }
}

What happens at runtime

User runs:  kubectl run nginx --image=nginx
            ↓
API server creates Pod "nginx"
            ↓
controller-runtime cache sees the new Pod
            ↓
PodReconciler.Reconcile() runs for Pod nginx
            ↓
Reconciler adds label managed-by=demo-controller
            ↓
kubectl get pod nginx --show-labels
NAME    ...   LABELS
nginx   ...   managed-by=demo-controller

That is the whole pattern used by real operators — just with your own custom resource (CRD) instead of a Pod, and more logic inside Reconcile (create Deployments, Services, Secrets, etc.).

Install in your Go module

go mod init my-operator
go get sigs.k8s.io/controller-runtime@latest
go get k8s.io/api@latest
go get k8s.io/apimachinery@latest