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
spec.highAvailability.podDisruptionBudgetDefined in api/v1alpha1/memcached_types.go on the PDBSpec struct:
type PDBSpec struct {
Enabled bool `json:"enabled,omitempty"`
MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty,omitzero"`
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty,omitzero"`
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | bool | No | false | Controls whether a PDB is created |
minAvailable | int or string | No | 1 (controller default) | Minimum available pods during disruption |
maxUnavailable | int or string | No | — | Maximum 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 Configuration | PDB Result |
|---|---|
Neither minAvailable nor maxUnavailable set | minAvailable: 1 (default) |
minAvailable set | Uses minAvailable, clears maxUnavailable |
maxUnavailable set (no minAvailable) | Uses maxUnavailable, clears minAvailable |
Both minAvailable and maxUnavailable set | Uses 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 Key | Value | Purpose |
|---|---|---|
app.kubernetes.io/name | memcached | Identifies the application |
app.kubernetes.io/instance | <cr-name> | Distinguishes instances of the same application |
app.kubernetes.io/managed-by | memcached-operator | Identifies 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:
pdb.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labelsForMemcached(mc.Name),
}Reconciliation Method
reconcilePDB(ctx, mc *Memcached) on MemcachedReconciler ensures the PDB matches the desired state:
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.highAvailabilityis nilspec.highAvailability.podDisruptionBudgetis nilspec.highAvailability.podDisruptionBudget.enabledisfalse
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:
| Field | Value |
|---|---|
apiVersion | memcached.c5c3.io/v1alpha1 |
kind | Memcached |
name | <cr-name> |
uid | <cr-uid> |
controller | true |
blockOwnerDeletion | true |
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
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
name: my-cache
namespace: default
spec:
replicas: 3
highAvailability:
podDisruptionBudget:
enabled: trueProduces:
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-operatorPDB with Custom minAvailable (Integer)
spec:
replicas: 5
highAvailability:
podDisruptionBudget:
enabled: true
minAvailable: 3Produces a PDB with spec.minAvailable: 3.
PDB with minAvailable (Percentage)
spec:
replicas: 5
highAvailability:
podDisruptionBudget:
enabled: true
minAvailable: "50%"Produces a PDB with spec.minAvailable: "50%".
PDB with maxUnavailable
spec:
replicas: 5
highAvailability:
podDisruptionBudget:
enabled: true
maxUnavailable: 1Produces a PDB with spec.maxUnavailable: 1 and no minAvailable.
PDB Disabled (Default)
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
name: my-cache
spec:
replicas: 3No PDB is created. The reconcilePDB method returns immediately.
Runtime Behavior
| Action | Result |
|---|---|
Enable PDB (enabled: true) | PDB created with minAvailable: 1 on next reconcile |
Set minAvailable: 2 | PDB updated on next reconcile |
Set minAvailable: "50%" | PDB updated with percentage value |
Switch from minAvailable to maxUnavailable | PDB updated: maxUnavailable set, minAvailable cleared |
Set both minAvailable and maxUnavailable | PDB uses minAvailable only |
Disable PDB (enabled: false) | PDB reconciliation skipped; existing PDB persists until CR is deleted |
Remove highAvailability section | PDB reconciliation skipped; existing PDB persists until CR is deleted |
| Delete Memcached CR | PDB deleted via garbage collection (owner reference) |
| Reconcile twice with same spec | No 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:
func constructPDB(mc *Memcached, pdb *PodDisruptionBudget)- Sets
metadata.labelsandspec.selector.matchLabelsusinglabelsForMemcached - Applies
minAvailablewhen set (takes precedence overmaxUnavailable) - Applies
maxUnavailableonly whenminAvailableis not set - Defaults
minAvailableto1when neither is set - Clears the unused field to satisfy the Kubernetes PDB API constraint
The pdbEnabled function is a pure guard:
func pdbEnabled(mc *Memcached) bool- Returns
falsewhenspec.highAvailabilityis nil - Returns
falsewhenspec.highAvailability.podDisruptionBudgetis nil - Returns
falsewhenenabledisfalse - Returns
trueonly whenenabledistrue