The pvc primitive is the framework's built-in integration abstraction for managing Kubernetes PersistentVolumeClaim
resources. It integrates with the component lifecycle as an Operational, Graceful, Suspendable resource and provides a
structured mutation API for managing storage requests and object metadata.
| Capability | Detail |
|---|---|
| Operational tracking | Monitors PVC phase. Reports OperationalStatusOperational (Bound), OperationalStatusPending, or OperationalStatusFailing (Lost) |
| Grace status | Bound is Healthy, Lost is Down, any other phase is Degraded |
| Suspension | PVCs are immediately suspended (no runtime state to wind down); data is preserved by default |
| Mutation pipeline | Typed editors for PVC spec and object metadata, with a raw escape hatch for free-form access |
| Data extraction | Reads bound volume name, capacity, or other status fields after each sync cycle |
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pvc"
base := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "app-data",
Namespace: owner.Namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
},
},
}
resource, err := pvc.NewBuilder(base).
WithMutation(MyStorageMutation(owner.Spec.Version)).
Build()Mutations are the primary mechanism for modifying a PersistentVolumeClaim beyond its baseline. Each mutation is a
named function that receives a *Mutator and records edit intent through typed editors.
The Feature field controls when a mutation applies. Leaving it nil applies the mutation unconditionally. A feature
with no version constraints and no When() conditions is also always enabled:
func MyStorageMutation(version string) pvc.Mutation {
return pvc.Mutation{
Name: "storage-expansion",
Feature: feature.NewVersionGate(version, nil), // always enabled
Mutate: func(m *pvc.Mutator) error {
m.SetStorageRequest(resource.MustParse("20Gi"))
return nil
},
}
}Mutations are applied in the order they are registered with the builder. If one mutation depends on a change made by another, register the dependency first.
func LargeStorageMutation(version string, needsLargeStorage bool) pvc.Mutation {
return pvc.Mutation{
Name: "large-storage",
Feature: feature.NewVersionGate(version, nil).When(needsLargeStorage),
Mutate: func(m *pvc.Mutator) error {
m.SetStorageRequest(resource.MustParse("100Gi"))
return nil
},
}
}var v2Constraint = mustSemverConstraint(">= 2.0.0")
func V2StorageMutation(version string) pvc.Mutation {
return pvc.Mutation{
Name: "v2-storage",
Feature: feature.NewVersionGate(
version,
[]feature.VersionConstraint{v2Constraint},
),
Mutate: func(m *pvc.Mutator) error {
m.SetStorageRequest(resource.MustParse("50Gi"))
return nil
},
}
}All version constraints and When() conditions must be satisfied for a mutation to apply.
Within a single mutation, edit operations are applied 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 PersistentVolumeClaim |
| 2 | Spec edits | PVC spec: storage requests, access modes, etc. |
Within each category, edits are applied in their registration order. The PVC primitive groups mutations by feature
boundary: for each applicable feature (after evaluating version constraints and any When() conditions), all of its
planned edits are applied in order, and later features and mutations observe the fully-applied state from earlier ones.
The primary API for modifying PVC spec fields. Use m.EditPVCSpec for full control:
m.EditPVCSpec(func(e *editors.PVCSpecEditor) error {
e.SetStorageRequest(resource.MustParse("20Gi"))
return nil
})Available methods:
| Method | What it does |
|---|---|
SetStorageRequest |
Sets spec.resources.requests[storage] |
SetAccessModes |
Sets spec.accessModes (immutable after creation) |
SetStorageClassName |
Sets spec.storageClassName (immutable after creation) |
SetVolumeMode |
Sets spec.volumeMode (immutable after creation) |
SetVolumeName |
Sets spec.volumeName (immutable after creation) |
Raw |
Returns *corev1.PersistentVolumeClaimSpec |
Raw() returns the underlying *corev1.PersistentVolumeClaimSpec for free-form editing when none of the structured
methods are sufficient:
m.EditPVCSpec(func(e *editors.PVCSpecEditor) error {
raw := e.Raw()
raw.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"type": "fast"},
}
return nil
})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)
e.EnsureAnnotation("storage/class-hint", "fast-ssd")
return nil
})The Mutator exposes a convenience wrapper for the most common PVC operation:
| Method | Equivalent to |
|---|---|
SetStorageRequest(quantity) |
EditPVCSpec → e.SetStorageRequest(quantity) |
Use this for simple, single-operation mutations. Use EditPVCSpec when you need multiple operations or raw access in a
single edit block.
The default handler (DefaultOperationalStatusHandler) maps PVC phase to operational status:
| PVC Phase | Status | Reason |
|---|---|---|
Bound |
OperationalStatusOperational |
PVC is bound to volume <name> |
Pending |
OperationalStatusPending |
Waiting for PVC to be bound |
Lost |
OperationalStatusFailing |
PVC has lost its bound volume |
Override with WithCustomOperationalStatus for additional checks (e.g. verifying specific annotations or volume
attributes).
PVCs have no runtime state to wind down, so:
DefaultSuspendMutationHandleris a no-op.DefaultSuspensionStatusHandleralways reportsSuspended.DefaultDeleteOnSuspendHandlerreturnsfalseto preserve data.
Override these handlers if you need custom suspension behavior, such as adding annotations when suspended or deleting PVCs that use ephemeral storage.
The default grace status handler maps the PVC phase to a grace status after the grace period expires:
| PVC Phase | Status | Meaning |
|---|---|---|
Bound |
Healthy |
PVC is bound to a volume |
Lost |
Down |
PVC has lost its bound volume |
| Other | Degraded |
PVC is not yet bound |
Override with WithCustomGraceStatus:
pvc.NewBuilder(base).
WithCustomGraceStatus(func(p *corev1.PersistentVolumeClaim) (concepts.GraceStatusWithReason, error) {
status, err := pvc.DefaultGraceStatusHandler(p)
if err != nil {
return status, err
}
// Add custom logic
return status, nil
})Register mutations for storage expansion carefully. Kubernetes only allows expanding PVC storage (not shrinking).
Ensure your mutations respect this constraint. The SetStorageRequest method does not enforce this; the API server will
reject invalid requests.
Prefer WithCustomSuspendDeletionDecision over deleting PVCs manually. If you need PVCs to be cleaned up during
suspension, register a deletion decision handler rather than deleting them in a mutation.