diff --git a/design/volume-policy-pvc-volume-mode-access-modes.md b/design/volume-policy-pvc-volume-mode-access-modes.md new file mode 100644 index 000000000..fd936e5c2 --- /dev/null +++ b/design/volume-policy-pvc-volume-mode-access-modes.md @@ -0,0 +1,364 @@ +# Add PVC VolumeMode and AccessModes as Criteria for Volume Policy + +## Abstract +This proposal extends Velero VolumePolicy conditions with two PVC-based criteria, `pvcVolumeMode` and `pvcAccessModes`. +These conditions allow users to select volumes according to the `volumeMode` and `accessModes` of the associated PersistentVolumeClaim (PVC), enabling backup behavior such as skipping block-mode PVCs or choosing a specific backup method for volumes with selected access modes. + +## Background +Velero VolumePolicy already supports selecting volumes by attributes such as capacity, storage class, volume source, volume type, PVC labels, and PVC phase. +PVC metadata and spec fields are often the most direct way for users to express the intended storage semantics of a workload. + +Kubernetes PVCs include a `spec.volumeMode` field that describes whether the volume is exposed as a filesystem or as a raw block device. +The field supports values such as `Filesystem` and `Block`. + +Kubernetes PVCs also include a `spec.accessModes` field that describes how the volume can be mounted. +Common values are `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`. +Kubernetes matching semantics for access modes require all requested modes to be satisfied by the PV/PVC relationship, so this proposal uses an all-of match for `pvcAccessModes`. + +## Goals +- Add a `pvcVolumeMode` VolumePolicy condition to match volumes by a single `spec.volumeMode` value of their associated PVC. +- Add a `pvcAccessModes` VolumePolicy condition to match volumes whose associated PVC contains all configured `spec.accessModes` values. +- Keep the new conditions consistent with existing VolumePolicy behavior, where all conditions in a policy must match and the first matching policy wins. + +## Non-Goals +- This proposal does not add new VolumePolicy actions. +- This proposal does not change how PVCs are discovered or passed into the resource policy matching code. +- This proposal does not add set-based or negative matching operators such as `NotIn`, `Exists`, or `DoesNotExist`. +- This proposal does not change Kubernetes PVC semantics or validate storage provider capabilities. + +## Use-cases/Scenarios + +### Skip block-mode PVCs +A user wants to skip volumes whose associated PVC is configured with raw block volume mode. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcVolumeMode: Block + action: + type: skip +``` + +### Snapshot filesystem PVCs +A user wants to use snapshots only for volumes whose associated PVC has filesystem mode. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcVolumeMode: Filesystem + action: + type: snapshot +``` + +### Match PVCs by access mode +A user wants to apply a policy to PVCs that include `ReadWriteOnce` in `spec.accessModes`. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcAccessModes: + - ReadWriteOnce + action: + type: skip +``` + +### Match all configured access modes +A user wants to match volumes whose associated PVC includes both `ReadOnlyMany` and `ReadWriteMany`. +A PVC that includes only one of these modes does not match. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcAccessModes: + - ReadOnlyMany + - ReadWriteMany + action: + type: snapshot +``` + +### Combine PVC spec criteria +A user wants to select block-mode PVCs that also include `ReadWriteOnce`. +Because VolumePolicy conditions are conjunctive, the volume must satisfy both conditions. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcVolumeMode: Block + pvcAccessModes: + - ReadWriteOnce + action: + type: snapshot +``` + +## High-Level Design +The VolumePolicy condition schema is extended with two optional fields, `pvcVolumeMode` and `pvcAccessModes`. +`pvcVolumeMode` is represented as a single string value in the resource policy YAML. +`pvcAccessModes` is represented as a string list in the resource policy YAML. + +The internal `structuredVolume` representation is extended to store the associated PVC's volume mode and access modes. +The existing PVC parsing path populates these fields when a PVC is available in `VolumeFilterData`. + +The policy builder creates a `pvcVolumeModeCondition` when `pvcVolumeMode` is specified and creates a `pvcAccessModesCondition` when `pvcAccessModes` is specified. +The existing matching flow remains unchanged: each condition implements the `volumeCondition` interface, all conditions in a policy must match, and the first matching policy's action is returned. + +## Detailed Design + +### Resource policy YAML schema +Two new fields are added under `volumePolicies[].conditions`. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcVolumeMode: Block + pvcAccessModes: + - ReadWriteOnce + - ReadWriteMany + action: + type: snapshot +``` + +`pvcVolumeMode` is a string. +The intended values are Kubernetes PVC volume mode values, including `Filesystem` and `Block`. +The condition matches only when the PVC volume mode value observed by Velero exactly equals the configured value. +Matching is case-sensitive, so `block` does not match `Block`. + +`pvcAccessModes` is a list of strings. +The intended values are Kubernetes PVC access mode values, including `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`. +The condition matches only when every configured access mode is present in the PVC's `spec.accessModes`. +Matching is case-sensitive, so `readwriteonce` does not match `ReadWriteOnce`. + +The implementation validates that `pvcVolumeMode`, when present, is a string. +The implementation validates that `pvcAccessModes`, when present, is a list of strings. +The implementation does not strictly reject unknown string values so that the condition format remains tolerant of Kubernetes additions or storage-provider-specific behavior. +Unknown values simply do not match unless the PVC has the same string value. + +### Volume condition struct +The parsed condition struct is extended as follows. + +```go +type volumeConditions struct { + Capacity string `yaml:"capacity,omitempty"` + StorageClass []string `yaml:"storageClass,omitempty"` + NFS *nFSVolumeSource `yaml:"nfs,omitempty"` + CSI *csiVolumeSource `yaml:"csi,omitempty"` + VolumeTypes []SupportedVolume `yaml:"volumeTypes,omitempty"` + PVCLabels map[string]string `yaml:"pvcLabels,omitempty"` + PVCPhase []string `yaml:"pvcPhase,omitempty"` + PVCVolumeMode string `yaml:"pvcVolumeMode,omitempty"` + PVCAccessModes []string `yaml:"pvcAccessModes,omitempty"` +} +``` + +### Structured volume data +The internal `structuredVolume` is extended with `pvcVolumeMode` and `pvcAccessModes`. + +```go +type structuredVolume struct { + capacity resource.Quantity + storageClass string + nfs *nFSVolumeSource + csi *csiVolumeSource + volumeType SupportedVolume + pvcLabels map[string]string + pvcPhase string + pvcVolumeMode string + pvcAccessModes []string +} +``` + +When a PVC is available, `parsePVC` extracts PVC attributes into `structuredVolume` for later condition evaluation. +This parsing step does not create or imply a `pvcVolumeMode` policy condition; `pvcVolumeMode` only constrains matching when the user explicitly configures `conditions.pvcVolumeMode` in the VolumePolicy. +Velero uses `pvc.Spec.VolumeMode` as-is when it is present. +If `pvc.Spec.VolumeMode` is nil, `pvcVolumeMode` remains empty and does not match any non-empty `pvcVolumeMode` condition. +If `pvc.Spec.AccessModes` is empty, `pvcAccessModes` remains empty and does not match any non-empty `pvcAccessModes` condition. + +```go +func (s *structuredVolume) parsePVC(pvc *corev1api.PersistentVolumeClaim) { + if pvc != nil { + if len(pvc.GetLabels()) > 0 { + s.pvcLabels = pvc.Labels + } + s.pvcPhase = string(pvc.Status.Phase) + if pvc.Spec.VolumeMode != nil { + s.pvcVolumeMode = string(*pvc.Spec.VolumeMode) + } + if len(pvc.Spec.AccessModes) > 0 { + s.pvcAccessModes = make([]string, 0, len(pvc.Spec.AccessModes)) + for _, accessMode := range pvc.Spec.AccessModes { + s.pvcAccessModes = append(s.pvcAccessModes, string(accessMode)) + } + } + } +} +``` + +### PVC volume mode condition +`pvcVolumeModeCondition` matches when the associated PVC's parsed volume mode exactly equals the configured value. +The comparison is case-sensitive and does not normalize values. +An empty configured value is treated as no constraint and always matches, consistent with other VolumePolicy conditions. +A non-empty configured value does not match if no PVC volume mode is available. + +```go +type pvcVolumeModeCondition struct { + volumeMode string +} + +func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool { + if c.volumeMode == "" { + return true + } + if v.pvcVolumeMode == "" { + return false + } + return v.pvcVolumeMode == c.volumeMode +} +``` + +### PVC access modes condition +`pvcAccessModesCondition` matches when all configured access modes are present in the associated PVC's access modes. +This all-of match aligns with Kubernetes access mode matching semantics. +The comparison is case-sensitive and does not normalize values. +An empty configured list is treated as no constraint and always matches. +A non-empty configured list does not match if the structured volume has no PVC access modes. + +```go +type pvcAccessModesCondition struct { + accessModes []string +} + +func (c *pvcAccessModesCondition) match(v *structuredVolume) bool { + if len(c.accessModes) == 0 { + return true + } + if len(v.pvcAccessModes) == 0 { + return false + } + for _, conditionAccessMode := range c.accessModes { + if !slices.Contains(v.pvcAccessModes, conditionAccessMode) { + return false + } + } + return true +} +``` + +### Condition validation +Both `pvcVolumeModeCondition` and `pvcAccessModesCondition` implement the `validate()` method required by the `volumeCondition` interface. +The `validate()` method returns nil for both conditions. + +```go +func (c *pvcVolumeModeCondition) validate() error { + return nil +} + +func (c *pvcAccessModesCondition) validate() error { + return nil +} +``` + +YAML shape validation is handled when resource policy conditions are unmarshaled. +`pvcVolumeMode` must be a string, and `pvcAccessModes` must be a list of strings. +Condition-level validation intentionally does not reject unknown string values. +This keeps the policy format forward-compatible with future Kubernetes values and consistent with other string-based VolumePolicy conditions. +Unknown values simply do not match normal PVCs unless the PVC contains the same value. + +### Policy builder integration +The policy builder appends the new conditions only when the corresponding YAML fields are present. + +```go +func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error { + for _, vp := range resPolicies.VolumePolicies { + con, err := unmarshalVolConditions(vp.Conditions) + if err != nil { + return errors.WithStack(err) + } + + // Existing conditions are appended here. + + if con.PVCVolumeMode != "" { + volP.conditions = append(volP.conditions, &pvcVolumeModeCondition{volumeMode: con.PVCVolumeMode}) + } + if len(con.PVCAccessModes) > 0 { + volP.conditions = append(volP.conditions, &pvcAccessModesCondition{accessModes: con.PVCAccessModes}) + } + } + return nil +} +``` + +### Matching behavior with other conditions +The new conditions follow the existing VolumePolicy matching behavior. +Within a single policy, every configured condition must match. +If `pvcVolumeMode` is omitted from a policy, Velero does not add a volume mode condition and the policy does not restrict volume mode. +`pvcVolumeMode` and `pvcAccessModes` are PVC-specific conditions and only match when the volume policy evaluation has associated PVC data. +For non-PVC volumes such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, the parsed PVC fields are empty and policies requiring `pvcVolumeMode` or `pvcAccessModes` do not match. +Across multiple policies, the first matching policy wins. + +For example, this policy matches only PVC-backed volumes that are both `Block` mode and have `ReadWriteOnce` in their access modes. + +```yaml +version: v1 +volumePolicies: +- conditions: + pvcVolumeMode: Block + pvcAccessModes: + - ReadWriteOnce + action: + type: snapshot +``` + +## Alternatives Considered + +### A single `pvcSpec` condition object +One alternative is to add a nested object such as `pvcSpec.volumeMode` and `pvcSpec.accessModes`. +This was not chosen because existing PVC-based VolumePolicy conditions use flat field names such as `pvcLabels` and `pvcPhase`. +Flat names keep the YAML concise and consistent with existing conditions. + +### List-based `pvcVolumeMode` +One alternative is to make `pvcVolumeMode` a list, similar to `pvcPhase`. +This was not chosen because Kubernetes PVC `spec.volumeMode` is a single value and the policy condition is intended to describe an exact match against that value. +Using a string avoids implying that multiple volume modes can apply to one PVC. + +### Any-of access mode matching +Another alternative is to make `pvcAccessModes` match when any configured access mode is present on the PVC. +This was not chosen because Kubernetes access mode matching is based on satisfying all requested access modes. +Using all-of matching avoids selecting PVCs that satisfy only part of the requested access mode set. + +### Strict validation of allowed Kubernetes values +Another alternative is to reject `pvcVolumeMode` or `pvcAccessModes` values that are not currently known Kubernetes constants. +This was not chosen because accepting strings is more forward-compatible and keeps behavior consistent with other string-based resource policy conditions. +Invalid or unknown values naturally fail to match unless a PVC has the same value. + +## Security Considerations +This proposal does not introduce new privileges or access to additional Kubernetes resources. +It only uses PVC data already available to the volume policy matching path. + +The new conditions can cause Velero to skip or choose different backup actions for matched volumes. +Users should review policy configuration carefully because an overly broad policy can exclude data from backup or select an unintended backup method. + +## Compatibility +The new fields are optional and do not affect existing resource policy files. +Existing VolumePolicy behavior remains unchanged when `pvcVolumeMode` and `pvcAccessModes` are not configured. + +PVCs without a parsed `spec.volumeMode` value do not match non-empty `pvcVolumeMode` conditions. +PVCs without `spec.accessModes` do not match non-empty `pvcAccessModes` conditions. + +Unknown `pvcVolumeMode` or `pvcAccessModes` string values in a policy are accepted as strings but will not match normal Kubernetes PVCs unless the PVC contains the same string value. + +## Implementation +Implementation requires changes in the resource policies package and documentation. + +- Extend `volumeConditions` with `PVCVolumeMode string` and `PVCAccessModes []string`. +- Extend `structuredVolume` with `pvcVolumeMode string` and `pvcAccessModes []string`. +- Update `parsePVC` to populate the new fields from the PVC spec. +- Add `pvcVolumeModeCondition` and `pvcAccessModesCondition` implementations. +- Update `Policies.BuildPolicy` to append the new conditions. +- Add YAML type validation to ensure `pvcVolumeMode` is a string and `pvcAccessModes` is a string list. +- Add unit tests for parsing, validation, condition matching, and end-to-end `GetMatchAction` behavior. +- Update user documentation in `site/content/docs/main/resource-filtering.md`.