mirror of
https://github.com/vmware-tanzu/velero.git
synced 2025-12-23 06:15:21 +00:00
VolumeGroupSnapshots were querying all PVCs with matching labels directly from the cluster without respecting volume policies. This caused errors when labeled PVCs included both CSI and non-CSI volumes, or volumes from different CSI drivers that were excluded by policies. This change filters PVCs by volume policy before VGS grouping, ensuring only PVCs that should be snapshotted are included in the group. A warning is logged when PVCs are excluded from VGS due to volume policy. Fixes #9344 Signed-off-by: Shubham Pampattiwar <spampatt@redhat.com>
1962 lines
58 KiB
Go
1962 lines
58 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 csi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
|
|
|
volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
|
|
"github.com/stretchr/testify/assert"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/utils/pointer"
|
|
|
|
"github.com/vmware-tanzu/velero/pkg/label"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/require"
|
|
corev1api "k8s.io/api/core/v1"
|
|
storagev1api "k8s.io/api/storage/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
crclient "sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
|
|
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
|
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
|
"github.com/vmware-tanzu/velero/pkg/builder"
|
|
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
|
|
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
|
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
|
)
|
|
|
|
const testDriver = "csi.example.com"
|
|
|
|
// errorInjectingClient is a wrapper around a normal client that injects an error
|
|
// when a specific resource type (VolumeSnapshot) is created.
|
|
type errorInjectingClient struct {
|
|
crclient.Client
|
|
}
|
|
|
|
// Create overrides the embedded client's Create method.
|
|
func (c *errorInjectingClient) Create(ctx context.Context, obj crclient.Object, opts ...crclient.CreateOption) error {
|
|
// Check if the object being created is a VolumeSnapshot.
|
|
if _, ok := obj.(*snapshotv1api.VolumeSnapshot); ok {
|
|
// If it is, return our injected error instead of proceeding.
|
|
return errors.New("injected error on create")
|
|
}
|
|
// For all other object types, call the original, embedded Create method.
|
|
return c.Client.Create(ctx, obj, opts...)
|
|
}
|
|
|
|
func TestExecute(t *testing.T) {
|
|
boolTrue := true
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1api.Backup
|
|
pvc *corev1api.PersistentVolumeClaim
|
|
pv *corev1api.PersistentVolume
|
|
sc *storagev1api.StorageClass
|
|
vsClass *snapshotv1api.VolumeSnapshotClass
|
|
operationID string
|
|
expectedErr error
|
|
expectErr bool // Use bool for cases where we just need to check for any error
|
|
expectedBackup *velerov1api.Backup
|
|
expectedDataUpload *velerov2alpha1.DataUpload
|
|
expectedPVC *corev1api.PersistentVolumeClaim
|
|
resourcePolicy *corev1api.ConfigMap
|
|
failVSCreate bool
|
|
skipVSReadyUpdate bool // New flag to control VS readiness
|
|
}{
|
|
{
|
|
name: "Skip PVC BIA when backup is in finalizing phase",
|
|
backup: builder.ForBackup("velero", "test").Phase(velerov1api.BackupPhaseFinalizing).Result(),
|
|
},
|
|
{
|
|
name: "Fail when creating volumesnapshot returns error",
|
|
backup: builder.ForBackup("velero", "test").CSISnapshotTimeout(1 * time.Minute).Result(),
|
|
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
|
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
|
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
|
failVSCreate: true,
|
|
expectedErr: errors.New("error creating volume snapshot: injected error on create"),
|
|
},
|
|
{
|
|
name: "Fail when waiting for VolumeSnapshot to be ready times out",
|
|
backup: builder.ForBackup("velero", "test").CSISnapshotTimeout(20 * time.Millisecond).Result(), // Short timeout
|
|
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
|
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
|
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
|
skipVSReadyUpdate: true, // This will cause the timeout
|
|
expectErr: true, // Expect an error, but the exact message can vary
|
|
},
|
|
{
|
|
name: "Test SnapshotMoveData",
|
|
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
|
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
|
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
|
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
|
operationID: ".",
|
|
expectedDataUpload: &velerov2alpha1.DataUpload{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "DataUpload",
|
|
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "test-",
|
|
Namespace: "velero",
|
|
Labels: map[string]string{
|
|
velerov1api.BackupNameLabel: "test",
|
|
velerov1api.BackupUIDLabel: "",
|
|
velerov1api.PVCUIDLabel: "",
|
|
velerov1api.AsyncOperationIDLabel: "du-.",
|
|
},
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "velero.io/v1",
|
|
Kind: "Backup",
|
|
Name: "test",
|
|
UID: "",
|
|
Controller: &boolTrue,
|
|
},
|
|
},
|
|
},
|
|
Spec: velerov2alpha1.DataUploadSpec{
|
|
SnapshotType: velerov2alpha1.SnapshotTypeCSI,
|
|
CSISnapshot: &velerov2alpha1.CSISnapshotSpec{
|
|
VolumeSnapshot: "",
|
|
StorageClass: "testSC",
|
|
SnapshotClass: "testVSClass",
|
|
},
|
|
SourcePVC: "testPVC",
|
|
SourceNamespace: "velero",
|
|
OperationTimeout: metav1.Duration{Duration: 1 * time.Minute},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Verify PVC is modified as expected",
|
|
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
|
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
|
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
|
vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
|
operationID: ".",
|
|
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").
|
|
ObjectMeta(builder.WithAnnotations(velerov1api.MustIncludeAdditionalItemAnnotation, "true", velerov1api.DataUploadNameAnnotation, "velero/"),
|
|
builder.WithLabels(velerov1api.BackupNameLabel, "test")).
|
|
VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
},
|
|
{
|
|
name: "Test ResourcePolicy",
|
|
backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").SnapshotVolumes(false).CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(),
|
|
resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", "{\"version\":\"v1\", \"volumePolicies\":[{\"conditions\":{\"csi\": {}},\"action\":{\"type\":\"snapshot\"}}]}").Result(),
|
|
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
|
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
|
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
|
vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
logger := logrus.New()
|
|
logger.Level = logrus.DebugLevel
|
|
objects := make([]runtime.Object, 0)
|
|
if tc.pvc != nil {
|
|
objects = append(objects, tc.pvc)
|
|
}
|
|
if tc.pv != nil {
|
|
objects = append(objects, tc.pv)
|
|
}
|
|
if tc.sc != nil {
|
|
objects = append(objects, tc.sc)
|
|
}
|
|
if tc.vsClass != nil {
|
|
objects = append(objects, tc.vsClass)
|
|
}
|
|
if tc.resourcePolicy != nil {
|
|
objects = append(objects, tc.resourcePolicy)
|
|
}
|
|
|
|
var crClient crclient.Client
|
|
if tc.failVSCreate {
|
|
realFakeClient := velerotest.NewFakeControllerRuntimeClient(t, objects...)
|
|
crClient = &errorInjectingClient{Client: realFakeClient}
|
|
} else {
|
|
crClient = velerotest.NewFakeControllerRuntimeClient(t, objects...)
|
|
}
|
|
|
|
pvcBIA := pvcBackupItemAction{
|
|
log: logger,
|
|
crClient: crClient,
|
|
}
|
|
|
|
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.pvc)
|
|
require.NoError(t, err)
|
|
|
|
if tc.pvc != nil && !tc.failVSCreate && !tc.skipVSReadyUpdate {
|
|
go func() {
|
|
var vsList snapshotv1api.VolumeSnapshotList
|
|
err := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (bool, error) {
|
|
err = pvcBIA.crClient.List(ctx, &vsList, &crclient.ListOptions{Namespace: tc.pvc.Namespace})
|
|
|
|
require.NoError(t, err)
|
|
if err != nil || len(vsList.Items) == 0 {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
vscName := "testVSC"
|
|
readyToUse := true
|
|
vsList.Items[0].Status = &snapshotv1api.VolumeSnapshotStatus{
|
|
BoundVolumeSnapshotContentName: &vscName,
|
|
ReadyToUse: &readyToUse,
|
|
}
|
|
err = pvcBIA.crClient.Update(t.Context(), &vsList.Items[0])
|
|
require.NoError(t, err)
|
|
|
|
handleName := "testHandle"
|
|
vsc := builder.ForVolumeSnapshotContent("testVSC").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &handleName}).Result()
|
|
err = pvcBIA.crClient.Create(t.Context(), vsc)
|
|
require.NoError(t, err)
|
|
}()
|
|
}
|
|
|
|
resultUnstructed, _, _, _, err := pvcBIA.Execute(&unstructured.Unstructured{Object: pvcMap}, tc.backup)
|
|
|
|
if tc.expectedErr != nil {
|
|
require.EqualError(t, err, tc.expectedErr.Error())
|
|
} else if tc.expectErr {
|
|
require.Error(t, err)
|
|
// On timeout failure, check that the cleanup logic was called
|
|
if tc.skipVSReadyUpdate {
|
|
vsList := new(snapshotv1api.VolumeSnapshotList)
|
|
errList := crClient.List(t.Context(), vsList, &crclient.ListOptions{Namespace: tc.pvc.Namespace})
|
|
require.NoError(t, errList)
|
|
require.Empty(t, vsList.Items, "VolumeSnapshot should have been cleaned up after readiness check failed")
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if tc.expectedDataUpload != nil {
|
|
dataUploadList := new(velerov2alpha1.DataUploadList)
|
|
err := crClient.List(t.Context(), dataUploadList, &crclient.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{velerov1api.BackupNameLabel: tc.backup.Name})})
|
|
require.NoError(t, err)
|
|
require.Len(t, dataUploadList.Items, 1)
|
|
require.True(t, cmp.Equal(tc.expectedDataUpload, &dataUploadList.Items[0], cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion", "Name", "Spec.CSISnapshot.VolumeSnapshot")))
|
|
}
|
|
|
|
if tc.expectedPVC != nil {
|
|
resultPVC := new(corev1api.PersistentVolumeClaim)
|
|
runtime.DefaultUnstructuredConverter.FromUnstructured(resultUnstructed.UnstructuredContent(), resultPVC)
|
|
require.True(t, cmp.Equal(tc.expectedPVC, resultPVC, cmpopts.IgnoreFields(corev1api.PersistentVolumeClaim{}, "ResourceVersion", "Annotations", "Labels")))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProgress(t *testing.T) {
|
|
currentTime := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1api.Backup
|
|
dataUpload *velerov2alpha1.DataUpload
|
|
operationID string
|
|
expectedErr string
|
|
expectedProgress velero.OperationProgress
|
|
}{
|
|
{
|
|
name: "DataUpload cannot be found",
|
|
backup: builder.ForBackup("velero", "test").Result(),
|
|
operationID: "testing",
|
|
expectedErr: "not found DataUpload for operationID testing",
|
|
},
|
|
{
|
|
name: "DataUpload is found",
|
|
backup: builder.ForBackup("velero", "test").Result(),
|
|
dataUpload: &velerov2alpha1.DataUpload{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "DataUpload",
|
|
APIVersion: "v2alpha1",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "velero",
|
|
Name: "testing",
|
|
Labels: map[string]string{
|
|
velerov1api.AsyncOperationIDLabel: "testing",
|
|
},
|
|
},
|
|
Status: velerov2alpha1.DataUploadStatus{
|
|
Phase: velerov2alpha1.DataUploadPhaseFailed,
|
|
Progress: shared.DataMoveOperationProgress{
|
|
BytesDone: 1000,
|
|
TotalBytes: 1000,
|
|
},
|
|
StartTimestamp: &metav1.Time{Time: currentTime},
|
|
CompletionTimestamp: &metav1.Time{Time: currentTime},
|
|
Message: "Testing error",
|
|
},
|
|
},
|
|
operationID: "testing",
|
|
expectedProgress: velero.OperationProgress{
|
|
Completed: true,
|
|
Err: "Testing error",
|
|
NCompleted: 1000,
|
|
NTotal: 1000,
|
|
OperationUnits: "Bytes",
|
|
Description: "Failed",
|
|
Started: currentTime,
|
|
Updated: currentTime,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
crClient := velerotest.NewFakeControllerRuntimeClient(t)
|
|
logger := logrus.New()
|
|
|
|
pvcBIA := pvcBackupItemAction{
|
|
log: logger,
|
|
crClient: crClient,
|
|
}
|
|
|
|
if tc.dataUpload != nil {
|
|
err := crClient.Create(t.Context(), tc.dataUpload)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
progress, err := pvcBIA.Progress(tc.operationID, tc.backup)
|
|
if tc.expectedErr != "" {
|
|
require.Equal(t, tc.expectedErr, err.Error())
|
|
}
|
|
require.True(t, cmp.Equal(tc.expectedProgress, progress, cmpopts.IgnoreFields(velero.OperationProgress{}, "Started", "Updated")))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCancel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1api.Backup
|
|
dataUpload velerov2alpha1.DataUpload
|
|
operationID string
|
|
expectedErr error
|
|
expectedDataUpload velerov2alpha1.DataUpload
|
|
}{
|
|
{
|
|
name: "Cancel DataUpload",
|
|
backup: builder.ForBackup("velero", "test").Result(),
|
|
dataUpload: velerov2alpha1.DataUpload{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "DataUpload",
|
|
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "velero",
|
|
Name: "testing",
|
|
Labels: map[string]string{
|
|
velerov1api.AsyncOperationIDLabel: "testing",
|
|
},
|
|
},
|
|
},
|
|
operationID: "testing",
|
|
expectedDataUpload: velerov2alpha1.DataUpload{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "DataUpload",
|
|
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "velero",
|
|
Name: "testing",
|
|
Labels: map[string]string{
|
|
velerov1api.AsyncOperationIDLabel: "testing",
|
|
},
|
|
},
|
|
Spec: velerov2alpha1.DataUploadSpec{
|
|
Cancel: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
crClient := velerotest.NewFakeControllerRuntimeClient(t)
|
|
logger := logrus.New()
|
|
|
|
pvcBIA := pvcBackupItemAction{
|
|
log: logger,
|
|
crClient: crClient,
|
|
}
|
|
|
|
err := crClient.Create(t.Context(), &tc.dataUpload)
|
|
require.NoError(t, err)
|
|
|
|
err = pvcBIA.Cancel(tc.operationID, tc.backup)
|
|
require.NoError(t, err)
|
|
|
|
du := new(velerov2alpha1.DataUpload)
|
|
err = crClient.Get(t.Context(), crclient.ObjectKey{Namespace: tc.dataUpload.Namespace, Name: tc.dataUpload.Name}, du)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, cmp.Equal(tc.expectedDataUpload, *du, cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion")))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPVCAppliesTo(t *testing.T) {
|
|
p := pvcBackupItemAction{
|
|
log: logrus.StandardLogger(),
|
|
}
|
|
selector, err := p.AppliesTo()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(
|
|
t,
|
|
velero.ResourceSelector{
|
|
IncludedResources: []string{"persistentvolumeclaims"},
|
|
},
|
|
selector,
|
|
)
|
|
}
|
|
|
|
func TestNewPVCBackupItemAction(t *testing.T) {
|
|
logger := logrus.StandardLogger()
|
|
crClient := velerotest.NewFakeControllerRuntimeClient(t)
|
|
|
|
f := &factorymocks.Factory{}
|
|
f.On("KubebuilderClient").Return(nil, fmt.Errorf(""))
|
|
plugin := NewPvcBackupItemAction(f)
|
|
_, err := plugin(logger)
|
|
require.Error(t, err)
|
|
|
|
f1 := &factorymocks.Factory{}
|
|
f1.On("KubebuilderClient").Return(crClient, nil)
|
|
plugin1 := NewPvcBackupItemAction(f1)
|
|
_, err1 := plugin1(logger)
|
|
require.NoError(t, err1)
|
|
}
|
|
|
|
func TestListGroupedPVCs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
namespace string
|
|
labelKey string
|
|
groupValue string
|
|
pvcs []corev1api.PersistentVolumeClaim
|
|
expectCount int
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "Match single PVC with label",
|
|
namespace: "ns1",
|
|
labelKey: "vgs-key",
|
|
groupValue: "group-a",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc1",
|
|
Namespace: "ns1",
|
|
Labels: map[string]string{
|
|
"vgs-key": "group-a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectCount: 1,
|
|
},
|
|
{
|
|
name: "No matching PVCs",
|
|
namespace: "ns1",
|
|
labelKey: "vgs-key",
|
|
groupValue: "group-b",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc1",
|
|
Namespace: "ns1",
|
|
Labels: map[string]string{
|
|
"vgs-key": "group-a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectCount: 0,
|
|
},
|
|
{
|
|
name: "Match multiple PVCs",
|
|
namespace: "ns1",
|
|
labelKey: "vgs-key",
|
|
groupValue: "group-a",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc1",
|
|
Namespace: "ns1",
|
|
Labels: map[string]string{"vgs-key": "group-a"},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc2",
|
|
Namespace: "ns1",
|
|
Labels: map[string]string{"vgs-key": "group-a"},
|
|
},
|
|
},
|
|
},
|
|
expectCount: 2,
|
|
},
|
|
{
|
|
name: "Different namespace",
|
|
namespace: "ns2",
|
|
labelKey: "vgs-key",
|
|
groupValue: "group-a",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc1",
|
|
Namespace: "ns1",
|
|
Labels: map[string]string{"vgs-key": "group-a"},
|
|
},
|
|
},
|
|
},
|
|
expectCount: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var objs []runtime.Object
|
|
for i := range tt.pvcs {
|
|
objs = append(objs, &tt.pvcs[i])
|
|
}
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
|
|
action := &pvcBackupItemAction{
|
|
log: logrus.New(),
|
|
crClient: client,
|
|
}
|
|
|
|
result, err := action.listGroupedPVCs(t.Context(), tt.namespace, tt.labelKey, tt.groupValue)
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Len(t, result, tt.expectCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterPVCsByVolumePolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pvcs []corev1api.PersistentVolumeClaim
|
|
pvs []corev1api.PersistentVolume
|
|
volumePolicyStr string
|
|
expectCount int
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "All PVCs should be included when no volume policy",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-1",
|
|
StorageClassName: pointer.String("sc-1"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-2", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-2",
|
|
StorageClassName: pointer.String("sc-1"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver-1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-2"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver-1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectCount: 2,
|
|
},
|
|
{
|
|
name: "Filter out NFS PVC by volume policy",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-csi", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-csi",
|
|
StorageClassName: pointer.String("sc-1"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-nfs", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-nfs",
|
|
StorageClassName: pointer.String("sc-nfs"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-csi"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-nfs"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
NFS: &corev1api.NFSVolumeSource{
|
|
Server: "nfs-server",
|
|
Path: "/export",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
volumePolicyStr: `
|
|
version: v1
|
|
volumePolicies:
|
|
- conditions:
|
|
nfs: {}
|
|
action:
|
|
type: skip
|
|
`,
|
|
expectCount: 1,
|
|
},
|
|
{
|
|
name: "All PVCs filtered out by volume policy",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-nfs-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-nfs-1",
|
|
StorageClassName: pointer.String("sc-nfs"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-nfs-2", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-nfs-2",
|
|
StorageClassName: pointer.String("sc-nfs"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-nfs-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
NFS: &corev1api.NFSVolumeSource{
|
|
Server: "nfs-server",
|
|
Path: "/export/1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-nfs-2"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
NFS: &corev1api.NFSVolumeSource{
|
|
Server: "nfs-server",
|
|
Path: "/export/2",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
volumePolicyStr: `
|
|
version: v1
|
|
volumePolicies:
|
|
- conditions:
|
|
nfs: {}
|
|
action:
|
|
type: skip
|
|
`,
|
|
expectCount: 0,
|
|
},
|
|
{
|
|
name: "Filter out non-CSI PVCs from mixed driver group",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc-linstor",
|
|
Namespace: "ns-1",
|
|
Labels: map[string]string{"app.kubernetes.io/instance": "myapp"},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-linstor",
|
|
StorageClassName: pointer.String("sc-linstor"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pvc-nfs",
|
|
Namespace: "ns-1",
|
|
Labels: map[string]string{"app.kubernetes.io/instance": "myapp"},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "pv-nfs",
|
|
StorageClassName: pointer.String("sc-nfs"),
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-linstor"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "linstor.csi.linbit.com"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-nfs"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
NFS: &corev1api.NFSVolumeSource{
|
|
Server: "nfs-server",
|
|
Path: "/export",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
volumePolicyStr: `
|
|
version: v1
|
|
volumePolicies:
|
|
- conditions:
|
|
nfs: {}
|
|
action:
|
|
type: skip
|
|
`,
|
|
expectCount: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
objs := []runtime.Object{}
|
|
for i := range tt.pvs {
|
|
objs = append(objs, &tt.pvs[i])
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
|
|
backup := &velerov1api.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-backup",
|
|
Namespace: "velero",
|
|
},
|
|
Spec: velerov1api.BackupSpec{},
|
|
}
|
|
|
|
// Add volume policy ConfigMap if specified
|
|
if tt.volumePolicyStr != "" {
|
|
cm := &corev1api.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "volume-policy",
|
|
Namespace: "velero",
|
|
},
|
|
Data: map[string]string{
|
|
"volume-policy": tt.volumePolicyStr,
|
|
},
|
|
}
|
|
require.NoError(t, client.Create(context.Background(), cm))
|
|
|
|
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{
|
|
Kind: "ConfigMap",
|
|
Name: "volume-policy",
|
|
}
|
|
}
|
|
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
result, err := action.filterPVCsByVolumePolicy(tt.pvcs, backup)
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Len(t, result, tt.expectCount)
|
|
|
|
// For mixed driver scenarios, verify filtered result can determine single CSI driver
|
|
if tt.name == "Filter out non-CSI PVCs from mixed driver group" && len(result) > 0 {
|
|
driver, err := action.determineCSIDriver(result)
|
|
require.NoError(t, err, "After filtering, determineCSIDriver should not fail with multiple drivers error")
|
|
require.Equal(t, "linstor.csi.linbit.com", driver, "Should have the Linstor driver after filtering out NFS")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetermineCSIDriver(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pvcs []corev1api.PersistentVolumeClaim
|
|
pvs []corev1api.PersistentVolume
|
|
expectError bool
|
|
expectedDriver string
|
|
}{
|
|
{
|
|
name: "Single PVC with CSI PV",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-1"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedDriver: "csi-driver",
|
|
},
|
|
{
|
|
name: "Multiple PVCs with same CSI driver",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-1"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-2", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-2"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-2"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedDriver: "csi-driver",
|
|
},
|
|
{
|
|
name: "PV not CSI provisioned",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-1"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "Multiple PVCs with different CSI drivers",
|
|
pvcs: []corev1api.PersistentVolumeClaim{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-1"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-2", Namespace: "ns-1"},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: "pv-2"},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: corev1api.ClaimBound},
|
|
},
|
|
},
|
|
pvs: []corev1api.PersistentVolume{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver-1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-2"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{Driver: "csi-driver-2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var initObjs []runtime.Object
|
|
for i := range tt.pvcs {
|
|
pvc := tt.pvcs[i]
|
|
initObjs = append(initObjs, &pvc)
|
|
}
|
|
for i := range tt.pvs {
|
|
pv := tt.pvs[i]
|
|
initObjs = append(initObjs, &pv)
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, initObjs...)
|
|
action := &pvcBackupItemAction{
|
|
log: logrus.New(),
|
|
crClient: client,
|
|
}
|
|
|
|
driver, err := action.determineCSIDriver(tt.pvcs)
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectedDriver, driver)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetermineVGSClass(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1api.Backup
|
|
pvc *corev1api.PersistentVolumeClaim
|
|
existingVGSClass []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass
|
|
expectError bool
|
|
expectResult string
|
|
}{
|
|
{
|
|
name: "PVC annotation override",
|
|
pvc: &corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
velerov1api.VolumeGroupSnapshotClassAnnotationPVC: "pvc-class",
|
|
},
|
|
},
|
|
},
|
|
backup: &velerov1api.Backup{},
|
|
expectResult: "pvc-class",
|
|
},
|
|
{
|
|
name: "Backup annotation override",
|
|
pvc: &corev1api.PersistentVolumeClaim{},
|
|
backup: &velerov1api.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
fmt.Sprintf("%s%s", velerov1api.VolumeGroupSnapshotClassAnnotationBackupPrefix, testDriver): "backup-class",
|
|
},
|
|
},
|
|
},
|
|
expectResult: "backup-class",
|
|
},
|
|
{
|
|
name: "Default label-based match",
|
|
pvc: &corev1api.PersistentVolumeClaim{},
|
|
backup: &velerov1api.Backup{},
|
|
existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default-class",
|
|
Labels: map[string]string{velerov1api.VolumeGroupSnapshotClassDefaultLabel: "true"},
|
|
},
|
|
Driver: testDriver,
|
|
},
|
|
},
|
|
expectResult: "default-class",
|
|
},
|
|
{
|
|
name: "No matching VGS class",
|
|
pvc: &corev1api.PersistentVolumeClaim{},
|
|
backup: &velerov1api.Backup{},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "Multiple matching VGS classes",
|
|
pvc: &corev1api.PersistentVolumeClaim{},
|
|
backup: &velerov1api.Backup{},
|
|
existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "class1",
|
|
Labels: map[string]string{velerov1api.VolumeGroupSnapshotClassDefaultLabel: "true"},
|
|
},
|
|
Driver: testDriver,
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "class2",
|
|
Labels: map[string]string{velerov1api.VolumeGroupSnapshotClassDefaultLabel: "true"},
|
|
},
|
|
Driver: testDriver,
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var initObjs []runtime.Object
|
|
for _, vgsClass := range tt.existingVGSClass {
|
|
vgsClassCopy := vgsClass
|
|
initObjs = append(initObjs, &vgsClassCopy)
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, initObjs...)
|
|
logger := logrus.New()
|
|
require.NoError(t, volumegroupsnapshotv1beta1.AddToScheme(client.Scheme()))
|
|
|
|
action := &pvcBackupItemAction{crClient: client, log: logger}
|
|
|
|
result, err := action.determineVGSClass(t.Context(), testDriver, tt.backup, tt.pvc)
|
|
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectResult, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateVolumeGroupSnapshot(t *testing.T) {
|
|
testNamespace := "test-ns"
|
|
testLabelKey := "velero.io/test-vgs-label"
|
|
testLabelValue := "group-1"
|
|
testVGSClass := "test-class"
|
|
testBackup := &velerov1api.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-backup",
|
|
UID: "test-uid",
|
|
},
|
|
}
|
|
testPVC := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pvc",
|
|
Namespace: testNamespace,
|
|
Labels: map[string]string{
|
|
testLabelKey: testLabelValue,
|
|
},
|
|
},
|
|
}
|
|
|
|
crClient := velerotest.NewFakeControllerRuntimeClient(t)
|
|
log := logrus.New()
|
|
action := &pvcBackupItemAction{
|
|
log: log,
|
|
crClient: crClient,
|
|
}
|
|
|
|
vgs, err := action.createVolumeGroupSnapshot(t.Context(), testBackup, testPVC, testLabelKey, testLabelValue, testVGSClass)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vgs)
|
|
|
|
// Verify VGS fields
|
|
assert.Equal(t, testNamespace, vgs.Namespace)
|
|
assert.NotEmpty(t, vgs.GenerateName)
|
|
assert.Equal(t, testVGSClass, *vgs.Spec.VolumeGroupSnapshotClassName)
|
|
assert.NotNil(t, vgs.Spec.Source.Selector)
|
|
assert.Equal(t, testLabelValue, vgs.Spec.Source.Selector.MatchLabels[testLabelKey])
|
|
assert.Equal(t, testLabelValue, vgs.Labels[testLabelKey])
|
|
assert.Equal(t, label.GetValidName(testBackup.Name), vgs.Labels[velerov1api.BackupNameLabel])
|
|
assert.Equal(t, string(testBackup.UID), vgs.Labels[velerov1api.BackupUIDLabel])
|
|
|
|
// Check that it exists in fake client
|
|
retrieved := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
|
|
err = crClient.Get(t.Context(), crclient.ObjectKey{Name: vgs.Name, Namespace: vgs.Namespace}, retrieved)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWaitForVGSAssociatedVS(t *testing.T) {
|
|
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-vgs",
|
|
Namespace: "test-ns",
|
|
UID: types.UID("1234-5678-uuid"),
|
|
},
|
|
}
|
|
|
|
makeVS := func(name string, hasStatus bool, hasVGSName bool, owned bool, pvcName string) *snapshotv1api.VolumeSnapshot {
|
|
var refs []metav1.OwnerReference
|
|
if owned {
|
|
refs = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "groupsnapshot.storage.k8s.io/v1beta1",
|
|
Kind: "VolumeGroupSnapshot",
|
|
Name: vgs.Name,
|
|
UID: vgs.UID,
|
|
},
|
|
}
|
|
}
|
|
|
|
vs := &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: vgs.Namespace,
|
|
OwnerReferences: refs,
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{
|
|
PersistentVolumeClaimName: pointer.String(pvcName),
|
|
},
|
|
},
|
|
}
|
|
|
|
if hasStatus {
|
|
vs.Status = &snapshotv1api.VolumeSnapshotStatus{}
|
|
if hasVGSName {
|
|
vs.Status.VolumeGroupSnapshotName = pointer.String(vgs.Name)
|
|
}
|
|
}
|
|
|
|
return vs
|
|
}
|
|
|
|
makePVC := func(name string) corev1api.PersistentVolumeClaim {
|
|
return corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: vgs.Namespace,
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vsList []*snapshotv1api.VolumeSnapshot
|
|
groupedPVCs []corev1api.PersistentVolumeClaim
|
|
expectErr bool
|
|
expectVSMap int
|
|
}{
|
|
{
|
|
name: "all owned VS have VGS name",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs1", true, true, true, "pvc1"),
|
|
makeVS("vs2", true, true, true, "pvc2"),
|
|
},
|
|
groupedPVCs: []corev1api.PersistentVolumeClaim{
|
|
makePVC("pvc1"),
|
|
makePVC("pvc2"),
|
|
},
|
|
expectErr: false,
|
|
expectVSMap: 2,
|
|
},
|
|
{
|
|
name: "one owned VS missing VGS name",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs1", true, true, true, "pvc1"),
|
|
makeVS("vs2", true, false, true, "pvc2"),
|
|
},
|
|
groupedPVCs: []corev1api.PersistentVolumeClaim{
|
|
makePVC("pvc1"),
|
|
makePVC("pvc2"),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "owned VS has no status",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs1", false, false, true, "pvc1"),
|
|
},
|
|
groupedPVCs: []corev1api.PersistentVolumeClaim{
|
|
makePVC("pvc1"),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unrelated VS ignored",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs1", true, true, false, "pvc1"),
|
|
},
|
|
groupedPVCs: []corev1api.PersistentVolumeClaim{
|
|
makePVC("pvc1"),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "no owned VS present",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs1", true, true, false, "pvc1"),
|
|
},
|
|
groupedPVCs: []corev1api.PersistentVolumeClaim{
|
|
makePVC("pvc1"),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var objs []runtime.Object
|
|
objs = append(objs, vgs)
|
|
for _, vs := range tt.vsList {
|
|
objs = append(objs, vs)
|
|
}
|
|
for _, pvc := range tt.groupedPVCs {
|
|
objs = append(objs, &pvc)
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
vsMap, err := action.waitForVGSAssociatedVS(t.Context(), tt.groupedPVCs, vgs, 2*time.Second)
|
|
|
|
if tt.expectErr {
|
|
if err == nil {
|
|
t.Errorf("expected error but got nil")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if len(vsMap) != tt.expectVSMap {
|
|
t.Errorf("expected vsMap length %d, got %d", tt.expectVSMap, len(vsMap))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateVGSCreatedVS(t *testing.T) {
|
|
backup := &velerov1api.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "backup-1",
|
|
UID: "backup-uid-123",
|
|
},
|
|
}
|
|
|
|
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-vgs",
|
|
Namespace: "ns",
|
|
UID: "vgs-uid-123",
|
|
},
|
|
}
|
|
|
|
makeVS := func(name string, withVGSOwner bool, vgsNamePtr *string, pvcName string) *snapshotv1api.VolumeSnapshot {
|
|
var refs []metav1.OwnerReference
|
|
if withVGSOwner {
|
|
refs = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "groupsnapshot.storage.k8s.io/v1beta1",
|
|
Kind: "VolumeGroupSnapshot",
|
|
Name: vgs.Name,
|
|
UID: vgs.UID,
|
|
},
|
|
}
|
|
}
|
|
return &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: vgs.Namespace,
|
|
OwnerReferences: refs,
|
|
Finalizers: []string{
|
|
VolumeSnapshotFinalizerGroupProtection,
|
|
VolumeSnapshotFinalizerSourceProtection,
|
|
},
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{
|
|
ReadyToUse: pointer.Bool(true),
|
|
VolumeGroupSnapshotName: vgsNamePtr,
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{
|
|
PersistentVolumeClaimName: pointer.String(pvcName),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vs *snapshotv1api.VolumeSnapshot
|
|
expectOwnerCleared bool
|
|
expectFinalizersCleared bool
|
|
expectLabelPatched bool
|
|
}{
|
|
{
|
|
name: "should update owned VS",
|
|
vs: makeVS("vs-owned", true, pointer.String(vgs.Name), "pvc-1"),
|
|
expectOwnerCleared: true,
|
|
expectFinalizersCleared: true,
|
|
expectLabelPatched: true,
|
|
},
|
|
{
|
|
name: "should skip VS not owned by VGS",
|
|
vs: makeVS("vs-unowned", false, nil, "pvc-1"),
|
|
expectOwnerCleared: false,
|
|
expectFinalizersCleared: false,
|
|
expectLabelPatched: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, vgs, tt.vs)
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
// Build vsMap using the PVC name from the VS
|
|
vsMap := map[string]*snapshotv1api.VolumeSnapshot{
|
|
*tt.vs.Spec.Source.PersistentVolumeClaimName: tt.vs,
|
|
}
|
|
|
|
err := action.updateVGSCreatedVS(t.Context(), vsMap, vgs, backup)
|
|
require.NoError(t, err)
|
|
|
|
// Fetch updated VS
|
|
updated := &snapshotv1api.VolumeSnapshot{}
|
|
err = client.Get(t.Context(), crclient.ObjectKey{Name: tt.vs.Name, Namespace: tt.vs.Namespace}, updated)
|
|
require.NoError(t, err)
|
|
|
|
if tt.expectOwnerCleared {
|
|
assert.Empty(t, updated.OwnerReferences, "expected ownerReferences to be cleared")
|
|
} else {
|
|
assert.Equal(t, tt.vs.OwnerReferences, updated.OwnerReferences, "expected ownerReferences to remain unchanged")
|
|
}
|
|
|
|
if tt.expectFinalizersCleared {
|
|
assert.Empty(t, updated.Finalizers, "expected finalizers to be cleared")
|
|
} else {
|
|
assert.Equal(t, tt.vs.Finalizers, updated.Finalizers, "expected finalizers to remain unchanged")
|
|
}
|
|
|
|
if tt.expectLabelPatched {
|
|
assert.Equal(t, "backup-1", updated.Labels[velerov1api.BackupNameLabel])
|
|
assert.Equal(t, "backup-uid-123", updated.Labels[velerov1api.BackupUIDLabel])
|
|
} else {
|
|
assert.Nil(t, updated.Labels, "expected no labels to be patched")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchVGSCDeletionPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialPolicy snapshotv1api.DeletionPolicy
|
|
expectedPolicy snapshotv1api.DeletionPolicy
|
|
expectPatch bool
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "patches Delete to Retain",
|
|
initialPolicy: snapshotv1api.VolumeSnapshotContentDelete,
|
|
expectedPolicy: snapshotv1api.VolumeSnapshotContentRetain,
|
|
expectPatch: true,
|
|
},
|
|
{
|
|
name: "no patch if already Retain",
|
|
initialPolicy: snapshotv1api.VolumeSnapshotContentRetain,
|
|
expectedPolicy: snapshotv1api.VolumeSnapshotContentRetain,
|
|
expectPatch: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "test-vgsc"},
|
|
Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{
|
|
DeletionPolicy: tt.initialPolicy,
|
|
},
|
|
}
|
|
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-vgs",
|
|
Namespace: "ns",
|
|
},
|
|
Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
|
|
BoundVolumeGroupSnapshotContentName: pointer.String("test-vgsc"),
|
|
},
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, vgs, vgsc)
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
err := action.patchVGSCDeletionPolicy(t.Context(), vgs)
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
updated := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{}
|
|
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, updated)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectedPolicy, updated.Spec.DeletionPolicy)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteVGSAndVGSC(t *testing.T) {
|
|
makeVGS := func(name, namespace string, boundVGSCName *string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
|
|
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
|
|
BoundVolumeGroupSnapshotContentName: boundVGSCName,
|
|
},
|
|
}
|
|
}
|
|
|
|
makeVGSC := func(name string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent {
|
|
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot
|
|
existingVGSC *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent
|
|
expectVGSCDelete bool
|
|
expectVGSDelete bool
|
|
}{
|
|
{
|
|
name: "deletes both VGSC and VGS",
|
|
vgs: makeVGS("test-vgs", "ns", pointer.String("test-vgsc")),
|
|
existingVGSC: makeVGSC("test-vgsc"),
|
|
expectVGSCDelete: true,
|
|
expectVGSDelete: true,
|
|
},
|
|
{
|
|
name: "VGSC not found, still deletes VGS",
|
|
vgs: makeVGS("test-vgs", "ns", pointer.String("missing-vgsc")),
|
|
existingVGSC: nil,
|
|
expectVGSCDelete: false,
|
|
expectVGSDelete: true,
|
|
},
|
|
{
|
|
name: "no BoundVGSCName set, only deletes VGS",
|
|
vgs: makeVGS("test-vgs", "ns", nil),
|
|
existingVGSC: nil,
|
|
expectVGSCDelete: false,
|
|
expectVGSDelete: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var objs []runtime.Object
|
|
objs = append(objs, tt.vgs)
|
|
if tt.existingVGSC != nil {
|
|
objs = append(objs, tt.existingVGSC)
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
err := action.deleteVGSAndVGSC(t.Context(), tt.vgs)
|
|
require.NoError(t, err)
|
|
|
|
// Check VGSC is deleted
|
|
if tt.expectVGSCDelete {
|
|
got := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{}
|
|
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, got)
|
|
assert.True(t, apierrors.IsNotFound(err), "expected VGSC to be deleted")
|
|
}
|
|
|
|
// Check VGS is deleted
|
|
gotVGS := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
|
|
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgs", Namespace: "ns"}, gotVGS)
|
|
assert.True(t, apierrors.IsNotFound(err), "expected VGS to be deleted")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindExistingVSForBackup(t *testing.T) {
|
|
backupUID := types.UID("backup-uid-123")
|
|
backupName := "backup-1"
|
|
pvcName := "pvc-1"
|
|
namespace := "ns"
|
|
|
|
makeVS := func(name, pvc string, match bool) *snapshotv1api.VolumeSnapshot {
|
|
labels := map[string]string{}
|
|
if match {
|
|
labels[velerov1api.BackupNameLabel] = label.GetValidName(backupName)
|
|
labels[velerov1api.BackupUIDLabel] = string(backupUID)
|
|
}
|
|
return &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
Labels: labels,
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{
|
|
PersistentVolumeClaimName: pointer.String(pvc),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vsList []*snapshotv1api.VolumeSnapshot
|
|
expectName string
|
|
expectNil bool
|
|
}{
|
|
{
|
|
name: "should find matching VS",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs-match", pvcName, true),
|
|
},
|
|
expectName: "vs-match",
|
|
expectNil: false,
|
|
},
|
|
{
|
|
name: "should skip VS with non-matching labels",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs-nolabel", pvcName, false),
|
|
},
|
|
expectNil: true,
|
|
},
|
|
{
|
|
name: "should skip VS with different PVC name",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{
|
|
makeVS("vs-other-pvc", "other-pvc", true),
|
|
},
|
|
expectNil: true,
|
|
},
|
|
{
|
|
name: "should return nil if VS list is empty",
|
|
vsList: []*snapshotv1api.VolumeSnapshot{},
|
|
expectNil: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var objs []runtime.Object
|
|
for _, vs := range tt.vsList {
|
|
objs = append(objs, vs)
|
|
}
|
|
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
vs, err := action.findExistingVSForBackup(t.Context(), backupUID, backupName, pvcName, namespace)
|
|
require.NoError(t, err)
|
|
|
|
if tt.expectNil {
|
|
assert.Nil(t, vs)
|
|
} else {
|
|
require.NotNil(t, vs)
|
|
assert.Equal(t, tt.expectName, vs.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForVGSCBinding(t *testing.T) {
|
|
makeVGS := func(name string, withStatus bool) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
|
|
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "ns",
|
|
},
|
|
}
|
|
if withStatus {
|
|
contentName := "vgsc-123"
|
|
vgs.Status = &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
|
|
BoundVolumeGroupSnapshotContentName: &contentName,
|
|
}
|
|
}
|
|
return vgs
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "status is already bound",
|
|
vgs: makeVGS("vgs1", true),
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "status is nil",
|
|
vgs: makeVGS("vgs2", false),
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client := velerotest.NewFakeControllerRuntimeClient(t, tt.vgs.DeepCopy())
|
|
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
err := action.waitForVGSCBinding(t.Context(), tt.vgs, 1*time.Second)
|
|
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, tt.vgs.Status)
|
|
require.NotNil(t, tt.vgs.Status.BoundVolumeGroupSnapshotContentName)
|
|
require.Equal(t, "vgsc-123", *tt.vgs.Status.BoundVolumeGroupSnapshotContentName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetVGSByLabels(t *testing.T) {
|
|
labelKey := "velero.io/backup-name"
|
|
labelVal := "backup-123"
|
|
testLabels := map[string]string{labelKey: labelVal}
|
|
|
|
makeVGS := func(name string, labels map[string]string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
|
|
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "test-ns",
|
|
Labels: labels,
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
vgsObjects []runtime.Object
|
|
expectError string
|
|
expectName string
|
|
}{
|
|
{
|
|
name: "exactly one matching VGS",
|
|
vgsObjects: []runtime.Object{
|
|
makeVGS("vgs1", testLabels),
|
|
},
|
|
expectName: "vgs1",
|
|
},
|
|
{
|
|
name: "no matching VGS",
|
|
vgsObjects: []runtime.Object{},
|
|
expectError: "no VolumeGroupSnapshot found matching labels",
|
|
},
|
|
{
|
|
name: "multiple matching VGS",
|
|
vgsObjects: []runtime.Object{
|
|
makeVGS("vgs1", testLabels),
|
|
makeVGS("vgs2", testLabels),
|
|
},
|
|
expectError: "multiple VolumeGroupSnapshots found matching labels",
|
|
},
|
|
{
|
|
name: "client list error",
|
|
vgsObjects: []runtime.Object{},
|
|
expectError: "failed to list VolumeGroupSnapshots by labels",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var client crclient.Client
|
|
if tt.name == "client list error" {
|
|
// Inject a client that always errors on List
|
|
client = &failingClient{}
|
|
} else {
|
|
client = velerotest.NewFakeControllerRuntimeClient(t, tt.vgsObjects...)
|
|
}
|
|
|
|
action := &pvcBackupItemAction{
|
|
log: velerotest.NewLogger(),
|
|
crClient: client,
|
|
}
|
|
|
|
vgs, err := action.getVGSByLabels(t.Context(), "test-ns", testLabels)
|
|
|
|
if tt.expectError != "" {
|
|
if err == nil || !strings.Contains(err.Error(), tt.expectError) {
|
|
t.Errorf("expected error containing '%s', got: %v", tt.expectError, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if vgs == nil || vgs.Name != tt.expectName {
|
|
t.Errorf("expected VGS name %s, got %v", tt.expectName, vgs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// failingClient is a dummy client that fails on List
|
|
type failingClient struct {
|
|
crclient.Client
|
|
}
|
|
|
|
func (f *failingClient) List(ctx context.Context, list crclient.ObjectList, opts ...crclient.ListOption) error {
|
|
return fmt.Errorf("simulated list error")
|
|
}
|
|
|
|
func TestHasOwnerReference(t *testing.T) {
|
|
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-vgs",
|
|
Namespace: "test-ns",
|
|
UID: types.UID("1234-uid"),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ownerRef metav1.OwnerReference
|
|
expect bool
|
|
}{
|
|
{
|
|
name: "match kind, apiversion, uid",
|
|
ownerRef: metav1.OwnerReference{
|
|
Kind: kuberesource.VGSKind,
|
|
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
|
|
UID: vgs.UID,
|
|
},
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "mismatch kind",
|
|
ownerRef: metav1.OwnerReference{
|
|
Kind: "other-kind",
|
|
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
|
|
UID: vgs.UID,
|
|
},
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "mismatch apiversion",
|
|
ownerRef: metav1.OwnerReference{
|
|
Kind: kuberesource.VGSKind,
|
|
APIVersion: "wrong.group/v1",
|
|
UID: vgs.UID,
|
|
},
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "mismatch uid",
|
|
ownerRef: metav1.OwnerReference{
|
|
Kind: kuberesource.VGSKind,
|
|
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
|
|
UID: "wrong-uid",
|
|
},
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "no owner references",
|
|
ownerRef: metav1.OwnerReference{},
|
|
expect: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
obj := &metav1.ObjectMeta{
|
|
Name: "dummy",
|
|
Namespace: "test-ns",
|
|
}
|
|
|
|
if tt.name != "no owner references" {
|
|
obj.OwnerReferences = []metav1.OwnerReference{tt.ownerRef}
|
|
}
|
|
|
|
found := hasOwnerReference(obj, vgs)
|
|
assert.Equal(t, tt.expect, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPVCRequestSize(t *testing.T) {
|
|
logger := logrus.New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
pvcInitial *corev1api.PersistentVolumeClaim // Use full PVC to allow for nil Requests
|
|
restoreSize string
|
|
expectedSize string
|
|
}{
|
|
{
|
|
name: "UpdateRequired: PVC request is lower than restore size",
|
|
pvcInitial: func() *corev1api.PersistentVolumeClaim {
|
|
pvc := builder.ForPersistentVolumeClaim("velero", "testPVC").Result()
|
|
pvc.Spec.Resources.Requests = corev1api.ResourceList{
|
|
corev1api.ResourceStorage: resource.MustParse("1Gi"),
|
|
}
|
|
return pvc
|
|
}(),
|
|
restoreSize: "2Gi",
|
|
expectedSize: "2Gi",
|
|
},
|
|
{
|
|
name: "NoUpdateRequired: PVC request is larger than restore size",
|
|
pvcInitial: func() *corev1api.PersistentVolumeClaim {
|
|
pvc := builder.ForPersistentVolumeClaim("velero", "testPVC").Result()
|
|
pvc.Spec.Resources.Requests = corev1api.ResourceList{
|
|
corev1api.ResourceStorage: resource.MustParse("3Gi"),
|
|
}
|
|
return pvc
|
|
}(),
|
|
restoreSize: "2Gi",
|
|
expectedSize: "3Gi",
|
|
},
|
|
{
|
|
name: "PVC has no initial storage request",
|
|
pvcInitial: func() *corev1api.PersistentVolumeClaim {
|
|
pvc := builder.ForPersistentVolumeClaim("velero", "testPVC").Result()
|
|
pvc.Spec.Resources.Requests = corev1api.ResourceList{} // Empty request list
|
|
return pvc
|
|
}(),
|
|
restoreSize: "2Gi",
|
|
expectedSize: "2Gi",
|
|
},
|
|
{
|
|
name: "PVC has no initial Resources.Requests map",
|
|
pvcInitial: func() *corev1api.PersistentVolumeClaim {
|
|
pvc := builder.ForPersistentVolumeClaim("velero", "testPVC").Result()
|
|
pvc.Spec.Resources.Requests = nil // This will trigger the line to be covered
|
|
return pvc
|
|
}(),
|
|
restoreSize: "2Gi",
|
|
expectedSize: "2Gi",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create a VolumeSnapshotContent with restore size
|
|
rsQty := resource.MustParse(tc.restoreSize)
|
|
|
|
vsc := &snapshotv1api.VolumeSnapshotContent{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "testVSC",
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotContentStatus{
|
|
RestoreSize: pointer.Int64(rsQty.Value()),
|
|
},
|
|
}
|
|
|
|
// Call the function under test
|
|
pvc := tc.pvcInitial
|
|
setPVCRequestSizeToVSRestoreSize(pvc, vsc, logger)
|
|
|
|
// Verify that the PVC storage request is updated as expected.
|
|
updatedSize := pvc.Spec.Resources.Requests[corev1api.ResourceStorage]
|
|
expected := resource.MustParse(tc.expectedSize)
|
|
// Corrected line below:
|
|
require.Equal(t, 0, expected.Cmp(updatedSize), "Expected size %s, but got %s", expected.String(), updatedSize.String())
|
|
})
|
|
}
|
|
}
|