mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-19 14:34:17 +00:00
* Update CRDs and CLI to support in-place restore (#10038) Update CRDs(Restore, DataDownload, PodVolumeRestore) and restore create CLI to support in-place restore Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> * Update Kopia(filesystem) uploader to support incremental and deleteExtraFile during restore (#10066) Update Kopia(filesystem) uploader to support incremental and deleteExtraFile during restore Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> * Update Restore Exposer and PVC CSI to support in-place restore (#10104) 1. Update Restore Exposer to support exposing with existing PV for in-place restore 2. Update PVC CSI RIA to continue the restore process for in-place restore Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> * Update Block uploader to support increase restore (#10244) Update Block uploader to support increase restore Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> * Update Exposer to recreate the target PV if the volume mode is different with the restore PVC (#10257) Update Exposer to recreate the target PV if the volume mode is different with t he restore PVC Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> * Preserve PVC selected-node annotation via carrier annotation for in-place restore For in-place volume data restore, the existing PVC is deleted and recreated. For StorageClasses with the WaitForFirstConsumer volume binding mode, losing the volume.kubernetes.io/selected-node annotation could let the scheduler place the recreated workload Pod in a different zone than the original PV, leaving it stuck in ContainerCreating. Instead of relying on RestoreItemAction execution order (the generic PVC RIA unconditionally strips the selected-node annotation), the PVC CSI RIA now captures the annotation from the existing PVC right before deleting it and carries it on the target PVC via the Velero-internal restore.velero.io/inplace-restore-selected-node annotation. The restore engine translates the carrier back to the Kubernetes annotation after all RestoreItemActions have run and always strips the carrier so it never lands on the cluster. This makes the behavior independent of RIA ordering: the Kubernetes annotation is stripped by default on every path (including when the target PVC does not exist and Velero falls back to provisioning a new PVC), and preservation only happens when the CSI RIA explicitly captured a value from the existing PVC. Signed-off-by: chlins <chlins.zhang@gmail.com> * Update the control path to make the in-place incremental restore with block data mover work E2E (#10410) Update the control path to make the in-place incremental restore with block data mover work E2E Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> --------- Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com> Signed-off-by: chlins <chlins.zhang@gmail.com> Co-authored-by: chlins <chlins.zhang@gmail.com>
2540 lines
73 KiB
Go
2540 lines
73 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 exposer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
|
snapshotFake "github.com/kubernetes-csi/external-snapshotter/client/v8/clientset/versioned/fake"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
appsv1api "k8s.io/api/apps/v1"
|
|
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/runtime"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
kubefake "k8s.io/client-go/kubernetes/fake"
|
|
clientTesting "k8s.io/client-go/testing"
|
|
"k8s.io/utils/ptr"
|
|
clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
|
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
|
velerotypes "github.com/vmware-tanzu/velero/pkg/types"
|
|
"github.com/vmware-tanzu/velero/pkg/util"
|
|
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
|
"github.com/vmware-tanzu/velero/pkg/util/csi"
|
|
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
|
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
|
)
|
|
|
|
type reactor struct {
|
|
verb string
|
|
resource string
|
|
reactorFunc clientTesting.ReactionFunc
|
|
}
|
|
|
|
func TestExpose(t *testing.T) {
|
|
vscName := "fake-vsc"
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
var restoreSize int64 = 123456
|
|
|
|
scObj := &storagev1api.StorageClass{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-sc",
|
|
},
|
|
}
|
|
|
|
snapshotClass := "fake-snapshot-class"
|
|
vsObject := &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-vs",
|
|
Namespace: "fake-ns",
|
|
Annotations: map[string]string{
|
|
"fake-key-1": "fake-value-1",
|
|
"fake-key-2": "fake-value-2",
|
|
},
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{
|
|
VolumeSnapshotContentName: &vscName,
|
|
},
|
|
VolumeSnapshotClassName: &snapshotClass,
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{
|
|
BoundVolumeSnapshotContentName: &vscName,
|
|
ReadyToUse: boolptr.True(),
|
|
RestoreSize: resource.NewQuantity(restoreSize, ""),
|
|
},
|
|
}
|
|
|
|
vsObjectWithoutRestoreSize := &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-vs",
|
|
Namespace: "fake-ns",
|
|
Annotations: map[string]string{
|
|
"fake-key-1": "fake-value-1",
|
|
"fake-key-2": "fake-value-2",
|
|
},
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{
|
|
VolumeSnapshotContentName: &vscName,
|
|
},
|
|
VolumeSnapshotClassName: &snapshotClass,
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{
|
|
BoundVolumeSnapshotContentName: &vscName,
|
|
ReadyToUse: boolptr.True(),
|
|
},
|
|
}
|
|
|
|
snapshotHandle := "fake-handle"
|
|
vscObj := &snapshotv1api.VolumeSnapshotContent{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: vscName,
|
|
Annotations: map[string]string{
|
|
"fake-key-3": "fake-value-3",
|
|
"fake-key-4": "fake-value-4",
|
|
},
|
|
},
|
|
Spec: snapshotv1api.VolumeSnapshotContentSpec{
|
|
DeletionPolicy: snapshotv1api.VolumeSnapshotContentDelete,
|
|
Driver: "fake-driver",
|
|
VolumeSnapshotClassName: &snapshotClass,
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotContentStatus{
|
|
RestoreSize: &restoreSize,
|
|
SnapshotHandle: &snapshotHandle,
|
|
},
|
|
}
|
|
|
|
vscObjWithLabels := vscObj
|
|
vscObjWithLabels.Labels = map[string]string{
|
|
"snapshot.storage.kubernetes.io/managed-by": "worker",
|
|
}
|
|
|
|
daemonSet := &appsv1api.DaemonSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "velero",
|
|
Name: "node-agent",
|
|
},
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "DaemonSet",
|
|
APIVersion: appsv1api.SchemeGroupVersion.String(),
|
|
},
|
|
Spec: appsv1api.DaemonSetSpec{
|
|
Template: corev1api.PodTemplateSpec{
|
|
Spec: corev1api.PodSpec{
|
|
Containers: []corev1api.Container{
|
|
{
|
|
Name: "node-agent",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
pvName := "pv-1"
|
|
volumeAttachement1 := &storagev1api.VolumeAttachment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "va1",
|
|
},
|
|
Spec: storagev1api.VolumeAttachmentSpec{
|
|
Source: storagev1api.VolumeAttachmentSource{
|
|
PersistentVolumeName: &pvName,
|
|
},
|
|
NodeName: "node-1",
|
|
},
|
|
}
|
|
|
|
volumeAttachement2 := &storagev1api.VolumeAttachment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "va2",
|
|
},
|
|
Spec: storagev1api.VolumeAttachmentSpec{
|
|
Source: storagev1api.VolumeAttachmentSource{
|
|
PersistentVolumeName: &pvName,
|
|
},
|
|
NodeName: "node-2",
|
|
},
|
|
}
|
|
|
|
sourcePV := &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-pv",
|
|
},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{
|
|
VolumeHandle: "csi-volume-handle",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
snapshotClientObj []runtime.Object
|
|
kubeClientObj []runtime.Object
|
|
ownerBackup *velerov1.Backup
|
|
exposeParam CSISnapshotExposeParam
|
|
snapReactors []reactor
|
|
kubeReactors []reactor
|
|
err string
|
|
expectedVolumeSize *resource.Quantity
|
|
expectedReadOnlyPVC bool
|
|
expectedRWOPPVC bool
|
|
expectedBackupPVCStorageClass string
|
|
expectedAffinity *corev1api.Affinity
|
|
expectedPVCAnnotation map[string]string
|
|
}{
|
|
{
|
|
name: "get volume topology fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
err: "error getting volume topology for PV fake-pv, storage class fake-sc: error getting storage class fake-sc: storageclasses.storage.k8s.io \"fake-sc\" not found",
|
|
},
|
|
{
|
|
name: "wait vs ready fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error wait volume snapshot ready: error to get VolumeSnapshot /fake-vs: volumesnapshots.snapshot.storage.k8s.io \"fake-vs\" not found",
|
|
},
|
|
{
|
|
name: "get vsc fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to get volume snapshot content: error getting volume snapshot content from API: volumesnapshotcontents.snapshot.storage.k8s.io \"fake-vsc\" not found",
|
|
},
|
|
{
|
|
name: "delete vs fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
snapReactors: []reactor{
|
|
{
|
|
verb: "delete",
|
|
resource: "volumesnapshots",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-delete-error")
|
|
},
|
|
},
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to delete volume snapshot: error to delete volume snapshot: fake-delete-error",
|
|
},
|
|
{
|
|
name: "delete vsc fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
snapReactors: []reactor{
|
|
{
|
|
verb: "delete",
|
|
resource: "volumesnapshotcontents",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-delete-error")
|
|
},
|
|
},
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to delete volume snapshot content: error to delete volume snapshot content: fake-delete-error",
|
|
},
|
|
{
|
|
name: "create backup vs fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
snapReactors: []reactor{
|
|
{
|
|
verb: "create",
|
|
resource: "volumesnapshots",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-create-error")
|
|
},
|
|
},
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to create backup volume snapshot: fake-create-error",
|
|
},
|
|
{
|
|
name: "create backup vsc fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
snapReactors: []reactor{
|
|
{
|
|
verb: "create",
|
|
resource: "volumesnapshotcontents",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-create-error")
|
|
},
|
|
},
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to create backup volume snapshot content: fake-create-error",
|
|
},
|
|
{
|
|
name: "create backup pvc fail, invalid access mode",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: "fake-mode",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to create backup pvc: unsupported access mode fake-mode",
|
|
},
|
|
{
|
|
name: "create backup pvc fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
AccessMode: AccessModeFileSystem,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeReactors: []reactor{
|
|
{
|
|
verb: "create",
|
|
resource: "persistentvolumeclaims",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-create-error")
|
|
},
|
|
},
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
scObj,
|
|
},
|
|
err: "error to create backup pvc: error to create pvc: fake-create-error",
|
|
},
|
|
{
|
|
name: "create backup pod fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
kubeReactors: []reactor{
|
|
{
|
|
verb: "create",
|
|
resource: "pods",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-create-error")
|
|
},
|
|
},
|
|
},
|
|
err: "error to create backup pod: fake-create-error",
|
|
},
|
|
{
|
|
name: "success",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "success-with-labels",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObjWithLabels,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "restore size from exposeParam",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
VolumeSize: *resource.NewQuantity(567890, ""),
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObjectWithoutRestoreSize,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedVolumeSize: resource.NewQuantity(567890, ""),
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "backupPod mounts read only backupPVC",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
StorageClass: "fake-sc-read-only",
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedReadOnlyPVC: true,
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "backupPod mounts read only backupPVC and storageClass specified in backupPVC config",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
StorageClass: "fake-sc-read-only",
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedReadOnlyPVC: true,
|
|
expectedBackupPVCStorageClass: "fake-sc-read-only",
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "backupPVC uses ReadWriteOncePod access mode",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
ReadWriteOncePod: true,
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedRWOPPVC: true,
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "readOnly takes precedence over readWriteOncePod",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
ReadOnly: true,
|
|
ReadWriteOncePod: true,
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedReadOnlyPVC: true,
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "backupPod mounts backupPVC with storageClass specified in backupPVC config",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
StorageClass: "fake-sc-read-only",
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedBackupPVCStorageClass: "fake-sc-read-only",
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Affinity per StorageClass",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
Affinity: []*kube.LoadAffinity{
|
|
{
|
|
NodeSelector: metav1.LabelSelector{
|
|
MatchExpressions: []metav1.LabelSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: metav1.LabelSelectorOpIn,
|
|
Values: []string{"Linux"},
|
|
},
|
|
},
|
|
},
|
|
StorageClass: "fake-sc",
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpIn,
|
|
Values: []string{"Linux"},
|
|
},
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Affinity per StorageClass with expectedBackupPVCStorageClass",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
StorageClass: "fake-sc-read-only",
|
|
},
|
|
},
|
|
Affinity: []*kube.LoadAffinity{
|
|
{
|
|
NodeSelector: metav1.LabelSelector{
|
|
MatchExpressions: []metav1.LabelSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelArchStable,
|
|
Operator: metav1.LabelSelectorOpIn,
|
|
Values: []string{"amd64"},
|
|
},
|
|
},
|
|
},
|
|
StorageClass: "fake-sc-read-only",
|
|
},
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedBackupPVCStorageClass: "fake-sc-read-only",
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelArchStable,
|
|
Operator: corev1api.NodeSelectorOpIn,
|
|
Values: []string{"amd64"},
|
|
},
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Affinity in exposeParam is nil",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
StorageClass: "fake-sc-read-only",
|
|
},
|
|
},
|
|
Affinity: nil,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedBackupPVCStorageClass: "fake-sc-read-only",
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "IntolerateSourceNode, get source node fail",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
SourcePVName: pvName,
|
|
StorageClass: "fake-sc",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
Annotations: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
|
},
|
|
},
|
|
Affinity: nil,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
kubeReactors: []reactor{
|
|
{
|
|
verb: "list",
|
|
resource: "volumeattachments",
|
|
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
return true, nil, errors.New("fake-create-error")
|
|
},
|
|
},
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPVCAnnotation: nil,
|
|
},
|
|
{
|
|
name: "IntolerateSourceNode, get empty source node",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
SourcePVName: pvName,
|
|
StorageClass: "fake-sc",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
Annotations: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
|
},
|
|
},
|
|
Affinity: nil,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
|
},
|
|
{
|
|
name: "IntolerateSourceNode, get source nodes",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
SourcePVName: pvName,
|
|
StorageClass: "fake-sc",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
BackupPVCConfig: map[string]velerotypes.BackupPVC{
|
|
"fake-sc": {
|
|
Annotations: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
|
},
|
|
},
|
|
Affinity: nil,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
volumeAttachement1,
|
|
volumeAttachement2,
|
|
scObj,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
{
|
|
Key: corev1api.LabelHostname,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"node-1", "node-2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
|
},
|
|
{
|
|
name: "block data mover success",
|
|
ownerBackup: backup,
|
|
exposeParam: CSISnapshotExposeParam{
|
|
SnapshotName: "fake-vs",
|
|
SourceNamespace: "fake-ns",
|
|
AccessMode: AccessModeFileSystem,
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Millisecond,
|
|
StorageClass: "fake-sc",
|
|
SourcePVName: "fake-pv",
|
|
DataMover: datamover.DataMoverTypeVeleroBlock,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
vsObject,
|
|
vscObj,
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
daemonSet,
|
|
scObj,
|
|
sourcePV,
|
|
},
|
|
expectedAffinity: &corev1api.Affinity{
|
|
NodeAffinity: &corev1api.NodeAffinity{
|
|
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
|
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
|
{
|
|
Key: corev1api.LabelOSStable,
|
|
Operator: corev1api.NodeSelectorOpNotIn,
|
|
Values: []string{"windows"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(test.snapshotClientObj...)
|
|
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
|
|
|
for _, reactor := range test.snapReactors {
|
|
fakeSnapshotClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
|
}
|
|
|
|
for _, reactor := range test.kubeReactors {
|
|
fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
|
}
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
var ownerObject corev1api.ObjectReference
|
|
if test.ownerBackup != nil {
|
|
ownerObject = corev1api.ObjectReference{
|
|
Kind: test.ownerBackup.Kind,
|
|
Namespace: test.ownerBackup.Namespace,
|
|
Name: test.ownerBackup.Name,
|
|
UID: test.ownerBackup.UID,
|
|
APIVersion: test.ownerBackup.APIVersion,
|
|
}
|
|
}
|
|
|
|
err := exposer.Expose(t.Context(), ownerObject, &test.exposeParam)
|
|
if err == nil {
|
|
require.NoError(t, err)
|
|
|
|
backupPod, err := exposer.kubeClient.CoreV1().Pods(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
backupPVC, err := exposer.kubeClient.CoreV1().PersistentVolumeClaims(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
backupVS, err := exposer.csiSnapshotClient.VolumeSnapshots(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
backupVSC, err := exposer.csiSnapshotClient.VolumeSnapshotContents().Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, vsObject.Annotations, backupVS.Annotations)
|
|
assert.Equal(t, *vsObject.Spec.VolumeSnapshotClassName, *backupVS.Spec.VolumeSnapshotClassName)
|
|
assert.Equal(t, *backupVS.Spec.Source.VolumeSnapshotContentName, backupVSC.Name)
|
|
|
|
anno := make(map[string]string)
|
|
maps.Copy(anno, vscObj.Annotations)
|
|
anno[kube.KubeAnnAllowVolumeModeChange] = "true"
|
|
|
|
assert.Equal(t, anno, backupVSC.Annotations)
|
|
assert.Equal(t, vscObj.Labels, backupVSC.Labels)
|
|
assert.Equal(t, vscObj.Spec.DeletionPolicy, backupVSC.Spec.DeletionPolicy)
|
|
assert.Equal(t, vscObj.Spec.Driver, backupVSC.Spec.Driver)
|
|
assert.Equal(t, *vscObj.Spec.VolumeSnapshotClassName, *backupVSC.Spec.VolumeSnapshotClassName)
|
|
|
|
if test.expectedVolumeSize != nil {
|
|
assert.Equal(t, *test.expectedVolumeSize, backupPVC.Spec.Resources.Requests[corev1api.ResourceStorage])
|
|
} else {
|
|
assert.Equal(t, *resource.NewQuantity(restoreSize, ""), backupPVC.Spec.Resources.Requests[corev1api.ResourceStorage])
|
|
}
|
|
|
|
if test.expectedReadOnlyPVC {
|
|
gotReadOnlyAccessMode := false
|
|
for _, accessMode := range backupPVC.Spec.AccessModes {
|
|
if accessMode == corev1api.ReadOnlyMany {
|
|
gotReadOnlyAccessMode = true
|
|
}
|
|
}
|
|
assert.Equal(t, test.expectedReadOnlyPVC, gotReadOnlyAccessMode)
|
|
}
|
|
|
|
if test.expectedRWOPPVC {
|
|
assert.Equal(t, []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOncePod}, backupPVC.Spec.AccessModes)
|
|
} else {
|
|
assert.NotContains(t, backupPVC.Spec.AccessModes, corev1api.ReadWriteOncePod)
|
|
}
|
|
|
|
if test.expectedBackupPVCStorageClass != "" {
|
|
assert.Equal(t, test.expectedBackupPVCStorageClass, *backupPVC.Spec.StorageClassName)
|
|
}
|
|
|
|
if test.expectedAffinity != nil {
|
|
assert.Equal(t, test.expectedAffinity, backupPod.Spec.Affinity)
|
|
} else {
|
|
assert.Nil(t, backupPod.Spec.Affinity)
|
|
}
|
|
|
|
if test.expectedPVCAnnotation != nil {
|
|
assert.Equal(t, test.expectedPVCAnnotation, backupPVC.Annotations)
|
|
} else {
|
|
assert.Empty(t, backupPVC.Annotations)
|
|
}
|
|
} else {
|
|
assert.EqualError(t, err, test.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetExpose(t *testing.T) {
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
backupPod := &corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
},
|
|
Spec: corev1api.PodSpec{
|
|
Volumes: []corev1api.Volume{
|
|
{
|
|
Name: "fake-volume",
|
|
},
|
|
{
|
|
Name: "fake-volume-2",
|
|
},
|
|
{
|
|
Name: string(backup.UID),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupPodWithoutVolume := &corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
},
|
|
Spec: corev1api.PodSpec{
|
|
Volumes: []corev1api.Volume{
|
|
{
|
|
Name: "fake-volume-1",
|
|
},
|
|
{
|
|
Name: "fake-volume-2",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupPVC := &corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "fake-pv-name",
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{
|
|
Phase: corev1api.ClaimBound,
|
|
},
|
|
}
|
|
|
|
backupPV := &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-pv-name",
|
|
},
|
|
}
|
|
|
|
scheme := runtime.NewScheme()
|
|
corev1api.AddToScheme(scheme)
|
|
|
|
tests := []struct {
|
|
name string
|
|
kubeClientObj []runtime.Object
|
|
ownerBackup *velerov1.Backup
|
|
exposeWaitParam CSISnapshotExposeWaitParam
|
|
Timeout time.Duration
|
|
err string
|
|
expectedResult *ExposeResult
|
|
}{
|
|
{
|
|
name: "backup pod is not found",
|
|
ownerBackup: backup,
|
|
exposeWaitParam: CSISnapshotExposeWaitParam{
|
|
NodeName: "fake-node",
|
|
},
|
|
},
|
|
{
|
|
name: "wait pvc bound fail",
|
|
ownerBackup: backup,
|
|
exposeWaitParam: CSISnapshotExposeWaitParam{
|
|
NodeName: "fake-node",
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
backupPod,
|
|
},
|
|
Timeout: time.Second,
|
|
err: "error to wait backup PVC bound, fake-backup: error to wait for rediness of PVC: error to get pvc velero/fake-backup: persistentvolumeclaims \"fake-backup\" not found",
|
|
},
|
|
{
|
|
name: "backup volume not found in pod",
|
|
ownerBackup: backup,
|
|
exposeWaitParam: CSISnapshotExposeWaitParam{
|
|
NodeName: "fake-node",
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
backupPodWithoutVolume,
|
|
backupPVC,
|
|
backupPV,
|
|
},
|
|
Timeout: time.Second,
|
|
err: "backup pod fake-backup doesn't have the expected backup volume",
|
|
},
|
|
{
|
|
name: "succeed",
|
|
ownerBackup: backup,
|
|
exposeWaitParam: CSISnapshotExposeWaitParam{
|
|
NodeName: "fake-node",
|
|
},
|
|
kubeClientObj: []runtime.Object{
|
|
backupPod,
|
|
backupPVC,
|
|
backupPV,
|
|
},
|
|
Timeout: time.Second,
|
|
expectedResult: &ExposeResult{
|
|
ByPod: ExposeByPod{
|
|
HostingPod: backupPod,
|
|
VolumeName: string(backup.UID),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
|
|
|
fakeClientBuilder := clientFake.NewClientBuilder()
|
|
fakeClientBuilder = fakeClientBuilder.WithScheme(scheme)
|
|
|
|
fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
var ownerObject corev1api.ObjectReference
|
|
if test.ownerBackup != nil {
|
|
ownerObject = corev1api.ObjectReference{
|
|
Kind: test.ownerBackup.Kind,
|
|
Namespace: test.ownerBackup.Namespace,
|
|
Name: test.ownerBackup.Name,
|
|
UID: test.ownerBackup.UID,
|
|
APIVersion: test.ownerBackup.APIVersion,
|
|
}
|
|
}
|
|
|
|
test.exposeWaitParam.NodeClient = fakeClient
|
|
|
|
result, err := exposer.GetExposed(t.Context(), ownerObject, test.Timeout, &test.exposeWaitParam)
|
|
if test.err == "" {
|
|
require.NoError(t, err)
|
|
|
|
if test.expectedResult == nil {
|
|
assert.Nil(t, result)
|
|
} else {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.expectedResult.ByPod.VolumeName, result.ByPod.VolumeName)
|
|
assert.Equal(t, test.expectedResult.ByPod.HostingPod.Name, result.ByPod.HostingPod.Name)
|
|
}
|
|
} else {
|
|
assert.EqualError(t, err, test.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPeekExpose(t *testing.T) {
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
backupPodUrecoverable := &corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
},
|
|
Status: corev1api.PodStatus{
|
|
Phase: corev1api.PodFailed,
|
|
},
|
|
}
|
|
|
|
backupPod := &corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
},
|
|
}
|
|
|
|
scheme := runtime.NewScheme()
|
|
corev1api.AddToScheme(scheme)
|
|
|
|
tests := []struct {
|
|
name string
|
|
kubeClientObj []runtime.Object
|
|
ownerBackup *velerov1.Backup
|
|
err string
|
|
}{
|
|
{
|
|
name: "backup pod is not found",
|
|
ownerBackup: backup,
|
|
},
|
|
{
|
|
name: "pod is unrecoverable",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
backupPodUrecoverable,
|
|
},
|
|
err: "Pod is in abnormal state [Failed], message []",
|
|
},
|
|
{
|
|
name: "succeed",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
backupPod,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
var ownerObject corev1api.ObjectReference
|
|
if test.ownerBackup != nil {
|
|
ownerObject = corev1api.ObjectReference{
|
|
Kind: test.ownerBackup.Kind,
|
|
Namespace: test.ownerBackup.Namespace,
|
|
Name: test.ownerBackup.Name,
|
|
UID: test.ownerBackup.UID,
|
|
APIVersion: test.ownerBackup.APIVersion,
|
|
}
|
|
}
|
|
|
|
err := exposer.PeekExposed(t.Context(), ownerObject)
|
|
if test.err == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.EqualError(t, err, test.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
dataSource := &corev1api.TypedLocalObjectReference{
|
|
APIGroup: &snapshotv1api.SchemeGroupVersion.Group,
|
|
Kind: "VolumeSnapshot",
|
|
Name: "fake-snapshot",
|
|
}
|
|
volumeMode := corev1api.PersistentVolumeFilesystem
|
|
|
|
backupPVC := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
Annotations: map[string]string{},
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
AccessModes: []corev1api.PersistentVolumeAccessMode{
|
|
corev1api.ReadWriteOnce,
|
|
},
|
|
VolumeMode: &volumeMode,
|
|
DataSource: dataSource,
|
|
DataSourceRef: nil,
|
|
StorageClassName: ptr.To("fake-storage-class"),
|
|
Resources: corev1api.VolumeResourceRequirements{
|
|
Requests: corev1api.ResourceList{
|
|
corev1api.ResourceStorage: resource.MustParse("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupPVCReadOnly := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
Annotations: map[string]string{},
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
AccessModes: []corev1api.PersistentVolumeAccessMode{
|
|
corev1api.ReadOnlyMany,
|
|
},
|
|
VolumeMode: &volumeMode,
|
|
DataSource: dataSource,
|
|
DataSourceRef: nil,
|
|
StorageClassName: ptr.To("fake-storage-class"),
|
|
Resources: corev1api.VolumeResourceRequirements{
|
|
Requests: corev1api.ResourceList{
|
|
corev1api.ResourceStorage: resource.MustParse("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupPVCReadWriteOncePod := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
Annotations: map[string]string{},
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
AccessModes: []corev1api.PersistentVolumeAccessMode{
|
|
corev1api.ReadWriteOncePod,
|
|
},
|
|
VolumeMode: &volumeMode,
|
|
DataSource: dataSource,
|
|
DataSourceRef: nil,
|
|
StorageClassName: ptr.To("fake-storage-class"),
|
|
Resources: corev1api.VolumeResourceRequirements{
|
|
Requests: corev1api.ResourceList{
|
|
corev1api.ResourceStorage: resource.MustParse("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ownerBackup *velerov1.Backup
|
|
backupVS string
|
|
storageClass string
|
|
accessMode string
|
|
resource resource.Quantity
|
|
readOnly bool
|
|
readWriteOncePod bool
|
|
kubeClientObj []runtime.Object
|
|
snapshotClientObj []runtime.Object
|
|
want *corev1api.PersistentVolumeClaim
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "backupPVC gets created successfully with parameters from source PVC",
|
|
ownerBackup: backup,
|
|
backupVS: "fake-snapshot",
|
|
storageClass: "fake-storage-class",
|
|
accessMode: AccessModeFileSystem,
|
|
resource: resource.MustParse("1Gi"),
|
|
readOnly: false,
|
|
want: &backupPVC,
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "backupPVC gets created successfully with parameters from source PVC but accessMode from backupPVC Config as read only",
|
|
ownerBackup: backup,
|
|
backupVS: "fake-snapshot",
|
|
storageClass: "fake-storage-class",
|
|
accessMode: AccessModeFileSystem,
|
|
resource: resource.MustParse("1Gi"),
|
|
readOnly: true,
|
|
want: &backupPVCReadOnly,
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "backupPVC gets created with ReadWriteOncePod access mode when readWriteOncePod is set",
|
|
ownerBackup: backup,
|
|
backupVS: "fake-snapshot",
|
|
storageClass: "fake-storage-class",
|
|
accessMode: AccessModeFileSystem,
|
|
resource: resource.MustParse("1Gi"),
|
|
readOnly: false,
|
|
readWriteOncePod: true,
|
|
want: &backupPVCReadWriteOncePod,
|
|
wantErr: assert.NoError,
|
|
},
|
|
{
|
|
name: "readOnly takes precedence over readWriteOncePod",
|
|
ownerBackup: backup,
|
|
backupVS: "fake-snapshot",
|
|
storageClass: "fake-storage-class",
|
|
accessMode: AccessModeFileSystem,
|
|
resource: resource.MustParse("1Gi"),
|
|
readOnly: true,
|
|
readWriteOncePod: true,
|
|
want: &backupPVCReadOnly,
|
|
wantErr: assert.NoError,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fakeKubeClient := fake.NewSimpleClientset(tt.kubeClientObj...)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(tt.snapshotClientObj...)
|
|
e := &csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
var ownerObject corev1api.ObjectReference
|
|
if tt.ownerBackup != nil {
|
|
ownerObject = corev1api.ObjectReference{
|
|
Kind: tt.ownerBackup.Kind,
|
|
Namespace: tt.ownerBackup.Namespace,
|
|
Name: tt.ownerBackup.Name,
|
|
UID: tt.ownerBackup.UID,
|
|
APIVersion: tt.ownerBackup.APIVersion,
|
|
}
|
|
}
|
|
got, err := e.createBackupPVC(t.Context(), ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly, tt.readWriteOncePod, map[string]string{}, "")
|
|
if !tt.wantErr(t, err, fmt.Sprintf("createBackupPVC(%v, %v, %v, %v, %v, %v)", ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.want, got, "createBackupPVC(%v, %v, %v, %v, %v, %v)", ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_csiSnapshotExposer_DiagnoseExpose(t *testing.T) {
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
backupPodWithoutNodeName := corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-pod-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Status: corev1api.PodStatus{
|
|
Phase: corev1api.PodPending,
|
|
Conditions: []corev1api.PodCondition{
|
|
{
|
|
Type: corev1api.PodInitialized,
|
|
Status: corev1api.ConditionTrue,
|
|
Message: "fake-pod-message",
|
|
},
|
|
},
|
|
Message: "fake-pod-message-1",
|
|
},
|
|
}
|
|
|
|
backupPodWithNodeName := corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-pod-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Spec: corev1api.PodSpec{
|
|
NodeName: "fake-node",
|
|
},
|
|
Status: corev1api.PodStatus{
|
|
Phase: corev1api.PodPending,
|
|
Conditions: []corev1api.PodCondition{
|
|
{
|
|
Type: corev1api.PodInitialized,
|
|
Status: corev1api.ConditionTrue,
|
|
Message: "fake-pod-message",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupPVCWithoutVolumeName := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-pvc-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{
|
|
Phase: corev1api.ClaimPending,
|
|
},
|
|
}
|
|
|
|
backupPVCWithVolumeName := corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-pvc-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{
|
|
VolumeName: "fake-pv",
|
|
},
|
|
Status: corev1api.PersistentVolumeClaimStatus{
|
|
Phase: corev1api.ClaimPending,
|
|
},
|
|
}
|
|
|
|
backupPV := corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-pv",
|
|
},
|
|
Status: corev1api.PersistentVolumeStatus{
|
|
Phase: corev1api.VolumePending,
|
|
Message: "fake-pv-message",
|
|
},
|
|
}
|
|
|
|
readyToUse := false
|
|
vscMessage := "fake-vsc-message"
|
|
backupVSC := snapshotv1api.VolumeSnapshotContent{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "fake-vsc",
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotContentStatus{
|
|
ReadyToUse: &readyToUse,
|
|
Error: &snapshotv1api.VolumeSnapshotError{
|
|
Message: &vscMessage,
|
|
},
|
|
},
|
|
}
|
|
|
|
backupVSWithoutStatus := snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-vs-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backupVSWithoutVSC := snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-vs-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{},
|
|
}
|
|
|
|
vsMessage := "fake-vs-message"
|
|
backupVSWithVSC := snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-vs-uid",
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: backup.APIVersion,
|
|
Kind: backup.Kind,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
},
|
|
},
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{
|
|
BoundVolumeSnapshotContentName: &backupVSC.Name,
|
|
Error: &snapshotv1api.VolumeSnapshotError{
|
|
Message: &vsMessage,
|
|
},
|
|
},
|
|
}
|
|
|
|
nodeAgentPod := corev1api.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "node-agent-pod-1",
|
|
Labels: map[string]string{"role": "node-agent"},
|
|
},
|
|
Spec: corev1api.PodSpec{
|
|
NodeName: "fake-node",
|
|
},
|
|
Status: corev1api.PodStatus{
|
|
Phase: corev1api.PodRunning,
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ownerBackup *velerov1.Backup
|
|
kubeClientObj []runtime.Object
|
|
snapshotClientObj []runtime.Object
|
|
expected string
|
|
}{
|
|
{
|
|
name: "no pod, pvc, vs",
|
|
ownerBackup: backup,
|
|
expected: `begin diagnose CSI exposer
|
|
error getting backup pod fake-backup, err: pods "fake-backup" not found
|
|
error getting backup pvc fake-backup, err: persistentvolumeclaims "fake-backup" not found
|
|
error getting backup vs fake-backup, err: volumesnapshots.snapshot.storage.k8s.io "fake-backup" not found
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pod without node name, pvc without volume name, vs without status",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithoutNodeName,
|
|
&backupPVCWithoutVolumeName,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutStatus,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name , message fake-pod-message-1
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pod without node name, pvc without volume name, vs without VSC",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithoutNodeName,
|
|
&backupPVCWithoutVolumeName,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name , message fake-pod-message-1
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pod with node name, no node agent",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithoutVolumeName,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
node-agent is not running in node fake-node, err: daemonset pod not found in running state in node fake-node
|
|
PVC velero/fake-backup, phase Pending, binding to
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pod with node name, node agent is running",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithoutVolumeName,
|
|
&nodeAgentPod,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pvc with volume name, no pv",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithVolumeName,
|
|
&nodeAgentPod,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to fake-pv
|
|
error getting backup pv fake-pv, err: persistentvolumes "fake-pv" not found
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "pvc with volume name, pv exists",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithVolumeName,
|
|
&backupPV,
|
|
&nodeAgentPod,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithoutVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to fake-pv
|
|
PV fake-pv, phase Pending, reason , message fake-pv-message
|
|
VS velero/fake-backup, bind to , readyToUse false, errMessage
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "vs with vsc, vsc doesn't exist",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithVolumeName,
|
|
&backupPV,
|
|
&nodeAgentPod,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to fake-pv
|
|
PV fake-pv, phase Pending, reason , message fake-pv-message
|
|
VS velero/fake-backup, bind to fake-vsc, readyToUse false, errMessage fake-vs-message
|
|
error getting backup vsc fake-vsc, err: volumesnapshotcontents.snapshot.storage.k8s.io "fake-vsc" not found
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "vs with vsc, vsc exists",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithVolumeName,
|
|
&backupPV,
|
|
&nodeAgentPod,
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithVSC,
|
|
&backupVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
PVC velero/fake-backup, phase Pending, binding to fake-pv
|
|
PV fake-pv, phase Pending, reason , message fake-pv-message
|
|
VS velero/fake-backup, bind to fake-vsc, readyToUse false, errMessage fake-vs-message
|
|
VSC fake-vsc, readyToUse false, errMessage fake-vsc-message, handle
|
|
end diagnose CSI exposer`,
|
|
},
|
|
{
|
|
name: "with events",
|
|
ownerBackup: backup,
|
|
kubeClientObj: []runtime.Object{
|
|
&backupPodWithNodeName,
|
|
&backupPVCWithVolumeName,
|
|
&backupPV,
|
|
&nodeAgentPod,
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: velerov1.DefaultNamespace, Name: "event-1"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-uid-1"},
|
|
Reason: "reason-1",
|
|
Message: "message-1",
|
|
},
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: velerov1.DefaultNamespace, Name: "event-2"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-pod-uid"},
|
|
Reason: "reason-2",
|
|
Message: "message-2",
|
|
},
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: velerov1.DefaultNamespace, Name: "event-3"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-pvc-uid"},
|
|
Reason: "reason-3",
|
|
Message: "message-3",
|
|
},
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: velerov1.DefaultNamespace, Name: "event-4"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-vs-uid"},
|
|
Reason: "reason-4",
|
|
Message: "message-4",
|
|
},
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: "other-namespace", Name: "event-5"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-pod-uid"},
|
|
Reason: "reason-5",
|
|
Message: "message-5",
|
|
},
|
|
&corev1api.Event{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: velerov1.DefaultNamespace, Name: "event-6"},
|
|
Type: corev1api.EventTypeWarning,
|
|
InvolvedObject: corev1api.ObjectReference{UID: "fake-pod-uid"},
|
|
Reason: "reason-6",
|
|
Message: "message-6",
|
|
},
|
|
},
|
|
snapshotClientObj: []runtime.Object{
|
|
&backupVSWithVSC,
|
|
&backupVSC,
|
|
},
|
|
expected: `begin diagnose CSI exposer
|
|
Pod velero/fake-backup, phase Pending, node name fake-node, message
|
|
Pod condition Initialized, status True, reason , message fake-pod-message
|
|
Pod event reason reason-2, message message-2
|
|
Pod event reason reason-6, message message-6
|
|
PVC velero/fake-backup, phase Pending, binding to fake-pv
|
|
PVC event reason reason-3, message message-3
|
|
PV fake-pv, phase Pending, reason , message fake-pv-message
|
|
VS velero/fake-backup, bind to fake-vsc, readyToUse false, errMessage fake-vs-message
|
|
VS event reason reason-4, message message-4
|
|
VSC fake-vsc, readyToUse false, errMessage fake-vsc-message, handle
|
|
end diagnose CSI exposer`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fakeKubeClient := fake.NewSimpleClientset(tt.kubeClientObj...)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(tt.snapshotClientObj...)
|
|
e := &csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
var ownerObject corev1api.ObjectReference
|
|
if tt.ownerBackup != nil {
|
|
ownerObject = corev1api.ObjectReference{
|
|
Kind: tt.ownerBackup.Kind,
|
|
Namespace: tt.ownerBackup.Namespace,
|
|
Name: tt.ownerBackup.Name,
|
|
UID: tt.ownerBackup.UID,
|
|
APIVersion: tt.ownerBackup.APIVersion,
|
|
}
|
|
}
|
|
|
|
diag := e.DiagnoseExpose(t.Context(), ownerObject)
|
|
assert.Equal(t, tt.expected, diag)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCBTInfo(t *testing.T) {
|
|
handle := "snapshot-handle-1"
|
|
|
|
tests := []struct {
|
|
name string
|
|
vs *snapshotv1api.VolumeSnapshot
|
|
vsc *snapshotv1api.VolumeSnapshotContent
|
|
pv *corev1api.PersistentVolume
|
|
sourcePVName string
|
|
want csi.CBTInfo
|
|
wantErrSubstr string
|
|
}{
|
|
{
|
|
name: "return error when vs is nil",
|
|
vs: nil,
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
|
sourcePVName: "pv-1",
|
|
wantErrSubstr: "vs or vsc is nil",
|
|
},
|
|
{
|
|
name: "use annotations when change-id and snapshot annotation exist",
|
|
vs: &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "vs-anno",
|
|
Annotations: map[string]string{
|
|
util.VSphereCNSChangeIDAnno: "change-id-1",
|
|
util.VSphereCNSSnapshotAnno: "volume-id-1+snapshot-id-1",
|
|
},
|
|
},
|
|
},
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
|
sourcePVName: "pv-ignored",
|
|
want: csi.CBTInfo{
|
|
ChangeID: "change-id-1",
|
|
VolumeID: "volume-id-1",
|
|
SnapshotID: "vs-anno",
|
|
},
|
|
},
|
|
{
|
|
name: "fallback to pv and vsc snapshot handle",
|
|
vs: &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "vs-fallback"},
|
|
},
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{
|
|
Status: &snapshotv1api.VolumeSnapshotContentStatus{
|
|
SnapshotHandle: &handle,
|
|
},
|
|
},
|
|
pv: &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{
|
|
VolumeHandle: "csi-volume-handle-1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
sourcePVName: "pv-1",
|
|
want: csi.CBTInfo{
|
|
ChangeID: "snapshot-handle-1",
|
|
VolumeID: "csi-volume-handle-1",
|
|
SnapshotID: "vs-fallback",
|
|
},
|
|
},
|
|
{
|
|
name: "return error when pv not found in fallback path",
|
|
vs: &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "vs-no-pv"},
|
|
},
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
|
sourcePVName: "pv-not-found",
|
|
wantErrSubstr: "failed to get pv pv-not-found",
|
|
},
|
|
{
|
|
name: "return error when pv has no csi volume handle",
|
|
vs: &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "vs-no-volume-handle"},
|
|
},
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
|
pv: &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-no-handle"},
|
|
Spec: corev1api.PersistentVolumeSpec{},
|
|
},
|
|
sourcePVName: "pv-no-handle",
|
|
wantErrSubstr: "volumeID must not be empty for CBT",
|
|
},
|
|
{
|
|
name: "return error when snapshot annotation is invalid",
|
|
vs: &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "vs-no-volume-handle",
|
|
Annotations: map[string]string{
|
|
util.VSphereCNSChangeIDAnno: "change-id-1",
|
|
util.VSphereCNSSnapshotAnno: "volume-id-1:snapshot-id-1",
|
|
},
|
|
},
|
|
},
|
|
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
|
pv: &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
|
Spec: corev1api.PersistentVolumeSpec{
|
|
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{
|
|
VolumeHandle: "csi-volume-handle-1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
sourcePVName: "pv-1",
|
|
wantErrSubstr: "volumeID must not be empty for CBT",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var objs []runtime.Object
|
|
if tc.pv != nil {
|
|
objs = append(objs, tc.pv)
|
|
}
|
|
exposer := &csiSnapshotExposer{
|
|
kubeClient: kubefake.NewSimpleClientset(objs...),
|
|
log: logrus.StandardLogger(),
|
|
}
|
|
|
|
got, err := csi.GetCBTInfo(context.Background(), exposer.kubeClient, exposer.log, tc.vs, tc.vsc, tc.sourcePVName)
|
|
|
|
if tc.wantErrSubstr != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tc.wantErrSubstr)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErrSubstr) {
|
|
t.Fatalf("expected error containing %q, got %q", tc.wantErrSubstr, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.ChangeID != tc.want.ChangeID || got.VolumeID != tc.want.VolumeID || got.SnapshotID != tc.want.SnapshotID {
|
|
t.Fatalf("unexpected CBTInfo, want %+v, got %+v", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpose_SecretCopy(t *testing.T) {
|
|
backup := &velerov1.Backup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: velerov1.SchemeGroupVersion.String(),
|
|
Kind: "Backup",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: velerov1.DefaultNamespace,
|
|
Name: "fake-backup",
|
|
UID: "fake-uid",
|
|
},
|
|
}
|
|
|
|
ownerObject := corev1api.ObjectReference{
|
|
Kind: backup.Kind,
|
|
Namespace: backup.Namespace,
|
|
Name: backup.Name,
|
|
UID: backup.UID,
|
|
APIVersion: backup.APIVersion,
|
|
}
|
|
|
|
// The secret/configmap copy runs after GetVolumeTopology and WaitVolumeSnapshotReady,
|
|
// so a StorageClass and a ready VolumeSnapshot are needed to reach the copy block.
|
|
scObj := &storagev1api.StorageClass{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "encrypted-sc"},
|
|
}
|
|
readyVS := func() *snapshotv1api.VolumeSnapshot {
|
|
vscName := "fake-vsc"
|
|
return &snapshotv1api.VolumeSnapshot{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "fake-vs", Namespace: "app-ns"},
|
|
Spec: snapshotv1api.VolumeSnapshotSpec{
|
|
Source: snapshotv1api.VolumeSnapshotSource{VolumeSnapshotContentName: &vscName},
|
|
},
|
|
Status: &snapshotv1api.VolumeSnapshotStatus{
|
|
BoundVolumeSnapshotContentName: &vscName,
|
|
ReadyToUse: boolptr.True(),
|
|
RestoreSize: resource.NewQuantity(1234, ""),
|
|
},
|
|
}
|
|
}
|
|
|
|
param := func() *CSISnapshotExposeParam {
|
|
return &CSISnapshotExposeParam{
|
|
SourceNamespace: "app-ns",
|
|
SourcePVName: "fake-pv",
|
|
SnapshotName: "fake-vs",
|
|
StorageClass: "encrypted-sc",
|
|
OperationTimeout: time.Millisecond,
|
|
ExposeTimeout: time.Second,
|
|
}
|
|
}
|
|
|
|
t.Run("copies secret from source namespace", func(t *testing.T) {
|
|
srcSecret := &corev1api.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "kms-token", Namespace: "app-ns"},
|
|
Data: map[string][]byte{"token": []byte("vault-token")},
|
|
Type: corev1api.SecretTypeOpaque,
|
|
}
|
|
fakeKubeClient := fake.NewSimpleClientset(srcSecret, scObj)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(readyVS())
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
p := param()
|
|
p.BackupPVCConfig = map[string]velerotypes.BackupPVC{
|
|
"encrypted-sc": {SecretNames: []string{"kms-token"}},
|
|
}
|
|
|
|
// Expose will fail later (no VSC exists), but the secret copy should succeed
|
|
_ = exposer.Expose(t.Context(), ownerObject, p)
|
|
|
|
copied, err := fakeKubeClient.CoreV1().Secrets(ownerObject.Namespace).Get(
|
|
t.Context(), "kms-token", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("vault-token"), copied.Data["token"])
|
|
assert.Equal(t, string(ownerObject.UID), copied.Labels[BackupPVCSecretLabel])
|
|
})
|
|
|
|
t.Run("copies configmap from source namespace", func(t *testing.T) {
|
|
srcCM := &corev1api.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "kms-config", Namespace: "app-ns"},
|
|
Data: map[string]string{"vaultAddress": "https://vault.example.com"},
|
|
}
|
|
fakeKubeClient := fake.NewSimpleClientset(srcCM, scObj)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(readyVS())
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
p := param()
|
|
p.BackupPVCConfig = map[string]velerotypes.BackupPVC{
|
|
"encrypted-sc": {ConfigMapNames: []string{"kms-config"}},
|
|
}
|
|
|
|
_ = exposer.Expose(t.Context(), ownerObject, p)
|
|
|
|
copied, err := fakeKubeClient.CoreV1().ConfigMaps(ownerObject.Namespace).Get(
|
|
t.Context(), "kms-config", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "https://vault.example.com", copied.Data["vaultAddress"])
|
|
assert.Equal(t, string(ownerObject.UID), copied.Labels[BackupPVCSecretLabel])
|
|
})
|
|
|
|
t.Run("returns error when source secret missing", func(t *testing.T) {
|
|
fakeKubeClient := fake.NewSimpleClientset(scObj)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset(readyVS())
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
p := param()
|
|
p.BackupPVCConfig = map[string]velerotypes.BackupPVC{
|
|
"encrypted-sc": {SecretNames: []string{"missing-secret"}},
|
|
}
|
|
|
|
err := exposer.Expose(t.Context(), ownerObject, p)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "error copying secret")
|
|
})
|
|
}
|
|
|
|
func TestCleanUp_SecretsAndConfigMaps(t *testing.T) {
|
|
ownerObject := corev1api.ObjectReference{
|
|
Kind: "Backup",
|
|
Namespace: "velero",
|
|
Name: "du-123",
|
|
UID: "fake-uid",
|
|
APIVersion: "v1",
|
|
}
|
|
|
|
secret := &corev1api.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "kms-token", Namespace: "velero",
|
|
Labels: map[string]string{BackupPVCSecretLabel: string(ownerObject.UID)},
|
|
UID: "secret-uid",
|
|
},
|
|
}
|
|
cm := &corev1api.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "kms-config", Namespace: "velero",
|
|
Labels: map[string]string{BackupPVCSecretLabel: string(ownerObject.UID)},
|
|
UID: "cm-uid",
|
|
},
|
|
}
|
|
unrelatedSecret := &corev1api.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "other-secret", Namespace: "velero",
|
|
Labels: map[string]string{BackupPVCSecretLabel: "other-owner-uid"},
|
|
UID: "other-uid",
|
|
},
|
|
}
|
|
|
|
fakeKubeClient := fake.NewSimpleClientset(secret, cm, unrelatedSecret)
|
|
fakeSnapshotClient := snapshotFake.NewSimpleClientset()
|
|
|
|
exposer := csiSnapshotExposer{
|
|
kubeClient: fakeKubeClient,
|
|
csiSnapshotClient: fakeSnapshotClient.SnapshotV1(),
|
|
log: velerotest.NewLogger(),
|
|
}
|
|
|
|
exposer.CleanUp(t.Context(), ownerObject, "", "app-ns")
|
|
|
|
_, err := fakeKubeClient.CoreV1().Secrets("velero").Get(t.Context(), "kms-token", metav1.GetOptions{})
|
|
require.Error(t, err, "owned secret should be deleted")
|
|
|
|
_, err = fakeKubeClient.CoreV1().ConfigMaps("velero").Get(t.Context(), "kms-config", metav1.GetOptions{})
|
|
require.Error(t, err, "owned configmap should be deleted")
|
|
|
|
_, err = fakeKubeClient.CoreV1().Secrets("velero").Get(t.Context(), "other-secret", metav1.GetOptions{})
|
|
assert.NoError(t, err, "unrelated secret should not be deleted")
|
|
}
|