Controller-Runtime Patterns
Reconciler Structure
- •Implement
reconcile.Reconcilerinterface - •Always accept and propagate
context.Context - •Return
ctrl.Result{}with appropriate requeue:- •
Result{}— success, no requeue - •
Result{RequeueAfter: 30 * time.Second}— delayed retry - •
Result{Requeue: true}— immediate retry
- •
- •Never return an error for expected/permanent failures — log and return
Result{} - •Return errors only for transient failures that should be retried
CRD Design
- •Group:
<domain>.example.com/v1alpha1→v1beta1→v1 - •Status subresource always enabled
- •Use status conditions following
metav1.Conditionpattern:- •
Type,Status(True/False/Unknown),Reason,Message,LastTransitionTime
- •
- •Printer columns for
kubectl getoutput - •Validation via CEL expressions in CRD markers
Finalizers
- •Add finalizer on creation if external cleanup is needed
- •Check
DeletionTimestampbefore reconciling - •Remove finalizer only after cleanup succeeds
- •Pattern:
go
if obj.DeletionTimestamp != nil { if controllerutil.ContainsFinalizer(obj, finalizerName) { // cleanup external resources controllerutil.RemoveFinalizer(obj, finalizerName) return ctrl.Result{}, r.Update(ctx, obj) } return ctrl.Result{}, nil }
Owner References
- •Set owner references for all child resources
- •Use
controllerutil.SetControllerReference()for single-owner - •Use
controllerutil.SetOwnerReference()for shared ownership - •Watch owned resources with
.Owns(&corev1.ConfigMap{})
RBAC
- •Use kubebuilder RBAC markers:
//+kubebuilder:rbac:groups=...,resources=...,verbs=... - •Principle of least privilege — only request what the controller needs
- •Separate ClusterRole for cluster-scoped vs namespace-scoped resources
Testing
- •Unit tests:
fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build() - •Integration tests:
envtest.Environmentwith real etcd + API server - •Test the full reconcile loop: create → reconcile → verify → update → reconcile → verify
- •Test idempotency: running reconcile twice should produce the same result