Files
velero/pkg/util/podvolume/pod_volume.go
Shubham Pampattiwar f592a264a6 Address review feedback: remove deprecated functions
Remove deprecated functions that were marked for removal per review:
- Remove GetPodsUsingPVC (replaced by GetPodsUsingPVCWithCache)
- Remove IsPVCDefaultToFSBackup (replaced by IsPVCDefaultToFSBackupWithCache)
- Remove associated tests for deprecated functions
- Add deprecation marker to NewVolumeHelperImpl
- Add deprecation marker to ShouldPerformSnapshotWithBackup

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>
2025-12-17 10:44:15 -08:00

385 lines
11 KiB
Go

/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podvolume
import (
"context"
"strings"
"sync"
"github.com/pkg/errors"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/util"
)
// PVCPodCache provides a cached mapping from PVC to the pods that use it.
// This cache is built once per backup to avoid repeated pod listings which
// cause O(N*M) performance issues when there are many PVCs and pods.
type PVCPodCache struct {
mu sync.RWMutex
// cache maps namespace -> pvcName -> []Pod
cache map[string]map[string][]corev1api.Pod
// built indicates whether the cache has been populated
built bool
}
// NewPVCPodCache creates a new empty PVC to Pod cache.
func NewPVCPodCache() *PVCPodCache {
return &PVCPodCache{
cache: make(map[string]map[string][]corev1api.Pod),
built: false,
}
}
// BuildCacheForNamespaces builds the cache by listing pods once per namespace.
// This is much more efficient than listing pods for each PVC lookup.
func (c *PVCPodCache) BuildCacheForNamespaces(
ctx context.Context,
namespaces []string,
crClient crclient.Client,
) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, ns := range namespaces {
podList := new(corev1api.PodList)
if err := crClient.List(
ctx,
podList,
&crclient.ListOptions{Namespace: ns},
); err != nil {
return errors.Wrapf(err, "failed to list pods in namespace %s", ns)
}
if c.cache[ns] == nil {
c.cache[ns] = make(map[string][]corev1api.Pod)
}
// Build mapping from PVC name to pods
for i := range podList.Items {
pod := podList.Items[i]
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil {
pvcName := v.PersistentVolumeClaim.ClaimName
c.cache[ns][pvcName] = append(c.cache[ns][pvcName], pod)
}
}
}
}
c.built = true
return nil
}
// GetPodsUsingPVC retrieves pods using a specific PVC from the cache.
// Returns nil slice if the PVC is not found in the cache.
func (c *PVCPodCache) GetPodsUsingPVC(namespace, pvcName string) []corev1api.Pod {
c.mu.RLock()
defer c.mu.RUnlock()
if nsPods, ok := c.cache[namespace]; ok {
if pods, ok := nsPods[pvcName]; ok {
// Return a copy to avoid race conditions
result := make([]corev1api.Pod, len(pods))
copy(result, pods)
return result
}
}
return nil
}
// IsBuilt returns true if the cache has been built.
func (c *PVCPodCache) IsBuilt() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.built
}
// IsNamespaceBuilt returns true if the cache has been built for the given namespace.
func (c *PVCPodCache) IsNamespaceBuilt(namespace string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.cache[namespace]
return ok
}
// BuildCacheForNamespace builds the cache for a single namespace lazily.
// This is used by plugins where namespaces are encountered one at a time.
// If the namespace is already cached, this is a no-op.
func (c *PVCPodCache) BuildCacheForNamespace(
ctx context.Context,
namespace string,
crClient crclient.Client,
) error {
// Check if already built (read lock first for performance)
c.mu.RLock()
if _, ok := c.cache[namespace]; ok {
c.mu.RUnlock()
return nil
}
c.mu.RUnlock()
// Need to build - acquire write lock
c.mu.Lock()
defer c.mu.Unlock()
// Double-check after acquiring write lock
if _, ok := c.cache[namespace]; ok {
return nil
}
podList := new(corev1api.PodList)
if err := crClient.List(
ctx,
podList,
&crclient.ListOptions{Namespace: namespace},
); err != nil {
return errors.Wrapf(err, "failed to list pods in namespace %s", namespace)
}
c.cache[namespace] = make(map[string][]corev1api.Pod)
// Build mapping from PVC name to pods
for i := range podList.Items {
pod := podList.Items[i]
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil {
pvcName := v.PersistentVolumeClaim.ClaimName
c.cache[namespace][pvcName] = append(c.cache[namespace][pvcName], pod)
}
}
}
// Mark as built for GetPodsUsingPVCWithCache fallback logic
c.built = true
return nil
}
// GetVolumesByPod returns a list of volume names to backup for the provided pod.
func GetVolumesByPod(pod *corev1api.Pod, defaultVolumesToFsBackup, backupExcludePVC bool, volsToProcessByLegacyApproach []string) ([]string, []string) {
// tracks the volumes that have been explicitly opted out of backup via the annotation in the pod
optedOutVolumes := make([]string, 0)
if !defaultVolumesToFsBackup {
return GetVolumesToBackup(pod), optedOutVolumes
}
volsToExclude := GetVolumesToExclude(pod)
podVolumes := []string{}
// Identify volume to process
// For normal case all the pod volume will be processed
// For case when volsToProcessByLegacyApproach is non-empty then only those volume will be processed
volsToProcess := GetVolumesToProcess(pod.Spec.Volumes, volsToProcessByLegacyApproach)
for _, pv := range volsToProcess {
// 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 pv.HostPath != nil {
continue
}
// don't backup volumes mounting secrets. Secrets will be backed up separately.
if pv.Secret != nil {
continue
}
// don't backup volumes mounting ConfigMaps. ConfigMaps will be backed up separately.
if pv.ConfigMap != nil {
continue
}
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
if pv.Projected != nil {
continue
}
// don't backup DownwardAPI volumes, all data in those come from kube state.
if pv.DownwardAPI != nil {
continue
}
if pv.PersistentVolumeClaim != nil && backupExcludePVC {
continue
}
// don't backup volumes that are included in the exclude list.
if util.Contains(volsToExclude, pv.Name) {
optedOutVolumes = append(optedOutVolumes, pv.Name)
continue
}
// don't include volumes that mount the default service account token.
if strings.HasPrefix(pv.Name, "default-token") {
continue
}
podVolumes = append(podVolumes, pv.Name)
}
return podVolumes, optedOutVolumes
}
// GetVolumesToBackup returns a list of volume names to backup for
// the provided pod.
// Deprecated: Use GetVolumesByPod instead.
func GetVolumesToBackup(obj metav1.Object) []string {
annotations := obj.GetAnnotations()
if annotations == nil {
return nil
}
backupsValue := annotations[velerov1api.VolumesToBackupAnnotation]
if backupsValue == "" {
return nil
}
return strings.Split(backupsValue, ",")
}
func GetVolumesToExclude(obj metav1.Object) []string {
annotations := obj.GetAnnotations()
if annotations == nil {
return nil
}
return strings.Split(annotations[velerov1api.VolumesToExcludeAnnotation], ",")
}
// IsPVCDefaultToFSBackupWithCache checks if a PVC should default to fs-backup based on pod annotations.
// If cache is nil or not built, it falls back to listing pods directly.
// Note: In the main backup path, the cache is always built (via NewVolumeHelperImplWithNamespaces),
// so the fallback is only used by plugins that don't need cache optimization.
func IsPVCDefaultToFSBackupWithCache(
pvcNamespace, pvcName string,
crClient crclient.Client,
defaultVolumesToFsBackup bool,
cache *PVCPodCache,
) (bool, error) {
var pods []corev1api.Pod
var err error
// Use cache if available, otherwise fall back to direct lookup
if cache != nil && cache.IsBuilt() {
pods = cache.GetPodsUsingPVC(pvcNamespace, pvcName)
} else {
pods, err = getPodsUsingPVCDirect(pvcNamespace, pvcName, crClient)
if err != nil {
return false, errors.WithStack(err)
}
}
return checkPodsForFSBackup(pods, pvcName, defaultVolumesToFsBackup)
}
// checkPodsForFSBackup is a helper function that checks if any pod using the PVC
// has the volume selected for fs-backup.
func checkPodsForFSBackup(pods []corev1api.Pod, pvcName string, defaultVolumesToFsBackup bool) (bool, error) {
for index := range pods {
vols, _ := GetVolumesByPod(&pods[index], defaultVolumesToFsBackup, false, []string{})
if len(vols) > 0 {
volName, err := getPodVolumeNameForPVC(pods[index], pvcName)
if err != nil {
return false, err
}
if util.Contains(vols, volName) {
return true, nil
}
}
}
return false, nil
}
func getPodVolumeNameForPVC(pod corev1api.Pod, pvcName string) (string, error) {
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == pvcName {
return v.Name, nil
}
}
return "", errors.Errorf("Pod %s/%s does not use PVC %s/%s", pod.Namespace, pod.Name, pod.Namespace, pvcName)
}
// GetPodsUsingPVCWithCache returns all pods that use the specified PVC.
// If cache is available and built, it uses the cache for O(1) lookup.
// Otherwise, it falls back to listing pods directly.
// Note: In the main backup path, the cache is always built (via NewVolumeHelperImplWithNamespaces),
// so the fallback is only used by plugins that don't need cache optimization.
func GetPodsUsingPVCWithCache(
pvcNamespace, pvcName string,
crClient crclient.Client,
cache *PVCPodCache,
) ([]corev1api.Pod, error) {
// Use cache if available
if cache != nil && cache.IsBuilt() {
pods := cache.GetPodsUsingPVC(pvcNamespace, pvcName)
if pods == nil {
return []corev1api.Pod{}, nil
}
return pods, nil
}
// Fall back to direct lookup (for plugins without cache)
return getPodsUsingPVCDirect(pvcNamespace, pvcName, crClient)
}
// getPodsUsingPVCDirect returns all pods in the given namespace that use the specified PVC.
// This is an internal function that lists all pods in the namespace and filters them.
func getPodsUsingPVCDirect(
pvcNamespace, pvcName string,
crClient crclient.Client,
) ([]corev1api.Pod, error) {
podsUsingPVC := []corev1api.Pod{}
podList := new(corev1api.PodList)
if err := crClient.List(
context.TODO(),
podList,
&crclient.ListOptions{Namespace: pvcNamespace},
); err != nil {
return nil, err
}
for _, p := range podList.Items {
for _, v := range p.Spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == pvcName {
podsUsingPVC = append(podsUsingPVC, p)
}
}
}
return podsUsingPVC, nil
}
func GetVolumesToProcess(volumes []corev1api.Volume, volsToProcessByLegacyApproach []string) []corev1api.Volume {
volsToProcess := make([]corev1api.Volume, 0)
// return empty list when no volumes associated with pod
if len(volumes) == 0 {
return volsToProcess
}
// legacy approach as a fallback option case
if len(volsToProcessByLegacyApproach) > 0 {
for _, vol := range volumes {
// don't process volumes that are already matched for supported action in volume policy approach
if !util.Contains(volsToProcessByLegacyApproach, vol.Name) {
continue
}
// add volume that is not processed in volume policy approach
volsToProcess = append(volsToProcess, vol)
}
return volsToProcess
}
// normal case return the list as in
return volumes
}