Infrastructure Go daemon
SECTION 11.4

The Reconciliation Loop

Desired State vs Actual State

Every K8s controller follows the same pattern:

1. Watch for changes (events)
2. For each event, compare desired state (spec) with actual state (status)
3. Take action to make actual match desired
4. Update status
5. If action failed, requeue

The Controller Pattern

type Controller struct {
    clientset     *kubernetes.Clientset
    podInformer   cache.SharedIndexInformer
    workqueue     workqueue.RateLimitingInterface
}

func (c *Controller) Run(ctx context.Context) error {
    defer c.workqueue.ShutDown()

    // Start informer
    go c.podInformer.Run(ctx.Done())

    // Wait for cache sync
    if !cache.WaitForCacheSync(ctx.Done(), c.podInformer.HasSynced) {
        return fmt.Errorf("cache sync failed")
    }

    // Process work items
    for c.processNextItem(ctx) {
    }
    return nil
}

func (c *Controller) processNextItem(ctx context.Context) bool {
    key, quit := c.workqueue.Get()
    if quit {
        return false
    }
    defer c.workqueue.Done(key)

    err := c.reconcile(ctx, key.(string))
    if err != nil {
        // Requeue with rate limiting
        c.workqueue.AddRateLimited(key)
        slog.Error("reconcile failed", "key", key, "error", err)
        return true
    }

    // Success — forget the rate limit history
    c.workqueue.Forget(key)
    return true
}

The Reconcile Function

func (c *Controller) reconcile(ctx context.Context, key string) error {
    namespace, name, _ := cache.SplitMetaNamespaceKey(key)

    // Get the desired state
    pod, err := c.podInformer.GetStore().GetByKey(key)
    if err != nil {
        return err
    }
    if pod == nil {
        // Resource was deleted — handle cleanup
        slog.Info("resource deleted", "key", key)
        return nil
    }

    // Compare desired vs actual, take action
    actual := pod.(*corev1.Pod)
    slog.Info("reconciling", "namespace", namespace, "name", name, "phase", actual.Status.Phase)

    // ... your business logic here ...

    return nil
}

Idempotency

2 · Worked example — read every step

Reconcile must be safe to run repeatedly. Because:

  • Events can be delivered multiple times
  • The controller can restart at any point
  • Multiple events can arrive for the same resource
// BAD: not idempotent
func reconcile(pod *corev1.Pod) {
    createSidecar(pod) // creates duplicate sidecars on re-run!
}

// GOOD: idempotent
func reconcile(pod *corev1.Pod) {
    if !hasSidecar(pod) {
        createSidecar(pod)
    }
}
3 · Fill the gaps
One work item, cradle to grave: claim it, always release it, and steer the retry backoff both ways.
key, quit := c.workqueue.()
if quit {
    return false
}
defer c.workqueue.(key)

if err := c.reconcile(ctx, key.(string)); err != nil { c.workqueue.(key) return true } c.workqueue.(key) return true

Miss the last call and every key that ever failed once keeps its backoff history forever — retries get slower and slower for no reason.

4 · From scratch — this feeds your review queue