Files
velero/pkg/util/podvolume/pod_volume_test.go
Shubham Pampattiwar bcdc30b59a Add PVC-to-Pod cache to improve volume policy performance
The GetPodsUsingPVC function had O(N*M) complexity - for each PVC,
it listed ALL pods in the namespace and iterated through each pod.
With many PVCs and pods, this caused significant performance
degradation (2+ seconds per PV in some cases).

This change introduces a PVC-to-Pod cache that is built once per
backup and reused for all PVC lookups, reducing complexity from
O(N*M) to O(N+M).

Changes:
- Add PVCPodCache struct with thread-safe caching in podvolume pkg
- Add NewVolumeHelperImplWithCache constructor for cache support
- Build cache before backup item processing in backup.go
- Add comprehensive unit tests for cache functionality
- Graceful fallback to direct lookups if cache fails

Fixes #9179

Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>
2025-12-15 14:18:05 -08:00

1255 lines
33 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 (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestGetVolumesToBackup(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected []string
}{
{
name: "nil annotations",
annotations: nil,
expected: nil,
},
{
name: "no volumes to backup",
annotations: map[string]string{"foo": "bar"},
expected: nil,
},
{
name: "one volume to backup",
annotations: map[string]string{"foo": "bar", velerov1api.VolumesToBackupAnnotation: "volume-1"},
expected: []string{"volume-1"},
},
{
name: "multiple volumes to backup",
annotations: map[string]string{"foo": "bar", velerov1api.VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"},
expected: []string{"volume-1", "volume-2", "volume-3"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pod := &corev1api.Pod{}
pod.Annotations = test.annotations
res := GetVolumesToBackup(pod)
// sort to ensure good compare of slices
sort.Strings(test.expected)
sort.Strings(res)
assert.Equal(t, test.expected, res)
})
}
}
func TestGetVolumesByPod(t *testing.T) {
testCases := []struct {
name string
pod *corev1api.Pod
expected struct {
included []string
optedOut []string
}
defaultVolumesToFsBackup bool
backupExcludePVC bool
}{
{
name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToFsBackup is false",
defaultVolumesToFsBackup: false,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToBackupAnnotation: "pvbPV1,pvbPV2,pvbPV3",
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
{
name: "should get all pod volumes when defaultVolumesToFsBackup is true and no PVs are excluded",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
{
name: "should get all pod volumes except ones excluded when defaultVolumesToFsBackup is true",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
/// Excluded from PVB through annotation
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
},
},
{
name: "should exclude default service account token from pod volume backup",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
/// Excluded from PVB because colume mounting default service account token
{Name: "default-token-5xq45"},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
{
name: "should exclude host path volumes from pod volume backups",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
/// Excluded from pod volume backup through annotation
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
// Excluded from pod volume backup because hostpath
{Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
},
},
{
name: "should exclude volumes mounting secrets",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
/// Excluded from pod volume backup through annotation
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
// Excluded from pod volume backup because hostpath
{Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
},
},
{
name: "should exclude volumes mounting ConfigMaps",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
// PVB Volumes
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
/// Excluded from pod volume backup through annotation
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
// Excluded from pod volume backup because hostpath
{Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
},
},
{
name: "should exclude projected volumes",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
{
Name: "projected",
VolumeSource: corev1api.VolumeSource{
Projected: &corev1api.ProjectedVolumeSource{
Sources: []corev1api.VolumeProjection{{
Secret: &corev1api.SecretProjection{
LocalObjectReference: corev1api.LocalObjectReference{},
Items: nil,
Optional: nil,
},
DownwardAPI: nil,
ConfigMap: nil,
ServiceAccountToken: nil,
}},
DefaultMode: nil,
},
},
},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
{
name: "should exclude DownwardAPI volumes",
defaultVolumesToFsBackup: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
{
Name: "downwardAPI",
VolumeSource: corev1api.VolumeSource{
DownwardAPI: &corev1api.DownwardAPIVolumeSource{
Items: []corev1api.DownwardAPIVolumeFile{
{
Path: "labels",
FieldRef: &corev1api.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.labels",
},
},
},
},
},
},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
{
name: "should exclude PVC volume when backup excludes PVC resource",
defaultVolumesToFsBackup: true,
backupExcludePVC: true,
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
{
Name: "downwardAPI",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "testPVC",
},
},
},
},
},
},
expected: struct {
included []string
optedOut []string
}{
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
optedOut: []string{},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualIncluded, actualOptedOut := GetVolumesByPod(tc.pod, tc.defaultVolumesToFsBackup, tc.backupExcludePVC, []string{})
sort.Strings(tc.expected.included)
sort.Strings(actualIncluded)
assert.Equal(t, tc.expected.included, actualIncluded)
sort.Strings(tc.expected.optedOut)
sort.Strings(actualOptedOut)
assert.Equal(t, tc.expected.optedOut, actualOptedOut)
})
}
}
func TestIsPVCDefaultToFSBackup(t *testing.T) {
objs := []runtime.Object{
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod3",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
EmptyDir: &corev1api.EmptyDirVolumeSource{},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome-pod-1",
Namespace: "awesome-ns",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "awesome-csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome-pod-2",
Namespace: "awesome-ns",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "awesome-csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "uploader-ns",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "uploader-ns",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
inPVCNamespace string
inPVCName string
expectedIsFSUploaderUsed bool
defaultVolumesToFSBackup bool
}{
{
name: "2 pods using PVC, 1 pod using uploader",
inPVCNamespace: "default",
inPVCName: "csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, 2 pods using uploader",
inPVCNamespace: "uploader-ns",
inPVCName: "csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, 0 pods using uploader",
inPVCNamespace: "awesome-ns",
inPVCName: "awesome-csi-pvc1",
expectedIsFSUploaderUsed: false,
defaultVolumesToFSBackup: false,
},
{
name: "0 pods using PVC",
inPVCNamespace: "default",
inPVCName: "does-not-exist",
expectedIsFSUploaderUsed: false,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, using uploader by default",
inPVCNamespace: "awesome-ns",
inPVCName: "awesome-csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualIsFSUploaderUsed, _ := IsPVCDefaultToFSBackup(tc.inPVCNamespace, tc.inPVCName, fakeClient, tc.defaultVolumesToFSBackup)
assert.Equal(t, tc.expectedIsFSUploaderUsed, actualIsFSUploaderUsed)
})
}
}
func TestGetPodVolumeNameForPVC(t *testing.T) {
testCases := []struct {
name string
pod corev1api.Pod
pvcName string
expectError bool
expectedVolumeName string
}{
{
name: "should get volume name for pod with multiple PVCs",
pod: corev1api.Pod{
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
{
Name: "csi-vol2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc2",
},
},
},
{
Name: "csi-vol3",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc3",
},
},
},
},
},
},
pvcName: "csi-pvc2",
expectedVolumeName: "csi-vol2",
expectError: false,
},
{
name: "should get volume name from pod using exactly one PVC",
pod: corev1api.Pod{
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
pvcName: "csi-pvc1",
expectedVolumeName: "csi-vol1",
expectError: false,
},
{
name: "should return error for pod with no PVCs",
pod: corev1api.Pod{
Spec: corev1api.PodSpec{},
},
pvcName: "csi-pvc2",
expectError: true,
},
{
name: "should return error for pod with no matching PVC",
pod: corev1api.Pod{
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
pvcName: "mismatch-pvc",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualVolumeName, err := getPodVolumeNameForPVC(tc.pod, tc.pvcName)
if tc.expectError && err == nil {
assert.Error(t, err, "Want error; Got nil error")
return
}
assert.Equalf(t, tc.expectedVolumeName, actualVolumeName, "unexpected podVolumename returned. Want %s; Got %s", tc.expectedVolumeName, actualVolumeName)
})
}
}
func TestGetPodsUsingPVC(t *testing.T) {
objs := []runtime.Object{
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod3",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
EmptyDir: &corev1api.EmptyDirVolumeSource{},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "awesome-ns",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "csi-vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
pvcNamespace string
pvcName string
expectedPodCount int
}{
{
name: "should find exactly 2 pods using the PVC",
pvcNamespace: "default",
pvcName: "csi-pvc1",
expectedPodCount: 2,
},
{
name: "should find exactly 1 pod using the PVC",
pvcNamespace: "awesome-ns",
pvcName: "csi-pvc1",
expectedPodCount: 1,
},
{
name: "should find 0 pods using the PVC",
pvcNamespace: "default",
pvcName: "unused-pvc",
expectedPodCount: 0,
},
{
name: "should find 0 pods in non-existent namespace",
pvcNamespace: "does-not-exist",
pvcName: "csi-pvc1",
expectedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualPods, err := GetPodsUsingPVC(tc.pvcNamespace, tc.pvcName, fakeClient)
require.NoErrorf(t, err, "Want error=nil; Got error=%v", err)
assert.Lenf(t, actualPods, tc.expectedPodCount, "unexpected number of pods in result; Want: %d; Got: %d", tc.expectedPodCount, len(actualPods))
})
}
}
func TestGetVolumesToProcess(t *testing.T) {
testCases := []struct {
name string
volumes []corev1api.Volume
volsToProcessByLegacyApproach []string
expectedVolumes []corev1api.Volume
}{
{
name: "pod has 2 volumes empty volsToProcessByLegacyApproach list return 2 volumes",
volumes: []corev1api.Volume{
{
Name: "sample-volume-1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-1",
},
},
},
{
Name: "sample-volume-2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-2",
},
},
},
},
volsToProcessByLegacyApproach: []string{},
expectedVolumes: []corev1api.Volume{
{
Name: "sample-volume-1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-1",
},
},
},
{
Name: "sample-volume-2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-2",
},
},
},
},
},
{
name: "pod has 2 volumes non-empty volsToProcessByLegacyApproach list returns 1 volumes",
volumes: []corev1api.Volume{
{
Name: "sample-volume-1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-1",
},
},
},
{
Name: "sample-volume-2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-2",
},
},
},
},
volsToProcessByLegacyApproach: []string{"sample-volume-2"},
expectedVolumes: []corev1api.Volume{
{
Name: "sample-volume-2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "sample-pvc-2",
},
},
},
},
},
{
name: "empty case, return empty list",
volumes: []corev1api.Volume{},
volsToProcessByLegacyApproach: []string{},
expectedVolumes: []corev1api.Volume{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualVolumes := GetVolumesToProcess(tc.volumes, tc.volsToProcessByLegacyApproach)
assert.Equal(t, tc.expectedVolumes, actualVolumes, "Want Volumes List %v; Got Volumes List %v", tc.expectedVolumes, actualVolumes)
})
}
}
func TestPVCPodCache_BuildAndGet(t *testing.T) {
objs := []runtime.Object{
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
{
Name: "vol2",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc2",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod3",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
EmptyDir: &corev1api.EmptyDirVolumeSource{},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod4",
Namespace: "other-ns",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
namespaces []string
pvcNamespace string
pvcName string
expectedPodCount int
}{
{
name: "should find 2 pods using pvc1 in default namespace",
namespaces: []string{"default", "other-ns"},
pvcNamespace: "default",
pvcName: "pvc1",
expectedPodCount: 2,
},
{
name: "should find 1 pod using pvc2 in default namespace",
namespaces: []string{"default", "other-ns"},
pvcNamespace: "default",
pvcName: "pvc2",
expectedPodCount: 1,
},
{
name: "should find 1 pod using pvc1 in other-ns",
namespaces: []string{"default", "other-ns"},
pvcNamespace: "other-ns",
pvcName: "pvc1",
expectedPodCount: 1,
},
{
name: "should find 0 pods for non-existent PVC",
namespaces: []string{"default", "other-ns"},
pvcNamespace: "default",
pvcName: "non-existent",
expectedPodCount: 0,
},
{
name: "should find 0 pods for non-existent namespace",
namespaces: []string{"default", "other-ns"},
pvcNamespace: "non-existent-ns",
pvcName: "pvc1",
expectedPodCount: 0,
},
{
name: "should find 0 pods when namespace not in cache",
namespaces: []string{"default"},
pvcNamespace: "other-ns",
pvcName: "pvc1",
expectedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cache := NewPVCPodCache()
err := cache.BuildCacheForNamespaces(t.Context(), tc.namespaces, fakeClient)
require.NoError(t, err)
assert.True(t, cache.IsBuilt())
pods := cache.GetPodsUsingPVC(tc.pvcNamespace, tc.pvcName)
assert.Len(t, pods, tc.expectedPodCount, "unexpected number of pods")
})
}
}
func TestGetPodsUsingPVCWithCache(t *testing.T) {
objs := []runtime.Object{
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
pvcNamespace string
pvcName string
buildCache bool
useNilCache bool
expectedPodCount int
}{
{
name: "returns cached results when cache is available",
pvcNamespace: "default",
pvcName: "pvc1",
buildCache: true,
useNilCache: false,
expectedPodCount: 2,
},
{
name: "falls back to direct lookup when cache is nil",
pvcNamespace: "default",
pvcName: "pvc1",
buildCache: false,
useNilCache: true,
expectedPodCount: 2,
},
{
name: "falls back to direct lookup when cache is not built",
pvcNamespace: "default",
pvcName: "pvc1",
buildCache: false,
useNilCache: false,
expectedPodCount: 2,
},
{
name: "returns empty slice for non-existent PVC with cache",
pvcNamespace: "default",
pvcName: "non-existent",
buildCache: true,
useNilCache: false,
expectedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var cache *PVCPodCache
if !tc.useNilCache {
cache = NewPVCPodCache()
if tc.buildCache {
err := cache.BuildCacheForNamespaces(t.Context(), []string{"default"}, fakeClient)
require.NoError(t, err)
}
}
pods, err := GetPodsUsingPVCWithCache(tc.pvcNamespace, tc.pvcName, fakeClient, cache)
require.NoError(t, err)
assert.Len(t, pods, tc.expectedPodCount, "unexpected number of pods")
})
}
}
func TestIsPVCDefaultToFSBackupWithCache(t *testing.T) {
objs := []runtime.Object{
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "vol1",
},
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc1",
},
},
},
},
},
},
&corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "vol1",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc2",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
pvcNamespace string
pvcName string
defaultVolumesToFsBackup bool
buildCache bool
useNilCache bool
expectedResult bool
}{
{
name: "returns true for PVC with opt-in annotation using cache",
pvcNamespace: "default",
pvcName: "pvc1",
defaultVolumesToFsBackup: false,
buildCache: true,
useNilCache: false,
expectedResult: true,
},
{
name: "returns false for PVC without annotation using cache",
pvcNamespace: "default",
pvcName: "pvc2",
defaultVolumesToFsBackup: false,
buildCache: true,
useNilCache: false,
expectedResult: false,
},
{
name: "returns true for any PVC with defaultVolumesToFsBackup using cache",
pvcNamespace: "default",
pvcName: "pvc2",
defaultVolumesToFsBackup: true,
buildCache: true,
useNilCache: false,
expectedResult: true,
},
{
name: "falls back to direct lookup when cache is nil",
pvcNamespace: "default",
pvcName: "pvc1",
defaultVolumesToFsBackup: false,
buildCache: false,
useNilCache: true,
expectedResult: true,
},
{
name: "returns false for non-existent PVC",
pvcNamespace: "default",
pvcName: "non-existent",
defaultVolumesToFsBackup: false,
buildCache: true,
useNilCache: false,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var cache *PVCPodCache
if !tc.useNilCache {
cache = NewPVCPodCache()
if tc.buildCache {
err := cache.BuildCacheForNamespaces(t.Context(), []string{"default"}, fakeClient)
require.NoError(t, err)
}
}
result, err := IsPVCDefaultToFSBackupWithCache(tc.pvcNamespace, tc.pvcName, fakeClient, tc.defaultVolumesToFsBackup, cache)
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result)
})
}
}