diff --git a/changelogs/unreleased/10070-shubham-pampattiwar b/changelogs/unreleased/10070-shubham-pampattiwar new file mode 100644 index 000000000..02f87194a --- /dev/null +++ b/changelogs/unreleased/10070-shubham-pampattiwar @@ -0,0 +1 @@ +Add snapshotClass parameter to volume policy snapshot action diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 08e0f8588..ad8f06ee4 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -54,6 +54,10 @@ const ( // DataMoverParameter is the key of the action parameter that selects the data // mover to be used for the matched volumes when the action type is snapshot. DataMoverParameter = "dataMover" + + // SnapshotClassParameter is the key of the action parameter that selects the + // VolumeSnapshotClass to use for CSI snapshots when the action type is snapshot. + SnapshotClassParameter = "snapshotClass" ) // validDataMovers is the set of data mover values accepted in the snapshot @@ -109,6 +113,30 @@ func (a *Action) GetDataMover() (string, error) { return dataMover, nil } +// GetSnapshotClass returns the VolumeSnapshotClass name configured in the +// snapshot action's snapshotClass parameter. The snapshotClass parameter is +// only meaningful for the snapshot action, so it returns an error when the +// action is nil or its type is not snapshot. When the parameter is absent, +// it returns an empty string, meaning the caller should fall back to the +// existing VolumeSnapshotClass selection logic. +func (a *Action) GetSnapshotClass() (string, error) { + if a == nil || a.Type != Snapshot { + return "", fmt.Errorf("the %q parameter is only supported for the %q action", SnapshotClassParameter, Snapshot) + } + if len(a.Parameters) == 0 { + return "", nil + } + raw, ok := a.Parameters[SnapshotClassParameter] + if !ok { + return "", nil + } + snapshotClass, ok := raw.(string) + if !ok { + return "", fmt.Errorf("parameter %q must be a string, got %T", SnapshotClassParameter, raw) + } + return snapshotClass, nil +} + // PolicyLabelSelector mirrors metav1.LabelSelector with yaml tags for ConfigMap decode. // metav1.LabelSelector only has json tags, which do not populate under go.yaml.in/yaml/v3. type PolicyLabelSelector struct { diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index aae458b7e..f75392d6e 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -3064,3 +3064,60 @@ func TestActionGetDataMover(t *testing.T) { }) } } + +func TestActionGetSnapshotClass(t *testing.T) { + testCases := []struct { + name string + action *Action + expectedClass string + expectErr bool + }{ + { + name: "nil action", + action: nil, + expectErr: true, + }, + { + name: "snapshot action without parameters", + action: &Action{Type: Snapshot}, + expectedClass: "", + }, + { + name: "snapshot action without snapshotClass parameter", + action: &Action{Type: Snapshot, Parameters: map[string]any{"other": "value"}}, + expectedClass: "", + }, + { + name: "snapshot action with snapshotClass", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": "my-vsc"}}, + expectedClass: "my-vsc", + }, + { + name: "non-snapshot action returns error", + action: &Action{Type: FSBackup, Parameters: map[string]any{"snapshotClass": "my-vsc"}}, + expectErr: true, + }, + { + name: "snapshot action with non-string snapshotClass returns error", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": 123}}, + expectErr: true, + }, + { + name: "snapshot action with both snapshotClass and dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": "my-vsc", "dataMover": "velero-fs"}}, + expectedClass: "my-vsc", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + snapshotClass, err := tc.action.GetSnapshotClass() + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedClass, snapshotClass) + }) + } +} diff --git a/internal/resourcepolicies/volume_resources_validator.go b/internal/resourcepolicies/volume_resources_validator.go index 332f98d2e..e1e55182a 100644 --- a/internal/resourcepolicies/volume_resources_validator.go +++ b/internal/resourcepolicies/volume_resources_validator.go @@ -118,5 +118,19 @@ func (a *Action) validate() error { } } + if raw, ok := a.Parameters[SnapshotClassParameter]; ok { + if a.Type != Snapshot { + return fmt.Errorf("parameter %q is only supported for the %q action, but the action type is %q", + SnapshotClassParameter, Snapshot, a.Type) + } + snapshotClass, ok := raw.(string) + if !ok { + return fmt.Errorf("parameter %q must be a string, got %T", SnapshotClassParameter, raw) + } + if snapshotClass == "" { + return fmt.Errorf("parameter %q must not be empty", SnapshotClassParameter) + } + } + return nil } diff --git a/internal/resourcepolicies/volume_resources_validator_test.go b/internal/resourcepolicies/volume_resources_validator_test.go index 489e9c653..6f55f8832 100644 --- a/internal/resourcepolicies/volume_resources_validator_test.go +++ b/internal/resourcepolicies/volume_resources_validator_test.go @@ -658,6 +658,86 @@ func TestValidate(t *testing.T) { }, wantErr: false, }, + { + name: "snapshot action with valid snapshotClass", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": "my-vsc"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshot action with both snapshotClass and dataMover", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": "my-vsc", "dataMover": "velero-fs"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshotClass parameter on non-snapshot action is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: FSBackup, + Parameters: map[string]any{"snapshotClass": "my-vsc"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action with non-string snapshotClass is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": 123}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action with empty snapshotClass is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": ""}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/volumehelper/volume_policy_helper.go b/internal/volumehelper/volume_policy_helper.go index 6931697c9..3259bdb43 100644 --- a/internal/volumehelper/volume_policy_helper.go +++ b/internal/volumehelper/volume_policy_helper.go @@ -430,6 +430,21 @@ func (v *volumeHelperImpl) GetActionParameters(obj runtime.Unstructured, groupRe return false, "", nil, nil } +func (v *volumeHelperImpl) GetSnapshotClass(obj runtime.Unstructured, groupResource schema.GroupResource) (string, error) { + matched, actionType, params, err := v.GetActionParameters(obj, groupResource) + if err != nil { + return "", err + } + if !matched { + return "", nil + } + action := &resourcepolicies.Action{ + Type: resourcepolicies.VolumeActionType(actionType), + Parameters: params, + } + return action.GetSnapshotClass() +} + func (v *volumeHelperImpl) shouldIncludeVolumeInBackup(vol corev1api.Volume) bool { includeVolumeInBackup := true // cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 69676da39..8df6d68de 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -212,6 +212,7 @@ func (p *pvcBackupItemAction) validatePVCAndPV( func (p *pvcBackupItemAction) createVolumeSnapshot( pvc corev1api.PersistentVolumeClaim, backup *velerov1api.Backup, + policySnapshotClass string, ) ( vs *snapshotv1api.VolumeSnapshot, err error, @@ -232,6 +233,7 @@ func (p *pvcBackupItemAction) createVolumeSnapshot( &pvc, p.log, p.crClient, + policySnapshotClass, ) if err != nil { return nil, errors.Wrapf( @@ -338,7 +340,14 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, err } - vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup) + policySnapshotClass, scErr := vh.GetSnapshotClass(item, kuberesource.PersistentVolumeClaims) + if scErr != nil { + p.log.WithError(scErr).Warn("failed to get snapshotClass from volume policy, proceeding without it") + } else if policySnapshotClass != "" { + p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", policySnapshotClass, pvc.Namespace, pvc.Name) + } + + vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup, policySnapshotClass) if err != nil { return nil, nil, "", nil, err } @@ -678,6 +687,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference( ctx context.Context, pvc corev1api.PersistentVolumeClaim, backup *velerov1api.Backup, + policySnapshotClass string, ) (*snapshotv1api.VolumeSnapshot, error) { vgsLabelKey := backup.Spec.VolumeGroupSnapshotLabelKey group, hasLabel := pvc.Labels[vgsLabelKey] @@ -808,7 +818,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference( } // Legacy fallback: create individual VS - return p.createVolumeSnapshot(pvc, backup) + return p.createVolumeSnapshot(pvc, backup, policySnapshotClass) } func (p *pvcBackupItemAction) findExistingVSForBackup( diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index b454cae9d..316bf5868 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -79,21 +79,22 @@ func (c *errorInjectingClient) Create(ctx context.Context, obj crclient.Object, func TestExecute(t *testing.T) { boolTrue := true tests := []struct { - name string - backup *velerov1api.Backup - pvc *corev1api.PersistentVolumeClaim - pv *corev1api.PersistentVolume - sc *storagev1api.StorageClass - vsClass *snapshotv1api.VolumeSnapshotClass - operationID string - expectedErr error - expectErr bool // Use bool for cases where we just need to check for any error - expectedBackup *velerov1api.Backup - expectedDataUpload *velerov2alpha1.DataUpload - expectedPVC *corev1api.PersistentVolumeClaim - resourcePolicy *corev1api.ConfigMap - failVSCreate bool - skipVSReadyUpdate bool // New flag to control VS readiness + name string + backup *velerov1api.Backup + pvc *corev1api.PersistentVolumeClaim + pv *corev1api.PersistentVolume + sc *storagev1api.StorageClass + vsClass *snapshotv1api.VolumeSnapshotClass + operationID string + expectedErr error + expectErr bool // Use bool for cases where we just need to check for any error + expectedBackup *velerov1api.Backup + expectedDataUpload *velerov2alpha1.DataUpload + expectedPVC *corev1api.PersistentVolumeClaim + resourcePolicy *corev1api.ConfigMap + failVSCreate bool + skipVSReadyUpdate bool // New flag to control VS readiness + expectedVSClassName string }{ { name: "Skip PVC BIA when backup is in finalizing phase", @@ -187,6 +188,16 @@ func TestExecute(t *testing.T) { sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), }, + { + name: "Volume policy with snapshotClass selects correct VolumeSnapshotClass", + backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(), + resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", `{"version":"v1","volumePolicies":[{"conditions":{"csi":{}},"action":{"type":"snapshot","parameters":{"snapshotClass":"policy-selected-vsclass"}}}]}`).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("policy-selected-vsclass").Driver("hostpath").Result(), + expectedVSClassName: "policy-selected-vsclass", + }, } for _, tc := range tests { @@ -300,6 +311,15 @@ func TestExecute(t *testing.T) { runtime.DefaultUnstructuredConverter.FromUnstructured(resultUnstructed.UnstructuredContent(), resultPVC) require.True(t, cmp.Equal(tc.expectedPVC, resultPVC, cmpopts.IgnoreFields(corev1api.PersistentVolumeClaim{}, "ResourceVersion", "Annotations", "Labels"))) } + + if tc.expectedVSClassName != "" { + vsList := new(snapshotv1api.VolumeSnapshotList) + require.NoError(t, crClient.List(t.Context(), vsList, &crclient.ListOptions{Namespace: tc.pvc.Namespace})) + require.NotEmpty(t, vsList.Items, "expected VolumeSnapshot to be created") + require.NotNil(t, vsList.Items[0].Spec.VolumeSnapshotClassName) + assert.Equal(t, tc.expectedVSClassName, *vsList.Items[0].Spec.VolumeSnapshotClassName, + "VolumeSnapshot should use the VolumeSnapshotClass specified by volume policy") + } }) } } diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index b78455bc8..e8fe9bead 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -314,6 +314,7 @@ func GetVolumeSnapshotClass( pvc *corev1api.PersistentVolumeClaim, log logrus.FieldLogger, crClient crclient.Client, + policySnapshotClass string, ) (*snapshotv1api.VolumeSnapshotClass, error) { snapshotClasses := new(snapshotv1api.VolumeSnapshotClassList) err := crClient.List(context.TODO(), snapshotClasses) @@ -331,6 +332,16 @@ func GetVolumeSnapshotClass( return snapshotClass, nil } + // If a snapshot class is specified by volume policy, use that + snapshotClass, err = GetVolumeSnapshotClassFromVolumePolicy( + policySnapshotClass, provisioner, snapshotClasses) + if err != nil { + log.Debugf("Didn't find VolumeSnapshotClass from volume policy: %v", err) + } + if snapshotClass != nil { + return snapshotClass, nil + } + // If there is no annotation in PVC, attempt to fetch it from backup annotations snapshotClass, err = GetVolumeSnapshotClassFromBackupAnnotationsForDriver( backup, provisioner, snapshotClasses) @@ -412,6 +423,34 @@ func GetVolumeSnapshotClassFromBackupAnnotationsForDriver( ) } +// GetVolumeSnapshotClassFromVolumePolicy returns a VolumeSnapshotClass +// specified by a volume policy's snapshotClass parameter. If +// policySnapshotClass is empty, it returns nil (no match). +func GetVolumeSnapshotClassFromVolumePolicy( + policySnapshotClass string, + provisioner string, + snapshotClasses *snapshotv1api.VolumeSnapshotClassList, +) (*snapshotv1api.VolumeSnapshotClass, error) { + if policySnapshotClass == "" { + return nil, nil + } + for _, sc := range snapshotClasses.Items { + if strings.EqualFold(policySnapshotClass, sc.ObjectMeta.Name) { + if !strings.EqualFold(sc.Driver, provisioner) { + return nil, errors.Errorf( + "VolumeSnapshotClass %s specified by volume policy is not for driver %s", + sc.ObjectMeta.Name, provisioner, + ) + } + return &sc, nil + } + } + return nil, errors.Errorf( + "No CSI VolumeSnapshotClass found with name %s specified by volume policy for driver %s", + policySnapshotClass, provisioner, + ) +} + // GetVolumeSnapshotClassForStorageClass returns a VolumeSnapshotClass // for the supplied volume provisioner/ driver name. func GetVolumeSnapshotClassForStorageClass( diff --git a/pkg/util/csi/volume_snapshot_test.go b/pkg/util/csi/volume_snapshot_test.go index 67a07d135..335cff6ee 100644 --- a/pkg/util/csi/volume_snapshot_test.go +++ b/pkg/util/csi/volume_snapshot_test.go @@ -1032,7 +1032,7 @@ func TestGetVolumeSnapshotClass(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualSnapshotClass, actualError := GetVolumeSnapshotClass( - tc.driverName, tc.backup, tc.pvc, logrus.New(), fakeClient) + tc.driverName, tc.backup, tc.pvc, logrus.New(), fakeClient, "") if tc.expectError { require.Error(t, actualError) assert.Nil(t, actualSnapshotClass) @@ -1043,6 +1043,93 @@ func TestGetVolumeSnapshotClass(t *testing.T) { } } +func TestGetVolumeSnapshotClassFromVolumePolicy(t *testing.T) { + vscArray1 := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-array-1"}, + Driver: "infinibox-csi-driver", + } + vscArray2 := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-array-2"}, + Driver: "infinibox-csi-driver", + } + vscOther := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-other"}, + Driver: "other-csi-driver", + } + + snapshotClasses := &snapshotv1api.VolumeSnapshotClassList{ + Items: []snapshotv1api.VolumeSnapshotClass{*vscArray1, *vscArray2, *vscOther}, + } + + testCases := []struct { + name string + policySnapshotClass string + provisioner string + expectedVSC *snapshotv1api.VolumeSnapshotClass + expectError bool + }{ + { + name: "empty policy returns nil", + policySnapshotClass: "", + provisioner: "infinibox-csi-driver", + expectedVSC: nil, + expectError: false, + }, + { + name: "matching VSC with correct driver", + policySnapshotClass: "vsc-array-1", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray1, + expectError: false, + }, + { + name: "matching VSC with correct driver second array", + policySnapshotClass: "vsc-array-2", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray2, + expectError: false, + }, + { + name: "VSC exists but wrong driver", + policySnapshotClass: "vsc-other", + provisioner: "infinibox-csi-driver", + expectError: true, + }, + { + name: "VSC does not exist", + policySnapshotClass: "non-existent", + provisioner: "infinibox-csi-driver", + expectError: true, + }, + { + name: "case-insensitive name matching", + policySnapshotClass: "VSC-ARRAY-1", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray1, + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualVSC, actualError := GetVolumeSnapshotClassFromVolumePolicy( + tc.policySnapshotClass, tc.provisioner, snapshotClasses) + if tc.expectError { + require.Error(t, actualError) + assert.Nil(t, actualVSC) + return + } + if tc.expectedVSC == nil { + assert.Nil(t, actualVSC) + } else { + require.NotNil(t, actualVSC) + assert.Equal(t, tc.expectedVSC.Name, actualVSC.Name) + assert.Equal(t, tc.expectedVSC.Driver, actualVSC.Driver) + } + }) + } +} + func TestGetVolumeSnapshotClassForStorageClass(t *testing.T) { hostpathClass := &snapshotv1api.VolumeSnapshotClass{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/util/volumehelper/volume_policy_helper.go b/pkg/util/volumehelper/volume_policy_helper.go index 95f104994..6abdc73f8 100644 --- a/pkg/util/volumehelper/volume_policy_helper.go +++ b/pkg/util/volumehelper/volume_policy_helper.go @@ -27,4 +27,5 @@ type VolumeHelper interface { ShouldPerformFSBackup(volume corev1api.Volume, pod corev1api.Pod) (bool, error) ShouldPerformCustomAction(obj runtime.Unstructured, groupResource schema.GroupResource, matchParams map[string]any) (bool, error) GetActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, string, map[string]any, error) + GetSnapshotClass(obj runtime.Unstructured, groupResource schema.GroupResource) (string, error) } diff --git a/site/content/docs/main/csi.md b/site/content/docs/main/csi.md index 11973f50a..68d2c5f67 100644 --- a/site/content/docs/main/csi.md +++ b/site/content/docs/main/csi.md @@ -86,8 +86,23 @@ This section documents some of the choices made during implementing the CSI snap ``` Note: Please ensure all your annotations are in lowercase. And follow the following format: `velero.io/csi-volumesnapshot-class_ = ` - 3. **Choosing VolumeSnapshotClass for a particular PVC:** - If you want to use a particular VolumeSnapshotClass for a particular PVC, you can add a annotation to the PVC to indicate which VolumeSnapshotClass to use. This overrides any annotation added to backup or schedule. For example, if you want to use the VolumeSnapshotClass `test-snapclass` for a particular PVC, you can create a PVC like this: + 3. **Choosing VolumeSnapshotClass via Volume Policy:** + If you want to use a particular VolumeSnapshotClass based on conditions like StorageClass, you can specify the `snapshotClass` parameter in a volume policy's `snapshot` action. This is useful when multiple storage arrays share the same CSI driver but require different VolumeSnapshotClasses. For example: + ```yaml + version: v1 + volumePolicies: + - conditions: + storageClass: + - nutanix-files + action: + type: snapshot + parameters: + snapshotClass: nutanix-files-snapclass + ``` + This overrides backup/schedule annotations and VolumeSnapshotClass labels, but is overridden by PVC-level annotations. See the [resource filtering documentation](resource-filtering.md) for more volume policy examples. + + 4. **Choosing VolumeSnapshotClass for a particular PVC:** + If you want to use a particular VolumeSnapshotClass for a particular PVC, you can add a annotation to the PVC to indicate which VolumeSnapshotClass to use. This overrides any other method of selecting a VolumeSnapshotClass. For example, if you want to use the VolumeSnapshotClass `test-snapclass` for a particular PVC, you can create a PVC like this: ```yaml apiVersion: v1 kind: PersistentVolumeClaim diff --git a/site/content/docs/main/resource-filtering.md b/site/content/docs/main/resource-filtering.md index d0c678462..9f57a7f4e 100644 --- a/site/content/docs/main/resource-filtering.md +++ b/site/content/docs/main/resource-filtering.md @@ -652,6 +652,7 @@ a volume policy but for a particular volume included in the backup there are no in such a scenario the legacy approach will be used for backing up the particular volume. Considering everything, the recommendation would be to use only one of the approaches to backup volumes - volume policy approach or the opt-in/opt-out legacy approach, and not mix them for clarity. - Snapshot action can either be a native snapshot or a csi snapshot or csi snapshot datamover, as is the case with the current flow where velero itself makes the decision based on the backup CR's existing options. +- The `snapshot` action supports an optional `snapshotClass` parameter that specifies which VolumeSnapshotClass to use for CSI snapshots. This is useful when multiple storage arrays share the same CSI driver but require different VolumeSnapshotClasses. When specified, this takes priority over backup annotations and VolumeSnapshotClass labels, but is overridden by PVC-level annotations. See the [CSI documentation](csi.md) for the full VolumeSnapshotClass selection priority order. - The `snapshot` action via Volume Policy has higher priority if there is a `snapshot` action matching for a particular volume, this volume would be backed up via snapshot irrespective of the value of `backup.Spec.SnapshotVolumes`. - If for a particular volume there is no `snapshot` matching action then the volume will be backed up via snapshot given that `backup.Spec.SnapshotVolumes` is not explicitly set to false. - Let's see some examples on how to use the volume policy feature for `fs-backup` and `snapshot` action purposes: @@ -740,6 +741,29 @@ volumePolicies: - `fs-backup` on `Volume 1` because `Volume 1` satisfies the criteria for `fs-backup` action. - Also, for Volume 2 as no matching action was found so legacy approach will be used as a fallback option for this volume (`fs-backup` operation will be done as `defaultVolumesToFSBackup: true` is specified by the user). +***Example 6: User has two storage arrays using the same CSI driver and needs different VolumeSnapshotClasses for each*** +1. User specifies the volume policy as follows: +```yaml +version: v1 +volumePolicies: +- conditions: + storageClass: + - array-1-sc + action: + type: snapshot + parameters: + snapshotClass: vsc-array-1 +- conditions: + storageClass: + - array-2-sc + action: + type: snapshot + parameters: + snapshotClass: vsc-array-2 +``` +2. User creates a backup using this volume policy +3. The outcome would be that velero would use `vsc-array-1` VolumeSnapshotClass for volumes on storage class `array-1-sc` and `vsc-array-2` VolumeSnapshotClass for volumes on storage class `array-2-sc`, even though both storage classes use the same CSI driver. + ### Global backup volume policies Resource policies (volume policies) are normally opt-in per backup via `--resource-policies-configmap`. An administrator can instead configure a cluster-wide baseline that applies to **every** backup by starting the Velero server with the `--global-backup-volume-policies-configmap` flag, pointing at a ConfigMap in the Velero install namespace: