mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-30 11:46:16 +00:00
Add snapshotClass parameter to volume policy snapshot action (#10070)
* Add SnapshotClassParameter constant and GetSnapshotClass getter Add a new snapshotClass action parameter to volume policies, allowing users to specify which VolumeSnapshotClass to use for CSI snapshots. This follows the existing dataMover parameter pattern with a typed constant and getter method on the Action struct. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Add snapshotClass parameter validation Validate the snapshotClass parameter in Action.validate(): it must only appear on snapshot actions, must be a string, and must not be empty. Follows the same validation pattern as the dataMover parameter. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Add volume policy tier to VolumeSnapshotClass selection Add GetVolumeSnapshotClassFromVolumePolicy helper and extend GetVolumeSnapshotClass with a policySnapshotClass parameter. The new tier sits between PVC annotation and backup annotation in the priority chain: PVC annotation > volume policy > backup annotation > VSC label. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Wire snapshotClass from volume policy through CSI plugin In pvcBackupItemAction.Execute, call GetActionParameters to extract the snapshotClass from the matched volume policy and pass it through getVolumeSnapshotReference and createVolumeSnapshot to GetVolumeSnapshotClass. This connects the volume policy parameter to the CSI snapshot creation path. Fixes #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Add changelog for PR #10070 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Document snapshotClass volume policy parameter Add documentation for the new snapshotClass parameter in the volume policy snapshot action. Update the CSI docs to include volume policy as a tier in the VolumeSnapshotClass selection priority, and add Example 6 to resource-filtering.md showing multi-array usage. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Fix import ordering in pvc_action.go Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Add end-to-end test for snapshotClass volume policy parameter Verify that when a volume policy specifies snapshotClass, the CSI plugin creates a VolumeSnapshot using that VolumeSnapshotClass. The test uses a VSC without the velero label to confirm selection comes from the volume policy parameter, not the label-based fallback. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Fix gofmt struct field alignment in pvc_action_test.go Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Add GetSnapshotClass to VolumeHelper interface Add a GetSnapshotClass method to VolumeHelper that encapsulates the extraction of the snapshotClass parameter from volume policy actions. This avoids requiring callers to parse raw parameters from GetActionParameters. Simplify the CSI plugin to use the new method. Ref: #8807 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> * Fix gofmt formatting in resource_policies.go Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com> --------- Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user