Skip to content

Commit

Permalink
Extend Volume Policies feature to support more actions
Browse files Browse the repository at this point in the history
Signed-off-by: Shubham Pampattiwar <[email protected]>

fix volume policy action execution

Signed-off-by: Shubham Pampattiwar <[email protected]>

remove unused files

Signed-off-by: Shubham Pampattiwar <[email protected]>

add changelog file

Signed-off-by: Shubham Pampattiwar <[email protected]>

fix CI linter errors

fix linter errors
  • Loading branch information
shubham-pampattiwar committed Apr 12, 2024
1 parent 3c377bc commit fd00a0f
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 27 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/7664-shubham-pampattiwar
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implementation for Extending VolumePolicies to support more actions
2 changes: 2 additions & 0 deletions internal/resourcepolicies/resource_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
// currently only support configmap type of resource config
ConfigmapRefType string = "configmap"
Skip VolumeActionType = "skip"
FSBackup VolumeActionType = "fs-backup"
Snapshot VolumeActionType = "snapshot"
)

// Action defined as one action for a specific way of backup
Expand Down
6 changes: 3 additions & 3 deletions internal/resourcepolicies/resource_policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestGetResourceMatchedAction(t *testing.T) {
},
},
{
Action: Action{Type: "volume-snapshot"},
Action: Action{Type: "snapshot"},
Conditions: map[string]interface{}{
"capacity": "10,100Gi",
"storageClass": []string{"gp2", "ebs-sc"},
Expand All @@ -147,7 +147,7 @@ func TestGetResourceMatchedAction(t *testing.T) {
},
},
{
Action: Action{Type: "file-system-backup"},
Action: Action{Type: "fs-backup"},
Conditions: map[string]interface{}{
"storageClass": []string{"gp2", "ebs-sc"},
"csi": interface{}(
Expand Down Expand Up @@ -179,7 +179,7 @@ func TestGetResourceMatchedAction(t *testing.T) {
storageClass: "ebs-sc",
csi: &csiVolumeSource{Driver: "aws.efs.csi.driver"},
},
expectedAction: &Action{Type: "volume-snapshot"},
expectedAction: &Action{Type: "snapshot"},
},
{
name: "dismatch all policies",
Expand Down
6 changes: 5 additions & 1 deletion internal/resourcepolicies/volume_resources_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ func decodeStruct(r io.Reader, s interface{}) error {
// validate check action format
func (a *Action) validate() error {
// validate Type
if a.Type != Skip {
valid := false
if a.Type == Skip || a.Type == Snapshot || a.Type == FSBackup {
valid = true
}
if !valid {
return fmt.Errorf("invalid action type %s", a.Type)
}

Expand Down
77 changes: 76 additions & 1 deletion internal/resourcepolicies/volume_resources_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func TestValidate(t *testing.T) {
wantErr: true,
},
{
name: "supported formart volume policies",
name: "supported format volume policies",
res: &resourcePolicies{
Version: "v1",
VolumePolicies: []volumePolicy{
Expand All @@ -245,6 +245,81 @@ func TestValidate(t *testing.T) {
},
wantErr: false,
},
{
name: "supported format volume policies, action type snapshot",
res: &resourcePolicies{
Version: "v1",
VolumePolicies: []volumePolicy{
{
Action: Action{Type: "snapshot"},
Conditions: map[string]interface{}{
"capacity": "0,10Gi",
"storageClass": []string{"gp2", "ebs-sc"},
"csi": interface{}(
map[string]interface{}{
"driver": "aws.efs.csi.driver",
}),
"nfs": interface{}(
map[string]interface{}{
"server": "192.168.20.90",
"path": "/mnt/data/",
}),
},
},
},
},
wantErr: false,
},
{
name: "supported format volume policies, action type fs-backup",
res: &resourcePolicies{
Version: "v1",
VolumePolicies: []volumePolicy{
{
Action: Action{Type: "fs-backup"},
Conditions: map[string]interface{}{
"capacity": "0,10Gi",
"storageClass": []string{"gp2", "ebs-sc"},
"csi": interface{}(
map[string]interface{}{
"driver": "aws.efs.csi.driver",
}),
"nfs": interface{}(
map[string]interface{}{
"server": "192.168.20.90",
"path": "/mnt/data/",
}),
},
},
},
},
wantErr: false,
},
{
name: "supported format volume policies, action type fs-backup and snapshot",
res: &resourcePolicies{
Version: "v1",
VolumePolicies: []volumePolicy{
{
Action: Action{Type: Snapshot},
Conditions: map[string]interface{}{
"storageClass": []string{"gp2"},
},
},
{
Action: Action{Type: FSBackup},
Conditions: map[string]interface{}{
"nfs": interface{}(
map[string]interface{}{
"server": "192.168.20.90",
"path": "/mnt/data/",
}),
},
},
},
},
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
245 changes: 245 additions & 0 deletions internal/volumehelper/volume_policy_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package volumehelper

import (
"strings"

"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/vmware-tanzu/velero/internal/resourcepolicies"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
pdvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
)

type VolumeHelper interface {
GetVolumesForFSBackup(pod *corev1api.Pod) ([]string, []string, error)
ShouldPerformSnapshot(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error)
}

type VolumeHelperImpl struct {
Backup *velerov1api.Backup
VolumePolicy *resourcepolicies.Policies
BackupExcludePVC bool
DefaultVolumesToFsBackup bool
SnapshotVolumes *bool
Logger logrus.FieldLogger
DynamicFactory client.DynamicFactory
}

func (v *VolumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error) {
// check if volume policy exists and also check if the object(pv/pvc) fits a volume policy criteria and see if the associated action is snapshot
// if it is not snapshot then skip the code path for snapshotting the PV/PVC
if groupResource == kuberesource.PersistentVolumeClaims {
pvc := new(corev1api.PersistentVolumeClaim)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
return false, err
}

if v.VolumePolicy != nil {
// fetch the PV for PVC and get matching action for it
pv, err := v.GetPVForPVC(pvc)
if err != nil {
return false, err
}
action, err := v.VolumePolicy.GetMatchAction(pv)
if err != nil {
return false, err
}

// Also account for SnapshotVolumes flag on backup CR
if action != nil && action.Type == resourcepolicies.Snapshot && boolptr.IsSetToTrue(v.SnapshotVolumes) {
return true, nil
}
return false, nil
}
}

if groupResource == kuberesource.PersistentVolumes {
pv := new(corev1api.PersistentVolume)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pv); err != nil {
return false, err
}

if v.VolumePolicy != nil {
action, err := v.VolumePolicy.GetMatchAction(pv)
if err != nil {
return false, err
}

// Also account for SnapshotVolumes flag on backup CR
if action != nil && action.Type == resourcepolicies.Snapshot && boolptr.IsSetToTrue(v.SnapshotVolumes) {
return true, nil
}
return false, nil
}
}

// now if volumepolicy is not specified then just check for snapshotVolumes flag
if boolptr.IsSetToTrue(v.SnapshotVolumes) {
return true, nil
}
return false, nil
}

func (v *VolumeHelperImpl) GetVolumesForFSBackup(pod *corev1api.Pod) ([]string, []string, error) {
// Check if there is a fs-backup/snapshot volume policy specified by the user, if yes then use the volume policy approach to
// get the list volumes for fs-backup else go via the legacy annotation based approach
var includedVolumes, optedOutVolumes, volsToProcessByLegacyApproach = make([]string, 0), make([]string, 0), make([]string, 0)
var err error

if v.VolumePolicy != nil {
// Get the list of volumes to back up using pod volume backup for the given pod matching fs-backup volume policy action
v.Logger.Infof("Volume Policy specified by the user, using volume policy approach to segregate pod volumes for fs-backup")
includedVolumes, optedOutVolumes, volsToProcessByLegacyApproach, err = v.GetVolumesMatchingFSBackupAction(pod, v.VolumePolicy)
if err != nil {
return includedVolumes, optedOutVolumes, err
}
}

// process legacy annotation based approach, this will be done for 2 scenarios:
// 1. volume policy not specified by the user
// 2. there are some volumes for which the volume policy approach did not get any supported matching actions
if v.VolumePolicy == nil || len(volsToProcessByLegacyApproach) > 0 {
// Get the list of volumes to back up using pod volume backup from the pod's annotations.
// We will also pass the list of volume that did not have any supported volume policy action matched in legacy approach so that
// those volumes get processed via legacy annotation based approach, this is a fallback option on annotation based legacy approach
v.Logger.Infof("fs-backup or snapshot Volume Policy not specified by the user, using legacy approach based on annotations")
includedVolumes, optedOutVolumes = pdvolumeutil.GetVolumesByPod(pod, v.DefaultVolumesToFsBackup, v.BackupExcludePVC, volsToProcessByLegacyApproach)
}
return includedVolumes, optedOutVolumes, nil
}

// GetVolumesMatchingFSBackupAction returns a list of volume names to backup for the provided pod having fs-backup volume policy action
func (v *VolumeHelperImpl) GetVolumesMatchingFSBackupAction(pod *corev1api.Pod, volumePolicies *resourcepolicies.Policies) ([]string, []string, []string, error) {
FSBackupActionMatchingVols := []string{}
FSBackupNonActionMatchingVols := []string{}
NoActionMatchingVols := []string{}

volsToExclude := pdvolumeutil.GetVolumesToExclude(pod)
for _, vol := range pod.Spec.Volumes {
if !v.ShouldIncludeVolumeInBackup(vol, volsToExclude) {
continue
}
// don't backup volumes that are included in the exclude list.
if util.Contains(volsToExclude, vol.Name) {
FSBackupNonActionMatchingVols = append(FSBackupNonActionMatchingVols, vol.Name)
}

if vol.PersistentVolumeClaim != nil {
// fetch the associated PVC first
pvc, err := v.GetPVCForPodVolume(vol, pod)
if err != nil {
return FSBackupActionMatchingVols, FSBackupNonActionMatchingVols, NoActionMatchingVols, err
}
// now fetch the PV and call GetMatchAction on it
pv, err := v.GetPVForPVC(pvc)
if err != nil {
return FSBackupActionMatchingVols, FSBackupNonActionMatchingVols, NoActionMatchingVols, err
}
// now get the action for pv
action, err := volumePolicies.GetMatchAction(pv)
if err != nil {
return FSBackupActionMatchingVols, FSBackupNonActionMatchingVols, NoActionMatchingVols, err
}

// Record volume list having at least some action so that they are not processed in legacy fallback option
if action == nil {
NoActionMatchingVols = append(NoActionMatchingVols, vol.Name)
}

// Now if the matched action is not nil and is `fs-backup` then add that Volume to the FSBackupActionMatchingVols
// else add that volume to FSBackupNonActionMatchingVols
// we already tracked the volume not matching any kind actions supported by volume policy in NoActionMatchingVols
// The NoActionMatchingVols list will be processed via legacy annotation based approach as a fallback option
if action != nil && action.Type == resourcepolicies.FSBackup {
FSBackupActionMatchingVols = append(FSBackupActionMatchingVols, vol.Name)
} else {
FSBackupNonActionMatchingVols = append(FSBackupNonActionMatchingVols, vol.Name)
}
}
}
return FSBackupActionMatchingVols, FSBackupNonActionMatchingVols, NoActionMatchingVols, nil
}

func (v *VolumeHelperImpl) ShouldIncludeVolumeInBackup(vol corev1api.Volume, volsToExclude []string) bool {
includeVolumeInBackup := true
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
// and therefore not accessible to the node agent daemon set.
if vol.HostPath != nil {
includeVolumeInBackup = false
}
// don't backup volumes mounting secrets. Secrets will be backed up separately.
if vol.Secret != nil {
includeVolumeInBackup = false
}
// don't backup volumes mounting ConfigMaps. ConfigMaps will be backed up separately.
if vol.ConfigMap != nil {
includeVolumeInBackup = false
}
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
if vol.Projected != nil {
includeVolumeInBackup = false
}
// don't backup DownwardAPI volumes, all data in those come from kube state.
if vol.DownwardAPI != nil {
includeVolumeInBackup = false
}
if vol.PersistentVolumeClaim != nil && v.BackupExcludePVC {
includeVolumeInBackup = false
}
// don't backup volumes that are included in the exclude list.
if util.Contains(volsToExclude, vol.Name) {
includeVolumeInBackup = false
}
// don't include volumes that mount the default service account token.
if strings.HasPrefix(vol.Name, "default-token") {
includeVolumeInBackup = false
}
return includeVolumeInBackup
}

func (v *VolumeHelperImpl) GetPVCForPodVolume(vol corev1api.Volume, pod *corev1api.Pod) (*corev1api.PersistentVolumeClaim, error) {
pvcResource := metav1.APIResource{Name: "persistentvolumeclaims", Namespaced: true}
pvcClient, err := v.DynamicFactory.ClientForGroupVersionResource(schema.GroupVersion{Group: "", Version: "v1"}, pvcResource, pod.Namespace)
if err != nil {
return nil, err
}

pvcObject, err := pvcClient.Get(vol.PersistentVolumeClaim.ClaimName, metav1.GetOptions{})
if err != nil {
return nil, err
}

pvc := new(corev1api.PersistentVolumeClaim)
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(pvcObject.UnstructuredContent(), pvc); err != nil {
return nil, err
}

return pvc, nil
}

func (v *VolumeHelperImpl) GetPVForPVC(pvc *corev1api.PersistentVolumeClaim) (*corev1api.PersistentVolume, error) {
pvResource := metav1.APIResource{Name: "persistentvolumes", Namespaced: false}
pvClient, err := v.DynamicFactory.ClientForGroupVersionResource(schema.GroupVersion{Group: "", Version: "v1"}, pvResource, "")
if err != nil {
return nil, err
}

pvObject, err := pvClient.Get(pvc.Spec.VolumeName, metav1.GetOptions{})
if err != nil {
return nil, err
}

pv := new(corev1api.PersistentVolume)
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(pvObject.UnstructuredContent(), pv); err != nil {
return nil, err
}
return pv, nil
}
Loading

0 comments on commit fd00a0f

Please sign in to comment.