Skip to content

PodDisruptionBudget Reconciliation

Reference documentation for the PodDisruptionBudget (PDB) reconciliation logic that protects Memcached pods from voluntary disruptions.

Source: internal/controller/pdb.go, internal/controller/memcached_controller.go

Overview

When spec.highAvailability.podDisruptionBudget.enabled is true, the reconciler ensures a matching PodDisruptionBudget exists in the same namespace with the same name as the Memcached CR. The PDB is constructed from the CR spec using a pure builder function, then applied via controllerutil.CreateOrUpdate for idempotent create/update semantics. A controller owner reference on the PDB enables automatic garbage collection when the Memcached CR is deleted.

The PDB is opt-in — it is only created when explicitly enabled.


CRD Field Path

text
spec.highAvailability.podDisruptionBudget

Defined in api/v1alpha1/memcached_types.go on the PDBSpec struct:

go
type PDBSpec struct {
    Enabled        bool                `json:"enabled,omitempty"`
    MinAvailable   *intstr.IntOrString `json:"minAvailable,omitempty,omitzero"`
    MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty,omitzero"`
}
FieldTypeRequiredDefaultDescription
enabledboolNofalseControls whether a PDB is created
minAvailableint or stringNo1 (controller default)Minimum available pods during disruption
maxUnavailableint or stringNoMaximum unavailable pods during disruption

PDB Construction

constructPDB(mc *Memcached, pdb *PodDisruptionBudget) sets the desired state of the PDB in-place. It is called within the controllerutil.CreateOrUpdate mutate function so that both creation and updates use identical logic.

Default Values

The controller applies a default minAvailable of 1 when neither minAvailable nor maxUnavailable is specified in the CR.

Mutual Exclusivity

The Kubernetes PDB API allows only one of minAvailable or maxUnavailable to be set. The controller enforces this:

CR ConfigurationPDB Result
Neither minAvailable nor maxUnavailable setminAvailable: 1 (default)
minAvailable setUses minAvailable, clears maxUnavailable
maxUnavailable set (no minAvailable)Uses maxUnavailable, clears minAvailable
Both minAvailable and maxUnavailable setUses minAvailable only, clears maxUnavailable

Both fields support integer values (e.g. 2) and percentage strings (e.g. "50%").

Labels

The PDB uses the same standard Kubernetes recommended labels as the Deployment and Service, generated by labelsForMemcached(name):

Label KeyValuePurpose
app.kubernetes.io/namememcachedIdentifies the application
app.kubernetes.io/instance<cr-name>Distinguishes instances of the same application
app.kubernetes.io/managed-bymemcached-operatorIdentifies the managing controller

These labels are set on both metadata.labels and spec.selector.matchLabels, ensuring the PDB protects pods managed by the same Memcached CR instance.

Selector

The PDB selector uses the same label set as the Deployment's spec.selector.matchLabels:

go
pdb.Spec.Selector = &metav1.LabelSelector{
    MatchLabels: labelsForMemcached(mc.Name),
}

Reconciliation Method

reconcilePDB(ctx, mc *Memcached) on MemcachedReconciler ensures the PDB matches the desired state:

go
func (r *MemcachedReconciler) reconcilePDB(ctx context.Context, mc *memcachedv1alpha1.Memcached) error {
    if !pdbEnabled(mc) {
        return nil
    }

    pdb := &policyv1.PodDisruptionBudget{
        ObjectMeta: metav1.ObjectMeta{
            Name:      mc.Name,
            Namespace: mc.Namespace,
        },
    }

    _, err := r.reconcileResource(ctx, mc, pdb, func() error {
        constructPDB(mc, pdb)
        return nil
    }, "PodDisruptionBudget")
    return err
}

Skip Logic

The pdbEnabled guard returns false (skipping PDB reconciliation) when:

  • spec.highAvailability is nil
  • spec.highAvailability.podDisruptionBudget is nil
  • spec.highAvailability.podDisruptionBudget.enabled is false

When PDB is not enabled, reconcilePDB returns nil immediately without error.

Owner Reference

The reconcileResource helper calls controllerutil.SetControllerReference, adding an owner reference to the PDB's metadata:

FieldValue
apiVersionmemcached.c5c3.io/v1alpha1
kindMemcached
name<cr-name>
uid<cr-uid>
controllertrue
blockOwnerDeletiontrue

This enables:

  • Garbage collection: Deleting the Memcached CR automatically deletes the owned PDB via Kubernetes' owner reference cascade.
  • Watch filtering: The Owns(&policyv1.PodDisruptionBudget{}) watch on the controller maps PDB events back to the owning Memcached CR for reconciliation.

CR Examples

PDB with Default minAvailable

yaml
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
  name: my-cache
  namespace: default
spec:
  replicas: 3
  highAvailability:
    podDisruptionBudget:
      enabled: true

Produces:

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-cache
  namespace: default
  labels:
    app.kubernetes.io/name: memcached
    app.kubernetes.io/instance: my-cache
    app.kubernetes.io/managed-by: memcached-operator
  ownerReferences:
    - apiVersion: memcached.c5c3.io/v1alpha1
      kind: Memcached
      name: my-cache
      controller: true
      blockOwnerDeletion: true
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: memcached
      app.kubernetes.io/instance: my-cache
      app.kubernetes.io/managed-by: memcached-operator

PDB with Custom minAvailable (Integer)

yaml
spec:
  replicas: 5
  highAvailability:
    podDisruptionBudget:
      enabled: true
      minAvailable: 3

Produces a PDB with spec.minAvailable: 3.

PDB with minAvailable (Percentage)

yaml
spec:
  replicas: 5
  highAvailability:
    podDisruptionBudget:
      enabled: true
      minAvailable: "50%"

Produces a PDB with spec.minAvailable: "50%".

PDB with maxUnavailable

yaml
spec:
  replicas: 5
  highAvailability:
    podDisruptionBudget:
      enabled: true
      maxUnavailable: 1

Produces a PDB with spec.maxUnavailable: 1 and no minAvailable.

PDB Disabled (Default)

yaml
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
  name: my-cache
spec:
  replicas: 3

No PDB is created. The reconcilePDB method returns immediately.


Runtime Behavior

ActionResult
Enable PDB (enabled: true)PDB created with minAvailable: 1 on next reconcile
Set minAvailable: 2PDB updated on next reconcile
Set minAvailable: "50%"PDB updated with percentage value
Switch from minAvailable to maxUnavailablePDB updated: maxUnavailable set, minAvailable cleared
Set both minAvailable and maxUnavailablePDB uses minAvailable only
Disable PDB (enabled: false)PDB reconciliation skipped; existing PDB persists until CR is deleted
Remove highAvailability sectionPDB reconciliation skipped; existing PDB persists until CR is deleted
Delete Memcached CRPDB deleted via garbage collection (owner reference)
Reconcile twice with same specNo PDB update (idempotent)
External drift (manual PDB edit)Corrected on next reconciliation cycle

Implementation

The constructPDB function in internal/controller/pdb.go is a pure function that sets PDB desired state in-place:

go
func constructPDB(mc *Memcached, pdb *PodDisruptionBudget)
  • Sets metadata.labels and spec.selector.matchLabels using labelsForMemcached
  • Applies minAvailable when set (takes precedence over maxUnavailable)
  • Applies maxUnavailable only when minAvailable is not set
  • Defaults minAvailable to 1 when neither is set
  • Clears the unused field to satisfy the Kubernetes PDB API constraint

The pdbEnabled function is a pure guard:

go
func pdbEnabled(mc *Memcached) bool
  • Returns false when spec.highAvailability is nil
  • Returns false when spec.highAvailability.podDisruptionBudget is nil
  • Returns false when enabled is false
  • Returns true only when enabled is true