mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-19 06:22:25 +00:00
feat(resourcepolicies): support PVC volume mode and access mode matching
Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user