Files
velero/pkg/itemblock/actions/pvc_action_test.go
T
2025-07-22 15:56:04 +08:00

276 lines
11 KiB
Go

/*
Copyright 2017 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 actions
import (
"fmt"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestBackupPVAction(t *testing.T) {
tests := []struct {
name string
pvc *corev1api.PersistentVolumeClaim
pods []*corev1api.Pod
expectedErr error
expectedRelated []velero.ResourceIdentifier
}{
{
name: "Test no volumeName",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").Phase(corev1api.ClaimBound).Result(),
expectedErr: nil,
expectedRelated: nil,
},
{
name: "Test empty volumeName",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("").Phase(corev1api.ClaimBound).Result(),
expectedErr: nil,
expectedRelated: nil,
},
{
name: "Test no status phase",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Result(),
expectedErr: nil,
expectedRelated: nil,
},
{
name: "Test pending status phase",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimPending).Result(),
expectedErr: nil,
expectedRelated: nil,
},
{
name: "Test lost status phase",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimLost).Result(),
expectedErr: nil,
expectedRelated: nil,
},
{
name: "Test with volume",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimBound).Result(),
expectedErr: nil,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "testPV"},
},
},
{
name: "Test with volume and one running pod",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimBound).Result(),
pods: []*corev1api.Pod{
builder.ForPod("velero", "testPod1").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodRunning).Result(),
},
expectedErr: nil,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "testPV"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod1"},
},
},
{
name: "Test with volume and multiple running pods",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimBound).Result(),
pods: []*corev1api.Pod{
builder.ForPod("velero", "testPod1").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodRunning).Result(),
builder.ForPod("velero", "testPod2").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodRunning).Result(),
builder.ForPod("velero", "testPod3").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodRunning).Result(),
},
expectedErr: nil,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "testPV"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod1"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod2"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod3"},
},
},
{
name: "Test with volume and multiple running pods, some not running",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").Phase(corev1api.ClaimBound).Result(),
pods: []*corev1api.Pod{
builder.ForPod("velero", "testPod1").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodSucceeded).Result(),
builder.ForPod("velero", "testPod2").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).NodeName("velero").Phase(corev1api.PodRunning).Result(),
builder.ForPod("velero", "testPod3").Volumes(builder.ForVolume("testPVC").PersistentVolumeClaimSource("testPVC").Result()).Phase(corev1api.PodRunning).Result(),
},
expectedErr: nil,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "testPV"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod2"},
},
},
{
name: "Test with PVC grouping via VGS label",
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC-1").ObjectMeta(builder.WithLabels("velero.io/group", "db")).VolumeName("testPV-1").Phase(corev1api.ClaimBound).Result(),
pods: []*corev1api.Pod{
builder.ForPod("velero", "testPod-1").
Volumes(builder.ForVolume("testPV-1").PersistentVolumeClaimSource("testPVC-1").Result()).
NodeName("node").
Phase(corev1api.PodRunning).Result(),
},
expectedErr: nil,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "testPV-1"},
{GroupResource: kuberesource.Pods, Namespace: "velero", Name: "testPod-1"},
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "velero", Name: "groupedPVC"},
},
},
}
backup := &v1.Backup{}
logger := logrus.New()
f := &factorymocks.Factory{}
f.On("KubebuilderClient").Return(nil, fmt.Errorf(""))
plugin := NewPVCAction(f)
_, err := plugin(logger)
require.Error(t, err)
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
f := &factorymocks.Factory{}
f.On("KubebuilderClient").Return(crClient, nil)
plugin := NewPVCAction(f)
i, err := plugin(logger)
require.NoError(t, err)
a := i.(*PVCAction)
if tc.pvc != nil {
require.NoError(t, crClient.Create(t.Context(), tc.pvc))
}
for _, pod := range tc.pods {
require.NoError(t, crClient.Create(t.Context(), pod))
}
if tc.name == "Test with PVC grouping via VGS label" {
groupedPVC := builder.ForPersistentVolumeClaim("velero", "groupedPVC").ObjectMeta(builder.WithLabels("velero.io/group", "db")).VolumeName("groupedPV").Phase(corev1api.ClaimBound).Result()
require.NoError(t, crClient.Create(t.Context(), groupedPVC))
backup.Spec.VolumeGroupSnapshotLabelKey = "velero.io/group"
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.pvc)
require.NoError(t, err)
relatedItems, err := a.GetRelatedItems(&unstructured.Unstructured{Object: pvcMap}, backup)
if tc.expectedErr != nil {
require.EqualError(t, err, tc.expectedErr.Error())
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.expectedRelated, relatedItems)
})
}
}
// Test_getGroupedPVCs verifies the PVC grouping logic for VolumeGroupSnapshots.
// This ensures only same-namespace PVCs with the same label key and value are included.
func Test_getGroupedPVCs(t *testing.T) {
tests := []struct {
name string
labelKey string
groupValue string
existingPVCs []*corev1api.PersistentVolumeClaim
targetPVC *corev1api.PersistentVolumeClaim
expectedRelated []velero.ResourceIdentifier
expectError bool
}{
{
name: "No label key in spec",
labelKey: "",
targetPVC: builder.ForPersistentVolumeClaim("ns", "pvc-1").Result(),
expectError: false,
},
{
name: "No group value",
labelKey: "velero.io/group",
groupValue: "",
targetPVC: builder.ForPersistentVolumeClaim("ns", "pvc-1").Result(),
expectError: false,
},
{
name: "Target PVC does not have the label",
labelKey: "velero.io/group",
targetPVC: builder.ForPersistentVolumeClaim("ns", "pvc-1").Result(),
expectError: false,
},
{
name: "Target PVC has label, but no group matches",
labelKey: "velero.io/group",
groupValue: "group-1",
targetPVC: builder.ForPersistentVolumeClaim("ns", "pvc-1").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
existingPVCs: []*corev1api.PersistentVolumeClaim{
builder.ForPersistentVolumeClaim("ns", "pvc-1").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
},
expectError: false,
expectedRelated: nil,
},
{
name: "Multiple PVCs in the same group",
labelKey: "velero.io/group",
groupValue: "group-1",
targetPVC: builder.ForPersistentVolumeClaim("ns", "pvc-1").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
existingPVCs: []*corev1api.PersistentVolumeClaim{
builder.ForPersistentVolumeClaim("ns", "pvc-1").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
builder.ForPersistentVolumeClaim("ns", "pvc-2").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
builder.ForPersistentVolumeClaim("ns", "pvc-3").ObjectMeta(builder.WithLabels("velero.io/group", "group-1")).Result(),
},
expectError: false,
expectedRelated: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns", Name: "pvc-2"},
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns", Name: "pvc-3"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
for _, pvc := range tc.existingPVCs {
require.NoError(t, crClient.Create(t.Context(), pvc))
}
logger := logrus.New()
a := &PVCAction{
log: logger,
crClient: crClient,
}
backup := builder.ForBackup("ns", "bkp").VolumeGroupSnapshotLabelKey(tc.labelKey).Result()
related, err := a.getGroupedPVCs(t.Context(), tc.targetPVC, backup)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.ElementsMatch(t, tc.expectedRelated, related)
})
}
}