PodDisruptionBudget Primitive¶
The pdb primitive wraps policy/v1 PodDisruptionBudget and reconciles it to desired state without health tracking or
suspension.
PDB is Static
Despite sitting in the "Scaling & Availability" nav group alongside HPA, PodDisruptionBudget is a
Static primitive. It has no convergence loop, no operational status, and no
suspension behavior. The resource is applied and considered ready once it exists. Readers expecting an
OperationPending condition will not find one.
Capabilities¶
The interfaces below are from pkg/component/concepts. The values in the table
are the runtime strings that appear in conditions.
| Interface | Reported status values | Notes |
|---|---|---|
Guardable |
Blocked |
Optional runtime precondition |
DataExtractable |
(side-effecting, no status) | Read generated fields after each sync cycle |
Building a PDB Primitive¶
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pdb"
minAvailable := intstr.FromString("50%")
base := &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-pdb",
Namespace: owner.Namespace,
},
Spec: policyv1.PodDisruptionBudgetSpec{
MinAvailable: &minAvailable,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "backend"},
},
},
}
resource, err := pdb.NewBuilder(base).
WithMutation(DisruptionPolicyMutation(owner.Spec.Version)).
Build()
Mutations¶
Mutations are named functions that receive a *pdb.Mutator and record edit intent through typed editors. For a full
explanation of the mutation system, boolean-gated mutations, and version-gated mutations see
The Mutation System,
Boolean-Gated Mutations, and
Version-Gated Mutations.
A concise boolean-gated example that switches from percentage-based MinAvailable to absolute MaxUnavailable:
func StrictAvailabilityMutation(version string, strict bool) pdb.Mutation {
return pdb.Mutation{
Name: "strict-availability",
Feature: feature.NewVersionGate(version, nil).When(strict),
Mutate: func(m *pdb.Mutator) error {
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.ClearMinAvailable()
e.SetMaxUnavailable(intstr.FromInt32(1))
return nil
})
return nil
},
}
}
Internal Mutation Ordering¶
Within a single mutation, edits execute in a fixed category order regardless of the order they are recorded:
| Step | Category | What it affects |
|---|---|---|
| 1 | Metadata edits | Labels and annotations on the PodDisruptionBudget |
| 2 | Spec edits | MinAvailable, MaxUnavailable, selector, eviction policy |
Features apply in registration order. Later features observe the PDB as modified by all earlier ones.
Relevant Editors¶
For the full method list of any editor see the Go API reference. The generic concept is explained in Mutation Editors.
PodDisruptionBudgetSpecEditor¶
The primary API for modifying the PDB spec. Access it via m.EditSpec.
Available methods: SetMinAvailable, SetMaxUnavailable, ClearMinAvailable, ClearMaxUnavailable, SetSelector,
SetUnhealthyPodEvictionPolicy, ClearUnhealthyPodEvictionPolicy, Raw.
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.SetMinAvailable(intstr.FromString("50%"))
e.SetSelector(&metav1.LabelSelector{
MatchLabels: map[string]string{"app": "backend"},
})
return nil
})
SetMinAvailable and SetMaxUnavailable¶
Both methods accept intstr.IntOrString, either an integer count or a percentage string (e.g. "50%"). These fields
are mutually exclusive in the Kubernetes API. When switching between them, clear the opposing field first:
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.ClearMinAvailable()
e.SetMaxUnavailable(intstr.FromInt32(1))
return nil
})
SetUnhealthyPodEvictionPolicy¶
Controls how unhealthy pods are handled during eviction. Valid values are policyv1.IfHealthyBudget and
policyv1.AlwaysAllow. Use ClearUnhealthyPodEvictionPolicy to revert to the cluster default:
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.SetUnhealthyPodEvictionPolicy(policyv1.AlwaysAllow)
return nil
})
For fields not covered by the typed API, use Raw():
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.Raw().MinAvailable = &customValue
return nil
})
ObjectMetaEditor¶
Modifies labels and annotations via m.EditObjectMetadata.
Available methods: EnsureLabel, RemoveLabel, EnsureAnnotation, RemoveAnnotation, Raw.
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
return nil
})
Data Extraction¶
Use WithDataExtractor to read generated or server-populated fields after each sync cycle. The extractor receives a
value copy of the reconciled PDB:
pdb.NewBuilder(base).
WithDataExtractor(func(p policyv1.PodDisruptionBudget) error {
// p.Status.ExpectedPods is populated by the Kubernetes PDB controller
myComponent.ExpectedPods = p.Status.ExpectedPods
return nil
})
Full Example¶
func BasePDBMutation(version string) pdb.Mutation {
return pdb.Mutation{
Name: "base-pdb",
Feature: feature.NewVersionGate(version, nil),
Mutate: func(m *pdb.Mutator) error {
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
return nil
})
return nil
},
}
}
func StrictAvailabilityMutation(version string, strict bool) pdb.Mutation {
return pdb.Mutation{
Name: "strict-availability",
Feature: feature.NewVersionGate(version, nil).When(strict),
Mutate: func(m *pdb.Mutator) error {
m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error {
e.ClearMinAvailable()
e.SetMaxUnavailable(intstr.FromInt32(1))
return nil
})
return nil
},
}
}
minAvailable := intstr.FromString("50%")
base := &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-pdb",
Namespace: owner.Namespace,
},
Spec: policyv1.PodDisruptionBudgetSpec{
MinAvailable: &minAvailable,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "backend"},
},
},
}
resource, err := pdb.NewBuilder(base).
WithMutation(BasePDBMutation(owner.Spec.Version)).
WithMutation(StrictAvailabilityMutation(owner.Spec.Version, owner.Spec.StrictMode)).
Build()
When StrictMode is true the PDB switches from percentage-based MinAvailable to an absolute MaxUnavailable of 1.
When false only the base mutation runs and the original MinAvailable from the baseline is preserved. Neither mutation
needs to know about the other.
Guidance¶
PDB is Static: there is no operational status. Registering a PDB in a component contributes no Operational or
Alive condition. It simply exists or does not. If you need lifecycle signals, use the HPA or another Integration
primitive alongside the PDB.
MinAvailable and MaxUnavailable are mutually exclusive. When switching between them, always clear the opposing
field first. The typed API makes this explicit with ClearMinAvailable and ClearMaxUnavailable.
Selector and workload labels must stay in sync. The PDB selector must match the pod labels of the workload it protects. If a mutation renames pods or changes their labels, update the PDB selector in the same release.
Register mutations in dependency order. If mutation B relies on state set by mutation A, register A first.
Use data extraction to read Status fields. Fields like Status.ExpectedPods, Status.CurrentHealthy, and
Status.DisruptionsAllowed are populated by the Kubernetes PDB controller after reconciliation. Access them through
WithDataExtractor rather than inspecting the baseline object.