feat(resourcepolicies): support PVC volume mode and access mode matching

Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
chlins
2026-06-11 14:49:17 +08:00
parent ea9e7da301
commit a191a449fb
8 changed files with 745 additions and 69 deletions
+1
View File
@@ -0,0 +1 @@
feat(resourcepolicies): support PVC volume mode and access mode matching
@@ -13,11 +13,11 @@ 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`.
For resource policies, `pvcAccessModes` uses an exact set match against the PVC's `spec.accessModes`, so a policy does not match PVCs that have missing or additional access modes.
## 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.
- Add a `pvcAccessModes` VolumePolicy condition to match volumes whose associated PVC has exactly the configured `spec.accessModes` values, regardless of order.
- 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
@@ -53,7 +53,7 @@ volumePolicies:
```
### Match PVCs by access mode
A user wants to apply a policy to PVCs that include `ReadWriteOnce` in `spec.accessModes`.
A user wants to apply a policy only to PVCs whose `spec.accessModes` is exactly `ReadWriteOnce`.
```yaml
version: v1
@@ -65,9 +65,9 @@ volumePolicies:
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.
### Match an exact access mode set
A user wants to match volumes whose associated PVC access modes are exactly `ReadOnlyMany` and `ReadWriteMany`.
A PVC that includes only one of these modes, or includes additional modes, does not match.
```yaml
version: v1
@@ -81,7 +81,7 @@ volumePolicies:
```
### Combine PVC spec criteria
A user wants to select block-mode PVCs that also include `ReadWriteOnce`.
A user wants to select block-mode PVCs whose access modes are exactly `ReadWriteOnce`.
Because VolumePolicy conditions are conjunctive, the volume must satisfy both conditions.
```yaml
@@ -130,13 +130,13 @@ 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`.
The condition matches only when the configured access modes exactly equal the PVC's `spec.accessModes`, ignoring order.
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.
Unknown `pvcVolumeMode` values match only when the PVC has the same string value, and unknown `pvcAccessModes` values match only as part of the same exact access-mode set.
### Volume condition struct
The parsed condition struct is extended as follows.
@@ -221,11 +221,10 @@ func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool {
```
### 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.
`pvcAccessModesCondition` matches when the configured access modes exactly equal the associated PVC's access modes, ignoring order.
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.
A non-empty configured list does not match if the structured volume has no PVC access modes, has a different number of access modes, or has a different access-mode set.
```go
type pvcAccessModesCondition struct {
@@ -236,15 +235,11 @@ func (c *pvcAccessModesCondition) match(v *structuredVolume) bool {
if len(c.accessModes) == 0 {
return true
}
if len(v.pvcAccessModes) == 0 {
if len(v.pvcAccessModes) == 0 || len(v.pvcAccessModes) != len(c.accessModes) {
return false
}
for _, conditionAccessMode := range c.accessModes {
if !slices.Contains(v.pvcAccessModes, conditionAccessMode) {
return false
}
}
return true
return sets.New(c.accessModes...).Equal(sets.New(v.pvcAccessModes...))
}
```
@@ -266,7 +261,7 @@ 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.
Unknown values simply do not match normal PVCs unless the evaluated PVC has the same exact value or access-mode set.
### Policy builder integration
The policy builder appends the new conditions only when the corresponding YAML fields are present.
@@ -300,7 +295,7 @@ If `pvcVolumeMode` is omitted from a policy, Velero does not add a volume mode c
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.
For example, this policy matches only PVC-backed volumes that are both `Block` mode and have exactly `ReadWriteOnce` as their access modes.
```yaml
version: v1
@@ -325,10 +320,10 @@ 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.
### Contains-based access mode matching
Another alternative is to make `pvcAccessModes` match when any or all configured access modes are present on the PVC.
This was not chosen because contains-based matching would also select PVCs with additional access modes.
Using exact set matching keeps `pvcAccessModes` consistent with `pvcVolumeMode`'s exact-match behavior and avoids matching PVCs whose access mode set differs from the policy.
### Strict validation of allowed Kubernetes values
Another alternative is to reject `pvcVolumeMode` or `pvcAccessModes` values that are not currently known Kubernetes constants.
@@ -349,7 +344,7 @@ Existing VolumePolicy behavior remains unchanged when `pvcVolumeMode` and `pvcAc
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.
Unknown `pvcVolumeMode` or `pvcAccessModes` string values in a policy are accepted as strings but will not match normal Kubernetes PVCs unless the evaluated PVC has the same exact value or access-mode set.
## Implementation
Implementation requires changes in the resource policies package and documentation.
@@ -155,10 +155,35 @@ func unmarshalResourcePolicies(yamlData *string) (*ResourcePolicies, error) {
return nil, fmt.Errorf("pvcLabels must be a map of string to string, got %T", raw)
}
}
if raw, ok := vp.Conditions["pvcVolumeMode"]; ok {
if _, ok := raw.(string); !ok {
return nil, fmt.Errorf("pvcVolumeMode must be a string, got %T", raw)
}
}
if raw, ok := vp.Conditions["pvcAccessModes"]; ok {
if err := validateStringSliceCondition("pvcAccessModes", raw); err != nil {
return nil, err
}
}
}
return resPolicies, nil
}
func validateStringSliceCondition(name string, raw any) error {
switch values := raw.(type) {
case []any:
for _, value := range values {
if _, ok := value.(string); !ok {
return fmt.Errorf("%s must be a list of strings, got element %T", name, value)
}
}
case []string:
default:
return fmt.Errorf("%s must be a list of strings, got %T", name, raw)
}
return nil
}
func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error {
for _, vp := range resPolicies.VolumePolicies {
con, err := unmarshalVolConditions(vp.Conditions)
@@ -182,6 +207,12 @@ func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error {
if len(con.PVCPhase) > 0 {
volP.conditions = append(volP.conditions, &pvcPhaseCondition{phases: con.PVCPhase})
}
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})
}
p.volumePolicies = append(p.volumePolicies, volP)
}
@@ -25,6 +25,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func pvcVolumeMode(mode corev1api.PersistentVolumeMode) *corev1api.PersistentVolumeMode {
return &mode
}
func TestLoadResourcePolicies(t *testing.T) {
testCases := []struct {
name string
@@ -158,6 +162,52 @@ volumePolicies:
`,
wantErr: false,
},
{
name: "supported format pvcVolumeMode",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
action:
type: skip
`,
wantErr: false,
},
{
name: "error format of pvcVolumeMode (not a string)",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode:
- Block
action:
type: skip
`,
wantErr: true,
},
{
name: "supported format pvcAccessModes",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes:
- ReadWriteOnce
action:
type: skip
`,
wantErr: false,
},
{
name: "error format of pvcAccessModes (not a list)",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ReadWriteOnce
action:
type: skip
`,
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -1046,6 +1096,271 @@ volumePolicies:
},
skip: true,
},
{
name: "PVC volume mode matching - Block volume mode should skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-block",
},
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
},
},
skip: true,
},
{
name: "PVC volume mode matching - Filesystem volume mode should not skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-filesystem",
},
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeFilesystem),
},
},
skip: false,
},
{
name: "PVC volume mode matching - nil volume mode should not match Filesystem",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Filesystem
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-without-volume-mode",
},
},
skip: false,
},
{
name: "PVC volume mode matching - unknown condition value should not match empty volume mode",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: foo
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-without-volume-mode",
},
},
skip: false,
},
{
name: "PVC volume mode matching - omitted condition should not restrict volume mode",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-block-rwo",
},
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
},
},
skip: true,
},
{
name: "PVC volume mode matching - non-PVC volume should not match",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Filesystem
action:
type: skip`,
vol: nil,
podVol: &corev1api.Volume{
Name: "empty-dir-volume",
VolumeSource: corev1api.VolumeSource{
EmptyDir: &corev1api.EmptyDirVolumeSource{},
},
},
pvc: nil,
skip: false,
},
{
name: "PVC access modes matching - non-PVC volume should not match",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: &corev1api.Volume{
Name: "configmap-volume",
VolumeSource: corev1api.VolumeSource{
ConfigMap: &corev1api.ConfigMapVolumeSource{},
},
},
pvc: nil,
skip: false,
},
{
name: "PVC access modes matching - ReadWriteOnce should skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-rwo",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
},
},
skip: true,
},
{
name: "PVC access modes matching - extra PVC access mode should not skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-rwo-rom",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce, corev1api.ReadOnlyMany},
},
},
skip: false,
},
{
name: "PVC access modes matching - ReadWriteMany should not skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-rwx",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteMany},
},
},
skip: false,
},
{
name: "PVC access modes matching - exact access mode set should match regardless of order",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadWriteMany", "ReadOnlyMany"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-rom-rwx",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadOnlyMany, corev1api.ReadWriteMany},
},
},
skip: true,
},
{
name: "PVC access modes matching - missing one configured access mode should not skip",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcAccessModes: ["ReadOnlyMany", "ReadWriteMany"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-rwx",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteMany},
},
},
skip: false,
},
{
name: "PVC access modes matching - Combined with volume mode",
yamlData: `version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
pvcAccessModes: ["ReadWriteOnce"]
action:
type: skip`,
vol: nil,
podVol: nil,
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pvc-block-rwo",
},
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
},
},
skip: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -1119,28 +1434,36 @@ func TestGetMatchAction_Errors(t *testing.T) {
func TestParsePVC(t *testing.T) {
tests := []struct {
name string
pvc *corev1api.PersistentVolumeClaim
expectedLabels map[string]string
expectedPhase string
expectErr bool
name string
pvc *corev1api.PersistentVolumeClaim
expectedLabels map[string]string
expectedPhase string
expectedVolumeMode string
expectedAccessModes []string
expectErr bool
}{
{
name: "valid PVC with labels and Pending phase",
name: "valid PVC with labels, Pending phase, Block volume mode, and access modes",
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"env": "prod"},
},
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce, corev1api.ReadOnlyMany},
},
Status: corev1api.PersistentVolumeClaimStatus{
Phase: corev1api.ClaimPending,
},
},
expectedLabels: map[string]string{"env": "prod"},
expectedPhase: "Pending",
expectErr: false,
expectedLabels: map[string]string{"env": "prod"},
expectedPhase: "Pending",
expectedVolumeMode: "Block",
expectedAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"},
expectErr: false,
},
{
name: "valid PVC with Bound phase",
name: "valid PVC with Bound phase and nil volume mode",
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{},
@@ -1149,27 +1472,52 @@ func TestParsePVC(t *testing.T) {
Phase: corev1api.ClaimBound,
},
},
expectedLabels: nil,
expectedPhase: "Bound",
expectErr: false,
expectedLabels: nil,
expectedPhase: "Bound",
expectedVolumeMode: "",
expectedAccessModes: nil,
expectErr: false,
},
{
name: "valid PVC with Lost phase",
name: "valid PVC with Lost phase and Filesystem volume mode",
pvc: &corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeFilesystem),
},
Status: corev1api.PersistentVolumeClaimStatus{
Phase: corev1api.ClaimLost,
},
},
expectedLabels: nil,
expectedPhase: "Lost",
expectErr: false,
expectedLabels: nil,
expectedPhase: "Lost",
expectedVolumeMode: "Filesystem",
expectedAccessModes: nil,
expectErr: false,
},
{
name: "nil PVC pointer",
pvc: (*corev1api.PersistentVolumeClaim)(nil),
expectedLabels: nil,
expectedPhase: "",
expectErr: false,
name: "valid PVC with unknown non-nil volume mode",
pvc: &corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeMode("foo")),
},
Status: corev1api.PersistentVolumeClaimStatus{
Phase: corev1api.ClaimBound,
},
},
expectedLabels: nil,
expectedPhase: "Bound",
expectedVolumeMode: "foo",
expectedAccessModes: nil,
expectErr: false,
},
{
name: "nil PVC pointer",
pvc: (*corev1api.PersistentVolumeClaim)(nil),
expectedLabels: nil,
expectedPhase: "",
expectedVolumeMode: "",
expectedAccessModes: nil,
expectErr: false,
},
}
@@ -1180,6 +1528,8 @@ func TestParsePVC(t *testing.T) {
assert.Equal(t, tc.expectedLabels, s.pvcLabels)
assert.Equal(t, tc.expectedPhase, s.pvcPhase)
assert.Equal(t, tc.expectedVolumeMode, s.pvcVolumeMode)
assert.Equal(t, tc.expectedAccessModes, s.pvcAccessModes)
})
}
}
@@ -1509,7 +1859,7 @@ namespacedFilterPolicies:
- namespaces: ["team-frontend-*", "specific-ns"]
resourceFilters:
- kinds: ["Pod", "ConfigMap", "Secret"]
- namespaces: ["team-*", "another-pattern"]
- namespaces: ["team-*", "another-pattern"]
resourceFilters:
- kinds: ["Deployment", "Service"]`
@@ -1680,3 +2030,145 @@ clusterScopedFilterPolicy:
})
}
}
func TestPVCVolumeModeMatch(t *testing.T) {
tests := []struct {
name string
condition *pvcVolumeModeCondition
volume *structuredVolume
expectedMatch bool
}{
{
name: "match Block volume mode",
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
volume: &structuredVolume{pvcVolumeMode: "Block"},
expectedMatch: true,
},
{
name: "match Filesystem volume mode",
condition: &pvcVolumeModeCondition{volumeMode: "Filesystem"},
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
expectedMatch: true,
},
{
name: "no match for different volume mode",
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
expectedMatch: false,
},
{
name: "case-sensitive no match for lowercase volume mode",
condition: &pvcVolumeModeCondition{volumeMode: "block"},
volume: &structuredVolume{pvcVolumeMode: "Block"},
expectedMatch: false,
},
{
name: "no match for unknown condition value against Filesystem",
condition: &pvcVolumeModeCondition{volumeMode: "foo"},
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
expectedMatch: false,
},
{
name: "match unknown condition value only when volume has same value",
condition: &pvcVolumeModeCondition{volumeMode: "foo"},
volume: &structuredVolume{pvcVolumeMode: "foo"},
expectedMatch: true,
},
{
name: "no match for empty volume mode",
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
volume: &structuredVolume{pvcVolumeMode: ""},
expectedMatch: false,
},
{
name: "match with empty volume mode condition (always match)",
condition: &pvcVolumeModeCondition{volumeMode: ""},
volume: &structuredVolume{pvcVolumeMode: "Block"},
expectedMatch: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.condition.match(tc.volume)
assert.Equal(t, tc.expectedMatch, result)
})
}
}
func TestPVCAccessModesMatch(t *testing.T) {
tests := []struct {
name string
condition *pvcAccessModesCondition
volume *structuredVolume
expectedMatch bool
}{
{
name: "match ReadWriteOnce access mode",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
expectedMatch: true,
},
{
name: "match exact multiple access modes",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
expectedMatch: true,
},
{
name: "match exact multiple access modes regardless of order",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadOnlyMany", "ReadWriteOnce"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
expectedMatch: true,
},
{
name: "no match when one of multiple access modes is missing",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadOnlyMany"}},
expectedMatch: false,
},
{
name: "no match when PVC has extra access modes",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteMany"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadWriteMany"}},
expectedMatch: false,
},
{
name: "no match for different access mode",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteMany"}},
expectedMatch: false,
},
{
name: "case-sensitive no match for lowercase access mode",
condition: &pvcAccessModesCondition{accessModes: []string{"readwriteonce"}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
expectedMatch: false,
},
{
name: "no match for empty PVC access modes",
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
volume: &structuredVolume{pvcAccessModes: []string{}},
expectedMatch: false,
},
{
name: "match with empty access modes list (always match)",
condition: &pvcAccessModesCondition{accessModes: []string{}},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
expectedMatch: true,
},
{
name: "match with nil access modes list (always match)",
condition: &pvcAccessModesCondition{accessModes: nil},
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
expectedMatch: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.condition.match(tc.volume)
assert.Equal(t, tc.expectedMatch, result)
})
}
}
+63 -13
View File
@@ -18,9 +18,11 @@ package resourcepolicies
import (
"bytes"
"fmt"
"slices"
"strings"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/cockroachdb/errors"
"go.yaml.in/yaml/v3"
@@ -45,13 +47,15 @@ type capacity struct {
}
type structuredVolume struct {
capacity resource.Quantity
storageClass string
nfs *nFSVolumeSource
csi *csiVolumeSource
volumeType SupportedVolume
pvcLabels map[string]string
pvcPhase string
capacity resource.Quantity
storageClass string
nfs *nFSVolumeSource
csi *csiVolumeSource
volumeType SupportedVolume
pvcLabels map[string]string
pvcPhase string
pvcVolumeMode string
pvcAccessModes []string
}
func (s *structuredVolume) parsePV(pv *corev1api.PersistentVolume) {
@@ -76,6 +80,15 @@ func (s *structuredVolume) parsePVC(pvc *corev1api.PersistentVolumeClaim) {
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))
}
}
}
}
@@ -127,18 +140,55 @@ func (c *pvcPhaseCondition) match(v *structuredVolume) bool {
if v.pvcPhase == "" {
return false
}
for _, phase := range c.phases {
if v.pvcPhase == phase {
return true
}
}
return false
return slices.Contains(c.phases, v.pvcPhase)
}
func (c *pvcPhaseCondition) validate() error {
return nil
}
// pvcVolumeModeCondition defines a condition that matches if the PVC's volume mode matches the provided volume mode.
type pvcVolumeModeCondition struct {
volumeMode string
}
func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool {
// No volume mode specified: always match.
if c.volumeMode == "" {
return true
}
// Here allows unknown strings for forward compatibility. If Kubernetes adds another volume mode later,
// Velero would not reject the policy just because the string is unfamiliar.
return v.pvcVolumeMode == c.volumeMode
}
func (c *pvcVolumeModeCondition) validate() error {
return nil
}
// pvcAccessModesCondition defines a condition that matches if the PVC has exactly the provided access modes.
type pvcAccessModesCondition struct {
accessModes []string
}
func (c *pvcAccessModesCondition) match(v *structuredVolume) bool {
// No access modes specified: always match.
if len(c.accessModes) == 0 {
return true
}
if len(v.pvcAccessModes) != len(c.accessModes) {
return false
}
return sets.New(c.accessModes...).Equal(sets.New(v.pvcAccessModes...))
}
func (c *pvcAccessModesCondition) validate() error {
return nil
}
type capacityCondition struct {
capacity capacity
}
@@ -430,6 +430,38 @@ func TestUnmarshalVolumeConditions(t *testing.T) {
},
expectedError: "!!str `production` into map[string]string",
},
{
name: "Valid pvcVolumeMode input",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcVolumeMode": "Block",
},
expectedError: "",
},
{
name: "Invalid pvcVolumeMode input: not a string",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcVolumeMode": []string{"Filesystem", "Block"},
},
expectedError: "cannot unmarshal !!seq",
},
{
name: "Valid pvcAccessModes input",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcAccessModes": []string{"ReadWriteOnce", "ReadWriteMany"},
},
expectedError: "",
},
{
name: "Invalid pvcAccessModes input: not a list",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcAccessModes": "ReadWriteOnce",
},
expectedError: "cannot unmarshal !!str",
},
}
for _, tc := range testCases {
@@ -40,13 +40,15 @@ type nFSVolumeSource struct {
// volumeConditions defined the current format of conditions we parsed
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"`
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"`
}
func (c *capacityCondition) validate() error {
@@ -287,6 +287,11 @@ The policies YAML config file would look like this:
# pvc matches specific phase(s)
pvcPhase:
- Pending
# pvc matches specific volume mode
pvcVolumeMode: Block
# pvc matches specific access mode(s)
pvcAccessModes:
- ReadWriteOnce
action:
type: skip
- conditions:
@@ -380,6 +385,8 @@ Currently, Velero supports the volume attributes listed below:
- storageClass: matching volumes those with specified `storageClass`, such as `gp2`, `ebs-sc` in eks
- volume sources: matching volumes that used specified volume sources. Currently we support nfs or csi backend volume source
- pvcPhase: matching volumes based on the phase of their associated PVCs (Pending, Bound, Lost)
- pvcVolumeMode: matching volumes based on the volume mode of their associated PVCs (Filesystem, Block)
- pvcAccessModes: matching volumes based on the access modes of their associated PVCs (ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod). All configured access modes must be present on the PVC.
Velero supported conditions and format listed below:
- capacity
@@ -521,6 +528,72 @@ Velero supported conditions and format listed below:
type: skip
```
- pvc VolumeMode
This condition filters PVC-backed volumes based on the volume mode of their associated PVCs. The condition is specified as a single volume mode to match. The volume matches this condition if the PVC's volume mode exactly matches the configured value. Matching is case-sensitive, so `block` does not match `Block`. Supported volume modes are: `Filesystem` and `Block`. If `pvcVolumeMode` is omitted from a policy, volume mode is not restricted. Non-PVC volumes, such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, do not match policies that require this condition.
```yaml
pvcVolumeMode: Block
```
Some examples:
- Skip Block PVCs: Skip backup of volumes whose associated PVC uses `Block` volume mode.
```yaml
volumePolicies:
- conditions:
pvcVolumeMode: Block
action:
type: skip
```
- Combine with other conditions: You can combine PVC volume mode conditions with other conditions like PVC phase, storage class, or labels.
```yaml
volumePolicies:
- conditions:
pvcVolumeMode: Block
pvcPhase:
- Bound
action:
type: snapshot
```
- pvc AccessModes
This condition filters PVC-backed volumes based on the access modes of their associated PVCs. The condition is specified as a list of access modes to match. The volume matches this condition only if the PVC has all of the access modes in the list. Matching is case-sensitive, so `readwriteonce` does not match `ReadWriteOnce`. Supported access modes are: `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`. Non-PVC volumes, such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, do not match policies that require this condition.
```yaml
pvcAccessModes:
- ReadWriteOnce
```
Some examples:
- Skip ReadWriteOnce PVCs: Skip backup of volumes whose associated PVC includes the `ReadWriteOnce` access mode.
```yaml
volumePolicies:
- conditions:
pvcAccessModes:
- ReadWriteOnce
action:
type: skip
```
- Match multiple access modes: Apply an action to volumes whose associated PVC includes both `ReadOnlyMany` and `ReadWriteMany`.
```yaml
volumePolicies:
- conditions:
pvcAccessModes:
- ReadOnlyMany
- ReadWriteMany
action:
type: snapshot
```
- Combine with other conditions: You can combine PVC access mode conditions with other conditions like PVC volume mode, PVC phase, storage class, or labels.
```yaml
volumePolicies:
- conditions:
pvcAccessModes:
- ReadWriteOnce
pvcVolumeMode: Block
action:
type: snapshot
```
### Resource policies rules