Explains the Kubernetes controller reconcile loop, level triggered reconciliation, idempotent design, fetching and handling deletions, requeue semantics, and managing desired versus actual child resources
Think of a thermostat on the wall.You set it to seventy degrees — that’s the desired state. The thermostat reads the room temperature — that’s the actual state. If the two don’t match, the thermostat turns the heat on or off, then checks again and again, forever.
Reconcile is exactly that loop applied to Kubernetes objects.Your WebApp’s spec is the set point; the cluster’s actual state — the Deployment, Service, ConfigMap, Pods, and so on — is the room temperature. The controller closes that gap on every event, every resync, and every restart.
Reconcile is the heart of every controller you write. When implemented correctly, the rest of the operator becomes straightforward; when it’s wrong, you’ll be struggling with unpredictable behavior.Here is the Reconcile signature provided by controller-runtime — it has two inputs and two outputs:
The req contains a NamespacedName (the key of the object that needs attention). Notice what it does not contain: neither the object itself nor the event that triggered the call. This is intentional.Controllers in Kubernetes are level-triggered, not edge-triggered. You do not react to “a WebApp was created”; you react to “the WebApp named foo in namespace bar might need work — go look.” Embracing this mental model is essential.
Controllers are level-triggered: always reconcile to the current desired state by looking up the object by key, not by relying on the event payload.
Every reconcile starts by answering three questions:
What’s the desired state?
What’s the actual state?
What’s the diff (what needs to change)?
A typical reconcile body follows these four steps:
Fetch the parent resource (the WebApp) from the cache using r.Get with the request’s NamespacedName.
Compute the desired child objects (Deployment, Service, ConfigMap, etc.) from the WebApp spec.
Create or update those child objects so they match the desired state.
Update the WebApp’s status to reflect the observed (actual) state.
The first step — fetching the WebApp safely — is a small, important pattern. Use the API to get the object, and treat a not-found as a normal deletion case:
webapp := &webappv1.WebApp{}if err := r.Get(ctx, req.NamespacedName, webapp); err != nil { if apierrors.IsNotFound(err) { // The WebApp was deleted. Owned children will be cleaned up by GC via OwnerReferences. return ctrl.Result{}, nil } // Any other error is a real problem (permissions, API server unreachable, etc.) return ctrl.Result{}, err}
Notes:
webappv1 is the package generated from your CRD group/version.
apierrors is k8s.io/apimachinery/pkg/api/errors; use IsNotFound to distinguish deletions from transient API errors.
Owned children created with proper OwnerReferences are garbage-collected automatically. See the Kubernetes docs on OwnerReferences for details.
This fetch-and-bail pattern (under ten lines of Go) is the canonical approach to “fetch the parent and exit safely if it’s gone.”The reconcile loop must be idempotent. The same WebApp will be reconciled many times — on creation, on spec changes, on periodic resyncs, after controller restarts, and after transient errors. If running reconcile twice yields a different result than running it once, you have a bug.
Idempotency is critical: ensure reconcile is safe to run repeatedly. Avoid relying on side-effects that make subsequent runs behave differently.
Return values from Reconcile control requeue behavior. Use these return patterns to express what should happen next:
When r.Get returns a not-found error, the resource was deleted. If you created children with OwnerReference, Kubernetes will garbage collect them, so you typically return without error:
return ctrl.Result{}, nil
If you need to perform work during deletion (for example, to clean up external resources), implement finalizers and handle that lifecycle explicitly — this will be covered in the finalizers and cleanup section.In the next lesson you’ll wire up the rest of the loop: computing desired child resources, performing create-or-update operations, and updating status to reflect observed state.Links and references: