Files
velero/internal/volumehelper/volume_policy_helper.go
T
Xun Jiang/Bruce JiangandGitHub c7a93be95a Add MustIncludeAdditionalItemPVCs to help track BIA added PVC's PVB creation. (#10501)
* Add MustIncludeAdditionalItemPVCs structure in backup. It's used to track PVCs returned by BIA with mustIncluded annotaion and PVC is excluded from backup by global filter.
* Modfiy the volumeHelper interface to add a parameter function for ShouldPerformFSBackup.
* Modify to support fine-grained backup filters.
* Modify according to comments. Use a read-only interface to replace the parameter function.

Signed-off-by: Xun Jiang <xun.jiang@broadcom.com>
2026-09-09 14:04:57 +08:00

527 lines
19 KiB
Go

package volumehelper
import (
"context"
"fmt"
"strings"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper"
)
var errGetPVForPVC = errors.New("fail to get PV for PVC")
type volumeHelperImpl struct {
volumePolicy *resourcepolicies.Policies
snapshotVolumes *bool
logger logrus.FieldLogger
client crclient.Client
defaultVolumesToFSBackup bool
// This parameter is used to align the fs-backup with snapshot action,
// because PVC is already filtered by the resource filter before getting
// to the volume policy check, but fs-backup is based on the pod resource,
// the resource filter on PVC and PV doesn't work on this scenario.
backupExcludePVC bool
// pvcPodCache provides cached PVC to Pod mappings for improved performance.
// When there are many PVCs and pods, using this cache avoids O(N*M) lookups.
pvcPodCache *podvolumeutil.PVCPodCache
// pvcMustInclusionTracker provides read-only checks for whether a PVC is included
// in the backup as BIA's additionalItems through annotation
// backup.velero.io/must-include-additional-items.
pvcMustInclusionTracker vhutil.PVCMustInclusionTracker
}
// NewVolumeHelperImpl creates a VolumeHelper without PVC-to-Pod caching.
//
// Deprecated: Use NewVolumeHelperImplWithNamespaces or NewVolumeHelperImplWithCache instead
// for better performance. These functions provide PVC-to-Pod caching which avoids O(N*M)
// complexity when there are many PVCs and pods. See issue #9179 for details.
func NewVolumeHelperImpl(
volumePolicy *resourcepolicies.Policies,
snapshotVolumes *bool,
logger logrus.FieldLogger,
client crclient.Client,
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
pvcMustInclusionTracker vhutil.PVCMustInclusionTracker,
) vhutil.VolumeHelper {
// Pass nil namespaces - no cache will be built, so this never fails.
// This is used by plugins that don't need the cache optimization.
vh, _ := NewVolumeHelperImplWithNamespaces(
volumePolicy,
snapshotVolumes,
logger,
client,
defaultVolumesToFSBackup,
backupExcludePVC,
nil,
pvcMustInclusionTracker,
)
return vh
}
// NewVolumeHelperImplWithNamespaces creates a VolumeHelper with a PVC-to-Pod cache for improved performance.
// The cache is built internally from the provided namespaces list.
// This avoids O(N*M) complexity when there are many PVCs and pods.
// See issue #9179 for details.
// Returns an error if cache building fails - callers should not proceed with backup in this case.
func NewVolumeHelperImplWithNamespaces(
volumePolicy *resourcepolicies.Policies,
snapshotVolumes *bool,
logger logrus.FieldLogger,
client crclient.Client,
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
namespaces []string,
pvcMustInclusionTracker vhutil.PVCMustInclusionTracker,
) (vhutil.VolumeHelper, error) {
var pvcPodCache *podvolumeutil.PVCPodCache
if len(namespaces) > 0 {
pvcPodCache = podvolumeutil.NewPVCPodCache()
if err := pvcPodCache.BuildCacheForNamespaces(context.Background(), namespaces, client); err != nil {
return nil, err
}
logger.Infof("Built PVC-to-Pod cache for %d namespaces", len(namespaces))
}
return &volumeHelperImpl{
volumePolicy: volumePolicy,
snapshotVolumes: snapshotVolumes,
logger: logger,
client: client,
defaultVolumesToFSBackup: defaultVolumesToFSBackup,
backupExcludePVC: backupExcludePVC,
pvcPodCache: pvcPodCache,
pvcMustInclusionTracker: pvcMustInclusionTracker,
}, nil
}
// NewVolumeHelperImplWithCache creates a VolumeHelper using an externally managed PVC-to-Pod cache.
// This is used by plugins that build the cache lazily per-namespace (following the pattern from PR #9226).
// The cache can be nil, in which case PVC-to-Pod lookups will fall back to direct API calls.
func NewVolumeHelperImplWithCache(
backup velerov1api.Backup,
client crclient.Client,
logger logrus.FieldLogger,
pvcPodCache *podvolumeutil.PVCPodCache,
pvcMustInclusionTracker vhutil.PVCMustInclusionTracker,
) (vhutil.VolumeHelper, error) {
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(backup, client, logger)
if err != nil {
return nil, errors.Wrap(err, "failed to get volume policies from backup")
}
return &volumeHelperImpl{
volumePolicy: resourcePolicies,
snapshotVolumes: backup.Spec.SnapshotVolumes,
logger: logger,
client: client,
defaultVolumesToFSBackup: boolptr.IsSetToTrue(backup.Spec.DefaultVolumesToFsBackup),
backupExcludePVC: boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData),
pvcPodCache: pvcPodCache,
pvcMustInclusionTracker: pvcMustInclusionTracker,
}, nil
}
func (v *volumeHelperImpl) getPVAndMatchAction(obj runtime.Unstructured, groupResource schema.GroupResource) (*resourcepolicies.Action, *corev1api.PersistentVolume, error) {
pvc := new(corev1api.PersistentVolumeClaim)
pv := new(corev1api.PersistentVolume)
var err error
var getPVErr error
if groupResource == kuberesource.PersistentVolumeClaims {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
v.logger.WithError(err).Warn("fail to convert unstructured into PVC")
return nil, nil, err
}
pv, err = kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
v.logger.WithError(err).Warnf("failed to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
getPVErr = fmt.Errorf("fail to get PV for PVC %s: %w", pvc.Namespace+"/"+pvc.Name, errGetPVForPVC)
}
} else if groupResource == kuberesource.PersistentVolumes {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pv); err != nil {
v.logger.WithError(err).Warn("fail to convert unstructured into PV")
return nil, nil, err
}
}
if v.volumePolicy != nil {
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Warnf("fail to get VolumePolicy match action for %+v", vfd)
return nil, nil, err
}
return action, pv, getPVErr
}
return nil, pv, getPVErr
}
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
pvc := new(corev1api.PersistentVolumeClaim)
pv := new(corev1api.PersistentVolume)
var err error
var pvNotFoundErr error
if groupResource == kuberesource.PersistentVolumeClaims {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PVC")
return false, err
}
pv, err = kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
// Any error means PV not available - save to return later if no policy matches
v.logger.Debugf("PV not found for PVC %s: %v", pvc.Namespace+"/"+pvc.Name, err)
pvNotFoundErr = err
pv = nil
}
}
if groupResource == kuberesource.PersistentVolumes {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pv); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PV")
return false, err
}
}
if v.volumePolicy != nil {
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Errorf("fail to get VolumePolicy match action for %+v", vfd)
return false, err
}
// If there is a match action, and the action type is snapshot, return true,
// or the action type is not snapshot, then return false.
// If there is no match action, go on to the next check.
if action != nil {
if action.Type == resourcepolicies.Snapshot {
v.logger.Infof("performing snapshot action for %+v", vfd)
return true, nil
} else {
v.logger.Infof("Skip snapshot action for %+v as the action type is %s", vfd, action.Type)
return false, nil
}
}
}
// If resource is PVC, and PV is nil (e.g., Pending/Lost PVC with no matching policy), return the original error
if groupResource == kuberesource.PersistentVolumeClaims && pv == nil && pvNotFoundErr != nil {
v.logger.WithError(pvNotFoundErr).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, pvNotFoundErr
}
// If this PV is claimed, see if we've already taken a (pod volume backup)
// snapshot of the contents of this PV. If so, don't take a snapshot.
if pv.Spec.ClaimRef != nil {
// Use cached lookup if available for better performance with many PVCs/pods
pods, err := podvolumeutil.GetPodsUsingPVCWithCache(
pv.Spec.ClaimRef.Namespace,
pv.Spec.ClaimRef.Name,
v.client,
v.pvcPodCache,
)
if err != nil {
v.logger.WithError(err).Errorf("fail to get pod for PV %s", pv.Name)
return false, err
}
for _, pod := range pods {
for _, vol := range pod.Spec.Volumes {
if vol.PersistentVolumeClaim != nil &&
vol.PersistentVolumeClaim.ClaimName == pv.Spec.ClaimRef.Name &&
v.shouldPerformFSBackupLegacy(vol, pod) {
v.logger.Infof("Skipping snapshot of pv %s because it is backed up with PodVolumeBackup.", pv.Name)
return false, nil
}
}
}
}
if !boolptr.IsSetToFalse(v.snapshotVolumes) {
// If the backup.Spec.SnapshotVolumes is not set, or set to true, then should take the snapshot.
v.logger.Infof("performing snapshot action for pv %s as the snapshotVolumes is not set to false", pv.Name)
return true, nil
}
v.logger.Infof("skipping snapshot action for pv %s possibly due to no volume policy setting or snapshotVolumes is false", pv.Name)
return false, nil
}
func (v volumeHelperImpl) ShouldPerformFSBackup(volume corev1api.Volume, pod corev1api.Pod) (bool, error) {
if !v.shouldIncludeVolumeInBackup(volume, pod) {
v.logger.Debugf("skip fs-backup action for pod %s's volume %s, due to not pass volume check.", pod.Namespace+"/"+pod.Name, volume.Name)
return false, nil
}
var pvNotFoundErr error
if v.volumePolicy != nil {
var resource any
var err error
resource = &volume
var pvc = &corev1api.PersistentVolumeClaim{}
if volume.VolumeSource.PersistentVolumeClaim != nil {
pvc, err = kubeutil.GetPVCForPodVolume(&volume, &pod, v.client)
if err != nil {
v.logger.WithError(err).Errorf("fail to get PVC for pod %s", pod.Namespace+"/"+pod.Name)
return false, err
}
pvResource, err := kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
// Any error means PV not available - save to return later if no policy matches
v.logger.Debugf("PV not found for PVC %s: %v", pvc.Namespace+"/"+pvc.Name, err)
pvNotFoundErr = err
} else {
resource = pvResource
}
}
pv, podVolume, err := v.getVolumeFromResource(resource)
if err != nil {
return false, err
}
vfd := resourcepolicies.NewVolumeFilterData(pv, podVolume, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Error("fail to get VolumePolicy match action for volume")
return false, err
}
if action != nil {
if action.Type == resourcepolicies.FSBackup {
v.logger.Infof("Perform fs-backup action for volume %s of pod %s due to volume policy match",
volume.Name, pod.Namespace+"/"+pod.Name)
return true, nil
} else {
v.logger.Infof("Skip fs-backup action for volume %s for pod %s because the action type is %s",
volume.Name, pod.Namespace+"/"+pod.Name, action.Type)
return false, nil
}
}
// If no policy matched and PV was not found, return the original error
if pvNotFoundErr != nil {
v.logger.WithError(pvNotFoundErr).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, pvNotFoundErr
}
}
if v.shouldPerformFSBackupLegacy(volume, pod) {
v.logger.Infof("Perform fs-backup action for volume %s of pod %s due to opt-in/out way",
volume.Name, pod.Namespace+"/"+pod.Name)
return true, nil
} else {
v.logger.Infof("Skip fs-backup action for volume %s of pod %s due to opt-in/out way",
volume.Name, pod.Namespace+"/"+pod.Name)
return false, nil
}
}
func (v volumeHelperImpl) shouldPerformFSBackupLegacy(
volume corev1api.Volume,
pod corev1api.Pod,
) bool {
// Check volume in opt-in way
if !v.defaultVolumesToFSBackup {
optInVolumeNames := podvolumeutil.GetVolumesToBackup(&pod)
for _, volumeName := range optInVolumeNames {
if volume.Name == volumeName {
return true
}
}
return false
} else {
// Check volume in opt-out way
optOutVolumeNames := podvolumeutil.GetVolumesToExclude(&pod)
for _, volumeName := range optOutVolumeNames {
if volume.Name == volumeName {
return false
}
}
return true
}
}
func (v *volumeHelperImpl) ShouldPerformCustomAction(obj runtime.Unstructured, groupResource schema.GroupResource, matchParams map[string]any) (bool, error) {
action, pv, err := v.getPVAndMatchAction(obj, groupResource)
if err != nil && !errors.Is(err, errGetPVForPVC) {
return false, err
}
metadata, metaErr := meta.Accessor(obj)
if metaErr != nil {
return false, metaErr
}
if action != nil {
if action.Type == resourcepolicies.Custom {
for k, requiredValue := range matchParams {
if actionValue, ok := action.Parameters[k]; !ok || actionValue != requiredValue {
v.logger.Infof("Skipping custom action for %s: %s as value for parameter %s is %s rather than the required %s",
groupResource.String(),
metadata.GetNamespace()+"/"+metadata.GetName(),
k, actionValue, requiredValue)
return false, nil
}
}
v.logger.Infof("performing custom action for %s: %s", groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName())
return true, nil
} else {
v.logger.Infof("Skipping custom action for %s: %s as the action type is %s",
groupResource.String(),
metadata.GetNamespace()+"/"+metadata.GetName(),
action.Type)
return false, nil
}
}
if (groupResource == kuberesource.PersistentVolumeClaims) && (pv == nil) && errors.Is(err, errGetPVForPVC) {
return false, nil
}
v.logger.Infof("skipping custom action for %s: %s due to no matching volume policy",
groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName())
return false, nil
}
// returns false if no matching action found. Returns true with the action name and Parameters map if there is a matching policy
func (v *volumeHelperImpl) GetActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, string, map[string]any, error) {
action, _, err := v.getPVAndMatchAction(obj, groupResource)
if err != nil {
if errors.Is(err, errGetPVForPVC) {
return false, "", nil, nil
}
return false, "", nil, err
}
metadata, metaErr := meta.Accessor(obj)
if metaErr != nil {
return false, "", nil, metaErr
}
if action != nil {
v.logger.Infof("found matching action for %s: %s, returning parameters",
groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName())
return true, string(action.Type), action.Parameters, nil
}
v.logger.Infof("no matching volume policy found for %s: %s, no parameters to return",
groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName())
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, pod corev1api.Pod) 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 {
if v.backupExcludePVC {
if v.pvcMustInclusionTracker == nil || !v.pvcMustInclusionTracker.IsPVCIncluded(pod.Namespace, vol.PersistentVolumeClaim.ClaimName) {
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) getVolumeFromResource(resource any) (*corev1api.PersistentVolume, *corev1api.Volume, error) {
if pv, ok := resource.(*corev1api.PersistentVolume); ok {
return pv, nil, nil
} else if podVol, ok := resource.(*corev1api.Volume); ok {
return nil, podVol, nil
}
return nil, nil, fmt.Errorf("resource is not a PersistentVolume or Volume")
}
func (v *volumeHelperImpl) GetDataMoverFromActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) string {
action, _, err := v.getPVAndMatchAction(obj, groupResource)
if err != nil {
return ""
}
metadata, metaErr := meta.Accessor(obj)
if metaErr != nil {
return ""
}
if action != nil {
dataMover, err := action.GetDataMover()
if err != nil {
v.logger.WithError(err).Warn("fail to get data mover.")
return ""
}
v.logger.Infof("found matching action for %s: %s, returning data mover %s",
groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName(), dataMover)
return dataMover
}
v.logger.Debugf("no matching volume policy found for %s: %s, no data mover parameter to return",
groupResource.String(), metadata.GetNamespace()+"/"+metadata.GetName())
return ""
}