Merge CSI plugin code.

Signed-off-by: Xun Jiang <blackpigletbruce@gmail.com>
pull/7609/head
Xun Jiang 2024-03-29 18:18:31 +08:00
parent 63fe9f1f1f
commit 31e140919a
71 changed files with 6565 additions and 188 deletions

View File

@ -0,0 +1 @@
Merge CSI plugin code into Velero.

View File

@ -0,0 +1,120 @@
/*
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"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/client"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
// volumeSnapshotDeleteItemAction is a backup item action plugin for Velero.
type volumeSnapshotDeleteItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating that the
// VolumeSnapshotBackupItemAction should be invoked to backup
// VolumeSnapshots.
func (p *volumeSnapshotDeleteItemAction) AppliesTo() (velero.ResourceSelector, error) {
p.log.Debug("VolumeSnapshotBackupItemAction AppliesTo")
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"},
}, nil
}
func (p *volumeSnapshotDeleteItemAction) Execute(
input *velero.DeleteItemActionExecuteInput,
) error {
p.log.Info("Starting VolumeSnapshotDeleteItemAction for volumeSnapshot")
var vs snapshotv1api.VolumeSnapshot
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(),
&vs,
); err != nil {
return errors.Wrapf(err, "failed to convert input.Item from unstructured")
}
// We don't want this DeleteItemAction plugin to delete VolumeSnapshot
// taken outside of Velero. So skip deleting VolumeSnapshot objects
// that were not created in the process of creating the Velero
// backup being deleted.
if !kubeutil.HasBackupLabel(&vs.ObjectMeta, input.Backup.Name) {
p.log.Info(
"VolumeSnapshot %s/%s was not taken by backup %s, skipping deletion",
vs.Namespace, vs.Name, input.Backup.Name,
)
return nil
}
p.log.Infof("Deleting VolumeSnapshot %s/%s", vs.Namespace, vs.Name)
if vs.Status != nil && vs.Status.BoundVolumeSnapshotContentName != nil {
// we patch the DeletionPolicy of the VolumeSnapshotContent
// to set it to Delete. This ensures that the volume snapshot
// in the storage provider is also deleted.
err := csi.SetVolumeSnapshotContentDeletionPolicy(
*vs.Status.BoundVolumeSnapshotContentName,
p.crClient,
)
if err != nil && !apierrors.IsNotFound(err) {
return errors.Wrapf(
err,
fmt.Sprintf("failed to patch DeletionPolicy of volume snapshot %s/%s",
vs.Namespace, vs.Name),
)
}
if apierrors.IsNotFound(err) {
return nil
}
}
err := p.crClient.Delete(context.TODO(), &vs)
if err != nil && !apierrors.IsNotFound(err) {
return err
}
return nil
}
func NewVolumeSnapshotDeleteItemAction(f client.Factory) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, errors.WithStack(err)
}
return &volumeSnapshotDeleteItemAction{
log: logger,
crClient: crClient,
}, nil
}
}

View File

@ -0,0 +1,126 @@
/*
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"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/client"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
// volumeSnapshotContentDeleteItemAction is a restore item action plugin for Velero
type volumeSnapshotContentDeleteItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating
// VolumeSnapshotContentRestoreItemAction action should be invoked
// while restoring VolumeSnapshotContent.snapshot.storage.k8s.io resources
func (p *volumeSnapshotContentDeleteItemAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"},
}, nil
}
func (p *volumeSnapshotContentDeleteItemAction) Execute(
input *velero.DeleteItemActionExecuteInput,
) error {
p.log.Info("Starting VolumeSnapshotContentDeleteItemAction")
var snapCont snapshotv1api.VolumeSnapshotContent
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(),
&snapCont,
); err != nil {
return errors.Wrapf(err, "failed to convert VolumeSnapshotContent from unstructured")
}
// We don't want this DeleteItemAction plugin to delete
// VolumeSnapshotContent taken outside of Velero.
// So skip deleting VolumeSnapshotContent not have the backup name
// in its labels.
if !kubeutil.HasBackupLabel(&snapCont.ObjectMeta, input.Backup.Name) {
p.log.Info(
"VolumeSnapshotContent %s was not taken by backup %s, skipping deletion",
snapCont.Name,
input.Backup.Name,
)
return nil
}
p.log.Infof("Deleting VolumeSnapshotContent %s", snapCont.Name)
if err := csi.SetVolumeSnapshotContentDeletionPolicy(
snapCont.Name,
p.crClient,
); err != nil {
// #4764: Leave a warning when VolumeSnapshotContent cannot be found for deletion.
// Manual deleting VolumeSnapshotContent can cause this.
// It's tricky for Velero to handle this inconsistency.
// Even if Velero restores the VolumeSnapshotContent, CSI snapshot controller
// may not delete it correctly due to the snapshot represented by VolumeSnapshotContent
// already deleted on cloud provider.
if apierrors.IsNotFound(err) {
p.log.Warnf(
"VolumeSnapshotContent %s of backup %s cannot be found. May leave orphan snapshot %s on cloud provider.",
snapCont.Name, input.Backup.Name, *snapCont.Status.SnapshotHandle)
return nil
}
return errors.Wrapf(err, fmt.Sprintf(
"failed to set DeletionPolicy on volumesnapshotcontent %s. Skipping deletion",
snapCont.Name))
}
if err := p.crClient.Delete(
context.TODO(),
&snapCont,
); err != nil && !apierrors.IsNotFound(err) {
p.log.Infof("VolumeSnapshotContent %s not found", snapCont.Name)
return err
}
return nil
}
func NewVolumeSnapshotContentDeleteItemAction(
f client.Factory,
) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, err
}
return &volumeSnapshotContentDeleteItemAction{
log: logger,
crClient: crClient,
}, nil
}
}

View File

@ -97,6 +97,10 @@ const (
// VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes
// should be excluded from pod volume backup.
VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes"
// ExcludeFromBackupLabel is the label to exclude k8s resource from backup,
// even if the resource contains a matching selector label.
ExcludeFromBackupLabel = "velero.io/exclude-from-backup"
)
type AsyncOperationIDPrefix string
@ -111,3 +115,39 @@ type VeleroResourceUsage string
const (
VeleroResourceUsageDataUploadResult VeleroResourceUsage = "DataUpload"
)
// CSI related plugin actions' constant variable
const (
VolumeSnapshotLabel = "velero.io/volume-snapshot-name"
VolumeSnapshotHandleAnnotation = "velero.io/csi-volumesnapshot-handle"
VolumeSnapshotRestoreSize = "velero.io/csi-volumesnapshot-restore-size"
DriverNameAnnotation = "velero.io/csi-driver-name"
DeleteSecretNameAnnotation = "velero.io/csi-deletesnapshotsecret-name" // #nosec G101
DeleteSecretNamespaceAnnotation = "velero.io/csi-deletesnapshotsecret-namespace" // #nosec G101
VSCDeletionPolicyAnnotation = "velero.io/csi-vsc-deletion-policy"
VolumeSnapshotClassSelectorLabel = "velero.io/csi-volumesnapshot-class"
VolumeSnapshotClassDriverBackupAnnotationPrefix = "velero.io/csi-volumesnapshot-class"
VolumeSnapshotClassDriverPVCAnnotation = "velero.io/csi-volumesnapshot-class"
// There is no release w/ these constants exported. Using the strings for now.
// CSI Annotation volumesnapshotclass
// https://github.com/kubernetes-csi/external-snapshotter/blob/master/pkg/utils/util.go#L59-L60
PrefixedListSecretNameAnnotation = "csi.storage.k8s.io/snapshotter-list-secret-name" // #nosec G101
PrefixedListSecretNamespaceAnnotation = "csi.storage.k8s.io/snapshotter-list-secret-namespace" // #nosec G101
// CSI Annotation volumesnapshotcontents
PrefixedSecretNameAnnotation = "csi.storage.k8s.io/snapshotter-secret-name" // #nosec G101
PrefixedSecretNamespaceAnnotation = "csi.storage.k8s.io/snapshotter-secret-namespace" // #nosec G101
// Velero checks this annotation to determine whether to skip resource excluding check.
MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
// DynamicPVRestoreLabel is the label key for dynamic PV restore
DynamicPVRestoreLabel = "velero.io/dynamic-pv-restore"
// DataUploadNameAnnotation is the label key for the DataUpload name
DataUploadNameAnnotation = "velero.io/data-upload-name"
)

View File

@ -0,0 +1,578 @@
/*
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"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
storagev1api "k8s.io/api/storage/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/runtime/schema"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
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/client"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/label"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2"
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
"github.com/vmware-tanzu/velero/pkg/util/podvolume"
)
// pvcBackupItemAction is a backup item action plugin for Velero.
type pvcBackupItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating that the PVCBackupItemAction
// should be invoked to backup PVCs.
func (p *pvcBackupItemAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"persistentvolumeclaims"},
}, nil
}
func (p *pvcBackupItemAction) validateBackup(backup velerov1api.Backup) (valid bool) {
// Do nothing if volume snapshots have not been requested in this backup
if boolptr.IsSetToFalse(backup.Spec.SnapshotVolumes) {
p.log.Infof(
"Volume snapshotting not requested for backup %s/%s",
backup.Namespace, backup.Name)
return false
}
if backup.Status.Phase == velerov1api.BackupPhaseFinalizing ||
backup.Status.Phase == velerov1api.BackupPhaseFinalizingPartiallyFailed {
p.log.WithFields(
logrus.Fields{
"Backup": fmt.Sprintf("%s/%s", backup.Namespace, backup.Name),
"Phase": backup.Status.Phase,
},
).Debug("Backup is in finalizing phase. Skip this PVC.")
return false
}
return true
}
func (p *pvcBackupItemAction) validatePVCandPV(
pvc corev1api.PersistentVolumeClaim,
defaultVolumesToFsBackup *bool,
item runtime.Unstructured,
) (
valid bool,
updateItem runtime.Unstructured,
err error,
) {
updateItem = item
// no storage class: we don't know how to map to a VolumeSnapshotClass
if pvc.Spec.StorageClassName == nil {
return false,
updateItem,
errors.Errorf(
"Cannot snapshot PVC %s/%s, PVC has no storage class.",
pvc.Namespace, pvc.Name)
}
p.log.Debugf(
"Fetching underlying PV for PVC %s",
fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name),
)
// Do nothing if this is not a CSI provisioned volume
pv, err := kubeutil.GetPVForPVC(&pvc, p.crClient)
if err != nil {
return false, updateItem, errors.WithStack(err)
}
if pv.Spec.PersistentVolumeSource.CSI == nil {
p.log.Infof(
"Skipping PVC %s/%s, associated PV %s is not a CSI volume",
pvc.Namespace, pvc.Name, pv.Name)
kubeutil.AddAnnotations(
&pvc.ObjectMeta,
map[string]string{
velerov1api.SkippedNoCSIPVAnnotation: "true",
})
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pvc)
updateItem = &unstructured.Unstructured{Object: data}
return false, updateItem, err
}
// Do nothing if FS uploader is used to backup this PV
isFSUploaderUsed, err := podvolume.IsPVCDefaultToFSBackup(
pvc.Namespace,
pvc.Name,
p.crClient,
boolptr.IsSetToTrue(defaultVolumesToFsBackup),
)
if err != nil {
return false, updateItem, errors.WithStack(err)
}
if isFSUploaderUsed {
p.log.Infof(
"Skipping PVC %s/%s, PV %s will be backed up using FS uploader",
pvc.Namespace, pvc.Name, pv.Name)
return false, updateItem, nil
}
return true, updateItem, nil
}
func (p *pvcBackupItemAction) createVolumeSnapshot(
pvc corev1api.PersistentVolumeClaim,
backup *velerov1api.Backup,
) (
vs *snapshotv1api.VolumeSnapshot,
err error,
) {
p.log.Debugf("Fetching storage class for PV %s", *pvc.Spec.StorageClassName)
storageClass := new(storagev1api.StorageClass)
if err := p.crClient.Get(
context.TODO(), crclient.ObjectKey{Name: *pvc.Spec.StorageClassName},
storageClass,
); err != nil {
return nil, errors.Wrap(err, "error getting storage class")
}
p.log.Debugf("Fetching VolumeSnapshotClass for %s", storageClass.Provisioner)
vsClass, err := csi.GetVolumeSnapshotClass(
storageClass.Provisioner,
backup,
&pvc,
p.log,
p.crClient,
)
if err != nil {
return nil, errors.Wrapf(
err, "failed to get VolumeSnapshotClass for StorageClass %s",
storageClass.Name,
)
}
p.log.Infof("VolumeSnapshotClass=%s", vsClass.Name)
vsLabels := map[string]string{}
for k, v := range pvc.ObjectMeta.Labels {
vsLabels[k] = v
}
vsLabels[velerov1api.BackupNameLabel] = label.GetValidName(backup.Name)
// Craft the vs object to be created
vs = &snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "velero-" + pvc.Name + "-",
Namespace: pvc.Namespace,
Labels: vsLabels,
},
Spec: snapshotv1api.VolumeSnapshotSpec{
Source: snapshotv1api.VolumeSnapshotSource{
PersistentVolumeClaimName: &pvc.Name,
},
VolumeSnapshotClassName: &vsClass.Name,
},
}
if err := p.crClient.Create(context.TODO(), vs); err != nil {
return nil, errors.Wrapf(
err, "error creating volume snapshot",
)
}
p.log.Infof(
"Created VolumeSnapshot %s",
fmt.Sprintf("%s/%s", vs.Namespace, vs.Name),
)
return vs, nil
}
// Execute recognizes PVCs backed by volumes provisioned by CSI drivers
// with VolumeSnapshotting capability and creates snapshots of the
// underlying PVs by creating VolumeSnapshot CSI API objects that will
// trigger the CSI driver to perform the snapshot operation on the volume.
func (p *pvcBackupItemAction) Execute(
item runtime.Unstructured,
backup *velerov1api.Backup,
) (
runtime.Unstructured,
[]velero.ResourceIdentifier,
string,
[]velero.ResourceIdentifier,
error,
) {
p.log.Info("Starting PVCBackupItemAction")
if valid := p.validateBackup(*backup); !valid {
return item, nil, "", nil, nil
}
var pvc corev1api.PersistentVolumeClaim
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
item.UnstructuredContent(),
&pvc,
); err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
if valid, item, err := p.validatePVCandPV(
pvc,
backup.Spec.DefaultVolumesToFsBackup,
item,
); !valid {
if err != nil {
return nil, nil, "", nil, err
}
return item, nil, "", nil, nil
}
vs, err := p.createVolumeSnapshot(pvc, backup)
if err != nil {
return nil, nil, "", nil, err
}
labels := map[string]string{
velerov1api.VolumeSnapshotLabel: vs.Name,
velerov1api.BackupNameLabel: backup.Name,
}
annotations := map[string]string{
velerov1api.VolumeSnapshotLabel: vs.Name,
velerov1api.MustIncludeAdditionalItemAnnotation: "true",
}
var additionalItems []velero.ResourceIdentifier
operationID := ""
var itemToUpdate []velero.ResourceIdentifier
if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) {
operationID = label.GetValidName(
string(
velerov1api.AsyncOperationIDPrefixDataUpload,
) + string(backup.UID) + "." + string(pvc.UID),
)
dataUploadLog := p.log.WithFields(logrus.Fields{
"Source PVC": fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name),
"VolumeSnapshot": fmt.Sprintf("%s/%s", vs.Namespace, vs.Name),
"Operation ID": operationID,
"Backup": backup.Name,
})
// Wait until VS associated VSC snapshot handle created before
// returning with the Async operation for data mover.
_, err := csi.WaitUntilVSCHandleIsReady(
vs,
p.crClient,
p.log,
true,
backup.Spec.CSISnapshotTimeout.Duration,
)
if err != nil {
dataUploadLog.Errorf(
"Fail to wait VolumeSnapshot turned to ReadyToUse: %s",
err.Error(),
)
csi.CleanupVolumeSnapshot(vs, p.crClient, p.log)
return nil, nil, "", nil, errors.WithStack(err)
}
dataUploadLog.Info("Starting data upload of backup")
dataUpload, err := createDataUpload(
context.Background(),
backup,
p.crClient,
vs,
&pvc,
operationID,
)
if err != nil {
dataUploadLog.WithError(err).Error("failed to submit DataUpload")
// TODO: need to use DeleteVolumeSnapshotIfAny, after data mover
// adopting the controller-runtime client.
if deleteErr := p.crClient.Delete(context.TODO(), vs); deleteErr != nil {
if !apierrors.IsNotFound(deleteErr) {
dataUploadLog.WithError(deleteErr).Error("fail to delete VolumeSnapshot")
}
}
// Return without modification to not fail the backup,
// and the above error log makes the backup partially fail.
return item, nil, "", nil, nil
} else {
itemToUpdate = []velero.ResourceIdentifier{
{
GroupResource: schema.GroupResource{
Group: "velero.io",
Resource: "datauploads",
},
Namespace: dataUpload.Namespace,
Name: dataUpload.Name,
},
}
// Set the DataUploadNameLabel, which is used for restore to
// let CSI plugin check whether it should handle the volume.
// If volume is CSI migration, PVC doesn't have the annotation.
annotations[velerov1api.DataUploadNameAnnotation] = dataUpload.Namespace + "/" + dataUpload.Name
dataUploadLog.Info("DataUpload is submitted successfully.")
}
} else {
additionalItems = []velero.ResourceIdentifier{
{
GroupResource: kuberesource.VolumeSnapshots,
Namespace: vs.Namespace,
Name: vs.Name,
},
}
}
kubeutil.AddAnnotations(&pvc.ObjectMeta, annotations)
kubeutil.AddLabels(&pvc.ObjectMeta, labels)
p.log.Infof("Returning from PVCBackupItemAction with %d additionalItems to backup",
len(additionalItems))
for _, ai := range additionalItems {
p.log.Debugf("%s: %s", ai.GroupResource.String(), ai.Name)
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pvc)
if err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
return &unstructured.Unstructured{Object: pvcMap},
additionalItems, operationID, itemToUpdate, nil
}
func (p *pvcBackupItemAction) Name() string {
return "PVCBackupItemAction"
}
func (p *pvcBackupItemAction) Progress(
operationID string,
backup *velerov1api.Backup,
) (velero.OperationProgress, error) {
progress := velero.OperationProgress{}
if operationID == "" {
return progress, biav2.InvalidOperationIDError(operationID)
}
dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID)
if err != nil {
p.log.Errorf(
"fail to get DataUpload for backup %s/%s by operation ID %s: %s",
backup.Namespace, backup.Name, operationID, err.Error(),
)
return progress, err
}
if dataUpload.Status.Phase == velerov2alpha1.DataUploadPhaseNew ||
dataUpload.Status.Phase == "" {
p.log.Debugf("DataUpload is still not processed yet. Skip progress update.")
return progress, nil
}
progress.Description = string(dataUpload.Status.Phase)
progress.OperationUnits = "Bytes"
progress.NCompleted = dataUpload.Status.Progress.BytesDone
progress.NTotal = dataUpload.Status.Progress.TotalBytes
if dataUpload.Status.StartTimestamp != nil {
progress.Started = dataUpload.Status.StartTimestamp.Time
}
if dataUpload.Status.CompletionTimestamp != nil {
progress.Updated = dataUpload.Status.CompletionTimestamp.Time
}
if dataUpload.Status.Phase == velerov2alpha1.DataUploadPhaseCompleted {
progress.Completed = true
} else if dataUpload.Status.Phase == velerov2alpha1.DataUploadPhaseFailed {
progress.Completed = true
progress.Err = dataUpload.Status.Message
} else if dataUpload.Status.Phase == velerov2alpha1.DataUploadPhaseCanceled {
progress.Completed = true
progress.Err = "DataUpload is canceled"
}
return progress, nil
}
func (p *pvcBackupItemAction) Cancel(operationID string, backup *velerov1api.Backup) error {
if operationID == "" {
return biav2.InvalidOperationIDError(operationID)
}
dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID)
if err != nil {
p.log.Errorf(
"fail to get DataUpload for backup %s/%s: %s",
backup.Namespace, backup.Name, err.Error(),
)
return err
}
return cancelDataUpload(context.Background(), p.crClient, dataUpload)
}
func newDataUpload(
backup *velerov1api.Backup,
vs *snapshotv1api.VolumeSnapshot,
pvc *corev1api.PersistentVolumeClaim,
operationID string,
) *velerov2alpha1.DataUpload {
dataUpload := &velerov2alpha1.DataUpload{
TypeMeta: metav1.TypeMeta{
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
Kind: "DataUpload",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: backup.Namespace,
GenerateName: backup.Name + "-",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: velerov1api.SchemeGroupVersion.String(),
Kind: "Backup",
Name: backup.Name,
UID: backup.UID,
Controller: boolptr.True(),
},
},
Labels: map[string]string{
velerov1api.BackupNameLabel: label.GetValidName(backup.Name),
velerov1api.BackupUIDLabel: string(backup.UID),
velerov1api.PVCUIDLabel: string(pvc.UID),
velerov1api.AsyncOperationIDLabel: operationID,
},
},
Spec: velerov2alpha1.DataUploadSpec{
SnapshotType: velerov2alpha1.SnapshotTypeCSI,
CSISnapshot: &velerov2alpha1.CSISnapshotSpec{
VolumeSnapshot: vs.Name,
StorageClass: *pvc.Spec.StorageClassName,
SnapshotClass: *vs.Spec.VolumeSnapshotClassName,
},
SourcePVC: pvc.Name,
DataMover: backup.Spec.DataMover,
BackupStorageLocation: backup.Spec.StorageLocation,
SourceNamespace: pvc.Namespace,
OperationTimeout: backup.Spec.CSISnapshotTimeout,
},
}
if backup.Spec.UploaderConfig != nil &&
backup.Spec.UploaderConfig.ParallelFilesUpload > 0 {
dataUpload.Spec.DataMoverConfig = make(map[string]string)
dataUpload.Spec.DataMoverConfig[uploaderUtil.ParallelFilesUpload] = fmt.Sprintf(
"%d", backup.Spec.UploaderConfig.ParallelFilesUpload,
)
}
return dataUpload
}
func createDataUpload(
ctx context.Context,
backup *velerov1api.Backup,
crClient crclient.Client,
vs *snapshotv1api.VolumeSnapshot,
pvc *corev1api.PersistentVolumeClaim,
operationID string,
) (*velerov2alpha1.DataUpload, error) {
dataUpload := newDataUpload(backup, vs, pvc, operationID)
err := crClient.Create(ctx, dataUpload)
if err != nil {
return nil, errors.Wrap(err, "fail to create DataUpload CR")
}
return dataUpload, err
}
func getDataUpload(
ctx context.Context,
crClient crclient.Client,
operationID string,
) (*velerov2alpha1.DataUpload, error) {
dataUploadList := new(velerov2alpha1.DataUploadList)
err := crClient.List(ctx, dataUploadList, &crclient.ListOptions{
LabelSelector: labels.SelectorFromSet(
map[string]string{velerov1api.AsyncOperationIDLabel: operationID},
),
})
if err != nil {
return nil, errors.Wrapf(err, "error to list DataUpload")
}
if len(dataUploadList.Items) == 0 {
return nil, errors.Errorf("not found DataUpload for operationID %s", operationID)
}
if len(dataUploadList.Items) > 1 {
return nil, errors.Errorf("more than one DataUpload found operationID %s", operationID)
}
return &dataUploadList.Items[0], nil
}
func cancelDataUpload(
ctx context.Context,
crClient crclient.Client,
dataUpload *velerov2alpha1.DataUpload,
) error {
updatedDataUpload := dataUpload.DeepCopy()
updatedDataUpload.Spec.Cancel = true
err := crClient.Patch(ctx, updatedDataUpload, crclient.MergeFrom(dataUpload))
if err != nil {
return errors.Wrap(err, "error patch DataUpload")
}
return nil
}
func NewPvcBackupItemAction(f client.Factory) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, errors.WithStack(err)
}
return &pvcBackupItemAction{
log: logger,
crClient: crClient,
}, nil
}
}

View File

@ -0,0 +1,367 @@
/*
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"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
v1 "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
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"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
)
func TestExecute(t *testing.T) {
boolTrue := true
tests := []struct {
name string
backup *velerov1api.Backup
pvc *corev1.PersistentVolumeClaim
pv *corev1.PersistentVolume
sc *storagev1.StorageClass
vsClass *snapshotv1api.VolumeSnapshotClass
operationID string
expectedErr error
expectedBackup *velerov1api.Backup
expectedDataUpload *velerov2alpha1.DataUpload
expectedPVC *corev1.PersistentVolumeClaim
}{
{
name: "Skip PVC handling if SnapshotVolume set to false",
backup: builder.ForBackup("velero", "test").SnapshotVolumes(false).Result(),
expectedErr: nil,
},
{
name: "Skip PVC BIA when backup is in finalizing phase",
backup: builder.ForBackup("velero", "test").Phase(velerov1api.BackupPhaseFinalizing).Result(),
expectedErr: nil,
},
{
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(corev1.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: ".",
expectedErr: nil,
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(corev1.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: ".",
expectedErr: nil,
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").
ObjectMeta(builder.WithAnnotations(velerov1api.MustIncludeAdditionalItemAnnotation, "true", velerov1api.DataUploadNameAnnotation, "velero/", velerov1api.VolumeSnapshotLabel, ""),
builder.WithLabels(velerov1api.BackupNameLabel, "test", velerov1api.VolumeSnapshotLabel, "")).
VolumeName("testPV").StorageClass("testSC").Phase(corev1.ClaimBound).Result(),
},
}
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
logger := logrus.New()
logger.Level = logrus.DebugLevel
if tc.pvc != nil {
require.NoError(t, crClient.Create(context.Background(), tc.pvc))
}
if tc.pv != nil {
require.NoError(t, crClient.Create(context.Background(), tc.pv))
}
if tc.sc != nil {
require.NoError(t, crClient.Create(context.Background(), tc.sc))
}
if tc.vsClass != nil {
require.NoError(t, crClient.Create(context.Background(), tc.vsClass))
}
pvcBIA := pvcBackupItemAction{
log: logger,
crClient: crClient,
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.pvc)
require.NoError(t, err)
if boolptr.IsSetToTrue(tc.backup.Spec.SnapshotMoveData) == true {
go func() {
var vsList v1.VolumeSnapshotList
err := wait.PollUntilContextTimeout(context.Background(), 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 {
//lint:ignore nilerr reason
return false, nil // ignore
}
return true, nil
})
require.NoError(t, err)
vscName := "testVSC"
readyToUse := true
vsList.Items[0].Status = &v1.VolumeSnapshotStatus{
BoundVolumeSnapshotContentName: &vscName,
ReadyToUse: &readyToUse,
}
err = pvcBIA.crClient.Update(context.Background(), &vsList.Items[0])
require.NoError(t, err)
handleName := "testHandle"
vsc := builder.ForVolumeSnapshotContent("testVSC").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &handleName}).Result()
err = pvcBIA.crClient.Create(context.Background(), vsc)
require.NoError(t, err)
}()
}
resultUnstructed, _, _, _, err := pvcBIA.Execute(&unstructured.Unstructured{Object: pvcMap}, tc.backup)
if tc.expectedErr != nil {
require.Equal(t, err, tc.expectedErr)
}
if tc.expectedDataUpload != nil {
dataUploadList := new(velerov2alpha1.DataUploadList)
err := crClient.List(context.Background(), 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(corev1.PersistentVolumeClaim)
runtime.DefaultUnstructuredConverter.FromUnstructured(resultUnstructed.UnstructuredContent(), resultPVC)
require.True(t, cmp.Equal(tc.expectedPVC, resultPVC, cmpopts.IgnoreFields(corev1.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(*testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
logger := logrus.New()
pvcBIA := pvcBackupItemAction{
log: logger,
crClient: crClient,
}
if tc.dataUpload != nil {
err := crClient.Create(context.Background(), 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",
expectedErr: nil,
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(*testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
logger := logrus.New()
pvcBIA := pvcBackupItemAction{
log: logger,
crClient: crClient,
}
err := crClient.Create(context.Background(), &tc.dataUpload)
require.NoError(t, err)
err = pvcBIA.Cancel(tc.operationID, tc.backup)
if tc.expectedErr != nil {
require.Equal(t, err, tc.expectedErr)
}
du := new(velerov2alpha1.DataUpload)
err = crClient.Get(context.Background(), 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")))
})
}
}

View File

@ -0,0 +1,379 @@
/*
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"
"time"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/label"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
// volumeSnapshotBackupItemAction is a backup item action plugin to backup
// CSI VolumeSnapshot objects using Velero
type volumeSnapshotBackupItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating that the
// VolumeSnapshotBackupItemAction should be invoked to
// backup VolumeSnapshots.
func (p *volumeSnapshotBackupItemAction) AppliesTo() (
velero.ResourceSelector,
error,
) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"},
}, nil
}
// Execute backs up a CSI VolumeSnapshot object and captures, as labels and annotations,
// information from its associated VolumeSnapshotContents such as CSI driver name,
// storage snapshot handle and namespace and name of the snapshot delete secret, if any.
// It returns the VolumeSnapshotClass and the VolumeSnapshotContents as additional items
// to be backed up.
func (p *volumeSnapshotBackupItemAction) Execute(
item runtime.Unstructured,
backup *velerov1api.Backup,
) (
runtime.Unstructured,
[]velero.ResourceIdentifier,
string,
[]velero.ResourceIdentifier,
error,
) {
p.log.Infof("Executing VolumeSnapshotBackupItemAction")
vs := new(snapshotv1api.VolumeSnapshot)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
item.UnstructuredContent(), vs); err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
additionalItems := []velero.ResourceIdentifier{
{
GroupResource: kuberesource.VolumeSnapshotClasses,
Name: *vs.Spec.VolumeSnapshotClassName,
},
}
// determine if we are backing up a VolumeSnapshot that was created by velero while
// performing backup of a CSI backed PVC.
// For VolumeSnapshots that were created during the backup of a CSI backed PVC,
// we will wait for the VolumeSnapshotContents to be available.
// For VolumeSnapshots created outside of velero, we expect the VolumeSnapshotContent
// to be available prior to backing up the VolumeSnapshot. In case of a failure,
// backup should be re-attempted after the CSI driver has reconciled the VolumeSnapshot.
// existence of the velerov1api.BackupNameLabel indicates that the VolumeSnapshot was
// created while backing up a CSI backed PVC.
// We want to await reconciliation of only those VolumeSnapshots created during the
// ongoing backup. For this we will wait only if the backup label exists on the
// VolumeSnapshot object and the backup name is the same as that of the value of the
// backup label.
backupOngoing := vs.Labels[velerov1api.BackupNameLabel] == label.GetValidName(backup.Name)
p.log.Infof("Getting VolumesnapshotContent for Volumesnapshot %s/%s",
vs.Namespace, vs.Name)
vsc, err := csi.WaitUntilVSCHandleIsReady(
vs,
p.crClient,
p.log,
backupOngoing,
backup.Spec.CSISnapshotTimeout.Duration,
)
if err != nil {
csi.CleanupVolumeSnapshot(vs, p.crClient, p.log)
return nil, nil, "", nil, errors.WithStack(err)
}
if backup.Status.Phase == velerov1api.BackupPhaseFinalizing ||
backup.Status.Phase == velerov1api.BackupPhaseFinalizingPartiallyFailed {
p.log.
WithField("Backup", fmt.Sprintf("%s/%s", backup.Namespace, backup.Name)).
WithField("BackupPhase", backup.Status.Phase).Debugf("Clean VolumeSnapshots.")
csi.DeleteVolumeSnapshot(*vs, *vsc, backup, p.crClient, p.log)
return item, nil, "", nil, nil
}
annotations := make(map[string]string)
if vsc != nil {
// when we are backing up VolumeSnapshots created outside of velero, we
// will not await VolumeSnapshot reconciliation and in this case
// GetVolumeSnapshotContentForVolumeSnapshot may not find the associated
// VolumeSnapshotContents to add to the backup. This is not an error
// encountered in the backup process. So we add the VolumeSnapshotContent
// to the backup only if one is found.
additionalItems = append(additionalItems, velero.ResourceIdentifier{
GroupResource: kuberesource.VolumeSnapshotContents,
Name: vsc.Name,
})
annotations[velerov1api.VSCDeletionPolicyAnnotation] = string(vsc.Spec.DeletionPolicy)
if vsc.Status != nil {
if vsc.Status.SnapshotHandle != nil {
// Capture storage provider snapshot handle and CSI driver name
// to be used on restore to create a static VolumeSnapshotContent
// that will be the source of the VolumeSnapshot.
annotations[velerov1api.VolumeSnapshotHandleAnnotation] = *vsc.Status.SnapshotHandle
annotations[velerov1api.DriverNameAnnotation] = vsc.Spec.Driver
}
if vsc.Status.RestoreSize != nil {
annotations[velerov1api.VolumeSnapshotRestoreSize] = resource.NewQuantity(
*vsc.Status.RestoreSize, resource.BinarySI).String()
}
}
if backupOngoing {
p.log.Infof("Patching VolumeSnapshotContent %s with velero BackupNameLabel",
vsc.Name)
// If we created the VolumeSnapshotContent object during this ongoing backup,
// we would have created it with a DeletionPolicy of Retain.
// But, we want to retain these VolumeSnapshotContent ONLY for the lifetime
// of the backup. To that effect, during velero backup
// deletion, we will update the DeletionPolicy of the VolumeSnapshotContent
// and then delete the VolumeSnapshot object which will cascade delete the
// VolumeSnapshotContent and the associated snapshot in the storage
// provider (handled by the CSI driver and the CSI common controller).
// However, in the event that the VolumeSnapshot object is deleted outside
// of the backup deletion process, it is possible that the dynamically created
// VolumeSnapshotContent object will be left as an orphaned and non-discoverable
// resource in the cluster as well as in the storage provider. To avoid piling
// up of such orphaned resources, we will want to discover and delete the
// dynamically created VolumeSnapshotContent. We do that by adding
// the "velero.io/backup-name" label on the VolumeSnapshotContent.
// Further, we want to add this label only on VolumeSnapshotContents that
// were created during an ongoing velero backup.
originVSC := vsc.DeepCopy()
kubeutil.AddLabels(
&vsc.ObjectMeta,
map[string]string{
velerov1api.BackupNameLabel: label.GetValidName(backup.Name),
},
)
if vscPatchError := p.crClient.Patch(
context.TODO(),
vsc,
crclient.MergeFrom(originVSC),
); vscPatchError != nil {
p.log.Warnf("Failed to patch VolumeSnapshotContent %s: %v",
vsc.Name, vscPatchError)
}
}
}
// Before applying the BIA v2, the in-cluster VS state is not persisted into backup.
// After the change, because the final state of VS will be stored in backup as the
// result of async operation result, need to patch the annotations into VS to work,
// because restore will check the annotations information.
originVS := vs.DeepCopy()
kubeutil.AddAnnotations(&vs.ObjectMeta, annotations)
if err := p.crClient.Patch(
context.TODO(),
vs,
crclient.MergeFrom(originVS),
); err != nil {
p.log.Errorf("Fail to patch VolumeSnapshot: %s.", err.Error())
return nil, nil, "", nil, errors.WithStack(err)
}
annotations[velerov1api.MustIncludeAdditionalItemAnnotation] = "true"
// save newly applied annotations into the backed-up VolumeSnapshot item
kubeutil.AddAnnotations(&vs.ObjectMeta, annotations)
vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(vs)
if err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
p.log.Infof("Returning from VolumeSnapshotBackupItemAction",
"with %d additionalItems to backup", len(additionalItems))
for _, ai := range additionalItems {
p.log.Debugf("%s: %s", ai.GroupResource.String(), ai.Name)
}
operationID := ""
var itemToUpdate []velero.ResourceIdentifier
// Only return Async operation for VSC created for this backup.
if backupOngoing {
// The operationID is of the form <namespace>/<volumesnapshot-name>/<started-time>
operationID = vs.Namespace + "/" + vs.Name + "/" + time.Now().Format(time.RFC3339)
itemToUpdate = []velero.ResourceIdentifier{
{
GroupResource: kuberesource.VolumeSnapshots,
Namespace: vs.Namespace,
Name: vs.Name,
},
{
GroupResource: kuberesource.VolumeSnapshotContents,
Name: vsc.Name,
},
}
}
return &unstructured.Unstructured{Object: vsMap},
additionalItems, operationID, itemToUpdate, nil
}
// Name returns the plugin's name.
func (p *volumeSnapshotBackupItemAction) Name() string {
return "VolumeSnapshotBackupItemAction"
}
func (p *volumeSnapshotBackupItemAction) Progress(
operationID string,
backup *velerov1api.Backup,
) (velero.OperationProgress, error) {
progress := velero.OperationProgress{}
if operationID == "" {
return progress, biav2.InvalidOperationIDError(operationID)
}
// The operationID is of the form <namespace>/<volumesnapshot-name>/<started-time>
operationIDParts := strings.Split(operationID, "/")
if len(operationIDParts) != 3 {
p.log.Errorf("invalid operation ID %s", operationID)
return progress, biav2.InvalidOperationIDError(operationID)
}
var err error
if progress.Started, err = time.Parse(time.RFC3339, operationIDParts[2]); err != nil {
p.log.Errorf("error parsing operation ID's StartedTime",
"part into time %s: %s", operationID, err.Error())
return progress, errors.WithStack(err)
}
vs := new(snapshotv1api.VolumeSnapshot)
if err := p.crClient.Get(
context.Background(),
crclient.ObjectKey{
Namespace: operationIDParts[0],
Name: operationIDParts[1],
},
vs); err != nil {
p.log.Errorf("error getting volumesnapshot %s/%s: %s",
operationIDParts[0], operationIDParts[1], err.Error())
return progress, errors.WithStack(err)
}
if vs.Status == nil {
p.log.Debugf("VolumeSnapshot %s/%s has an empty status.",
"Skip progress update.", vs.Namespace, vs.Name)
return progress, nil
}
if boolptr.IsSetToTrue(vs.Status.ReadyToUse) {
p.log.Debugf("VolumeSnapshot %s/%s is ReadyToUse.",
"Continue on querying corresponding VolumeSnapshotContent.",
vs.Namespace, vs.Name)
} else if vs.Status.Error != nil {
errorMessage := ""
if vs.Status.Error.Message != nil {
errorMessage = *vs.Status.Error.Message
}
p.log.Warnf("VolumeSnapshot has a temporary error %s.",
"Snapshot controller will retry later.", errorMessage)
return progress, nil
}
if vs.Status != nil && vs.Status.BoundVolumeSnapshotContentName != nil {
vsc := new(snapshotv1api.VolumeSnapshotContent)
err := p.crClient.Get(
context.Background(),
crclient.ObjectKey{Name: *vs.Status.BoundVolumeSnapshotContentName},
vsc,
)
if err != nil {
p.log.Errorf("error getting VolumeSnapshotContent %s: %s",
*vs.Status.BoundVolumeSnapshotContentName, err.Error())
return progress, errors.WithStack(err)
}
if vsc.Status == nil {
p.log.Debugf("VolumeSnapshotContent %s has an empty Status.",
"Skip progress update.", vsc.Name)
return progress, nil
}
now := time.Now()
if boolptr.IsSetToTrue(vsc.Status.ReadyToUse) {
progress.Completed = true
progress.Updated = now
} else if vsc.Status.Error != nil {
progress.Completed = true
progress.Updated = now
if vsc.Status.Error.Message != nil {
progress.Err = *vsc.Status.Error.Message
}
p.log.Warnf("VolumeSnapshotContent meets an error %s.", progress.Err)
}
}
return progress, nil
}
// Cancel is not implemented for VolumeSnapshotBackupItemAction
func (p *volumeSnapshotBackupItemAction) Cancel(
operationID string,
backup *velerov1api.Backup,
) error {
// CSI Specification doesn't support canceling a snapshot creation.
return nil
}
// NewVolumeSnapshotBackupItemAction returns
// VolumeSnapshotBackupItemAction instance.
func NewVolumeSnapshotBackupItemAction(
f client.Factory,
) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, errors.WithStack(err)
}
return &volumeSnapshotBackupItemAction{
log: logger,
crClient: crClient,
}, nil
}
}

View File

@ -0,0 +1,124 @@
/*
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 (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
csiutil "github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
// volumeSnapshotClassBackupItemAction is a backup item action plugin to
// backup CSI VolumeSnapshotClass objects using Velero
type volumeSnapshotClassBackupItemAction struct {
log logrus.FieldLogger
}
// AppliesTo returns information indicating that the
// VolumeSnapshotClassBackupItemAction action should be invoked
// to backup VolumeSnapshotClass.
func (p *volumeSnapshotClassBackupItemAction) AppliesTo() (
velero.ResourceSelector,
error,
) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshotclass.snapshot.storage.k8s.io"},
}, nil
}
// Execute backs up a VolumeSnapshotClass object and returns as additional
// items any snapshot lister secret that may be referenced in its annotations.
func (p *volumeSnapshotClassBackupItemAction) Execute(
item runtime.Unstructured,
backup *velerov1api.Backup,
) (
runtime.Unstructured,
[]velero.ResourceIdentifier,
string,
[]velero.ResourceIdentifier,
error,
) {
p.log.Infof("Executing VolumeSnapshotClassBackupItemAction")
var snapClass snapshotv1api.VolumeSnapshotClass
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
item.UnstructuredContent(),
&snapClass,
); err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
additionalItems := []velero.ResourceIdentifier{}
if csiutil.IsVolumeSnapshotClassHasListerSecret(&snapClass) {
additionalItems = append(additionalItems, velero.ResourceIdentifier{
GroupResource: kuberesource.Secrets,
Name: snapClass.Annotations[velerov1api.PrefixedListSecretNameAnnotation],
Namespace: snapClass.Annotations[velerov1api.PrefixedListSecretNamespaceAnnotation],
})
kubeutil.AddAnnotations(&snapClass.ObjectMeta, map[string]string{
velerov1api.MustIncludeAdditionalItemAnnotation: "true",
})
}
snapClassMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&snapClass)
if err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
p.log.Infof(
"Returning from VolumeSnapshotClassBackupItemAction with %d additionalItems to backup",
len(additionalItems),
)
return &unstructured.Unstructured{Object: snapClassMap}, additionalItems, "", nil, nil
}
// Name returns the plugin's name.
func (p *volumeSnapshotClassBackupItemAction) Name() string {
return "VolumeSnapshotClassBackupItemAction"
}
// Progress is not implemented for VolumeSnapshotClassBackupItemAction
func (p *volumeSnapshotClassBackupItemAction) Progress(
operationID string,
backup *velerov1api.Backup,
) (velero.OperationProgress, error) {
return velero.OperationProgress{}, nil
}
// Cancel is not implemented for VolumeSnapshotClassBackupItemAction
func (p *volumeSnapshotClassBackupItemAction) Cancel(
operationID string,
backup *velerov1api.Backup,
) error {
return nil
}
// NewVolumeSnapshotClassBackupItemAction returns a
// VolumeSnapshotClassBackupItemAction instance.
func NewVolumeSnapshotClassBackupItemAction(logger logrus.FieldLogger) (interface{}, error) {
return &volumeSnapshotClassBackupItemAction{log: logger}, nil
}

View File

@ -0,0 +1,141 @@
/*
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 (
"fmt"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
csiutil "github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
// volumeSnapshotContentBackupItemAction is a backup item action plugin to backup
// CSI VolumeSnapshotContent objects using Velero
type volumeSnapshotContentBackupItemAction struct {
log logrus.FieldLogger
}
// AppliesTo returns information indicating that the
// VolumeSnapshotContentBackupItemAction action should be invoked to
// backup VolumeSnapshotContents.
func (p *volumeSnapshotContentBackupItemAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"},
}, nil
}
// Execute returns the unmodified VolumeSnapshotContent object along
// with the snapshot deletion secret, if any, from its annotation
// as additional items to backup.
func (p *volumeSnapshotContentBackupItemAction) Execute(
item runtime.Unstructured,
backup *velerov1api.Backup,
) (
runtime.Unstructured,
[]velero.ResourceIdentifier,
string,
[]velero.ResourceIdentifier,
error,
) {
p.log.Infof("Executing VolumeSnapshotContentBackupItemAction")
if backup.Status.Phase == velerov1api.BackupPhaseFinalizing ||
backup.Status.Phase == velerov1api.BackupPhaseFinalizingPartiallyFailed {
p.log.WithField("Backup", fmt.Sprintf("%s/%s", backup.Namespace, backup.Name)).
WithField("BackupPhase", backup.Status.Phase).
Debug("Skipping VolumeSnapshotContentBackupItemAction",
"as backup is in finalizing phase.")
return item, nil, "", nil, nil
}
var snapCont snapshotv1api.VolumeSnapshotContent
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
item.UnstructuredContent(),
&snapCont,
); err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
additionalItems := []velero.ResourceIdentifier{}
// we should backup the snapshot deletion secrets that may be referenced
// in the VolumeSnapshotContent's annotation
if csiutil.IsVolumeSnapshotContentHasDeleteSecret(&snapCont) {
additionalItems = append(
additionalItems,
velero.ResourceIdentifier{
GroupResource: kuberesource.Secrets,
Name: snapCont.Annotations[velerov1api.PrefixedSecretNameAnnotation],
Namespace: snapCont.Annotations[velerov1api.PrefixedSecretNamespaceAnnotation],
})
kubeutil.AddAnnotations(&snapCont.ObjectMeta, map[string]string{
velerov1api.MustIncludeAdditionalItemAnnotation: "true",
})
}
snapContMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&snapCont)
if err != nil {
return nil, nil, "", nil, errors.WithStack(err)
}
p.log.Infof(
"Returning from VolumeSnapshotContentBackupItemAction",
"with %d additionalItems to backup",
len(additionalItems),
)
return &unstructured.Unstructured{Object: snapContMap}, additionalItems, "", nil, nil
}
// Name returns the plugin's name.
func (p *volumeSnapshotContentBackupItemAction) Name() string {
return "VolumeSnapshotContentBackupItemAction"
}
// Progress is not implemented for VolumeSnapshotContentBackupItemAction.
func (p *volumeSnapshotContentBackupItemAction) Progress(
operationID string,
backup *velerov1api.Backup,
) (velero.OperationProgress, error) {
return velero.OperationProgress{}, nil
}
// Cancel is not implemented for VolumeSnapshotContentBackupItemAction.
func (p *volumeSnapshotContentBackupItemAction) Cancel(
operationID string,
backup *velerov1api.Backup,
) error {
// CSI Specification doesn't support canceling a snapshot creation.
return nil
}
// NewVolumeSnapshotContentBackupItemAction returns a
// VolumeSnapshotContentBackupItemAction instance.
func NewVolumeSnapshotContentBackupItemAction(
logger logrus.FieldLogger,
) (interface{}, error) {
return &volumeSnapshotContentBackupItemAction{log: logger}, nil
}

View File

@ -383,16 +383,16 @@ func TestBackupOldResourceFiltering(t *testing.T) {
Result(),
apiResources: []*test.APIResource{
test.Pods(
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true")).Result(),
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true")).Result(),
builder.ForPod("zoo", "raz").Result(),
),
test.Deployments(
builder.ForDeployment("foo", "bar").Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true")).Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true")).Result(),
),
test.PVs(
builder.ForPersistentVolume("bar").ObjectMeta(builder.WithLabels("a", "b")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true")).Result(),
),
},
want: []string{
@ -411,16 +411,16 @@ func TestBackupOldResourceFiltering(t *testing.T) {
Result(),
apiResources: []*test.APIResource{
test.Pods(
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true", "a", "b")).Result(),
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true", "a", "b")).Result(),
builder.ForPod("zoo", "raz").ObjectMeta(builder.WithLabels("a", "b")).Result(),
),
test.Deployments(
builder.ForDeployment("foo", "bar").Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true", "a", "b")).Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true", "a", "b")).Result(),
),
test.PVs(
builder.ForPersistentVolume("bar").ObjectMeta(builder.WithLabels("a", "b")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels("a", "b", "velero.io/exclude-from-backup", "true")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels("a", "b", velerov1.ExcludeFromBackupLabel, "true")).Result(),
),
},
want: []string{
@ -436,16 +436,16 @@ func TestBackupOldResourceFiltering(t *testing.T) {
Result(),
apiResources: []*test.APIResource{
test.Pods(
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "false")).Result(),
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "false")).Result(),
builder.ForPod("zoo", "raz").Result(),
),
test.Deployments(
builder.ForDeployment("foo", "bar").Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "1")).Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "1")).Result(),
),
test.PVs(
builder.ForPersistentVolume("bar").ObjectMeta(builder.WithLabels("a", "b")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "")).Result(),
),
},
want: []string{
@ -1273,7 +1273,7 @@ func (a *recordResourcesAction) Execute(item runtime.Unstructured, backup *veler
a.backups = append(a.backups, *backup)
if a.skippedCSISnapshot {
u := &unstructured.Unstructured{Object: item.UnstructuredContent()}
u.SetAnnotations(map[string]string{skippedNoCSIPVAnnotation: "true"})
u.SetAnnotations(map[string]string{velerov1.SkippedNoCSIPVAnnotation: "true"})
item = u
a.additionalItems = nil
}
@ -2028,7 +2028,7 @@ func TestBackupActionAdditionalItems(t *testing.T) {
builder.ForPod("ns-1", "pod-1").Result(),
),
test.PVs(
builder.ForPersistentVolume("pv-1").ObjectMeta(builder.WithLabels("velero.io/exclude-from-backup", "true")).Result(),
builder.ForPersistentVolume("pv-1").ObjectMeta(builder.WithLabels(velerov1.ExcludeFromBackupLabel, "true")).Result(),
builder.ForPersistentVolume("pv-2").Result(),
),
},

View File

@ -24,19 +24,17 @@ import (
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
kbClient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/internal/hook"
@ -58,11 +56,8 @@ import (
)
const (
mustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
skippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
excludeFromBackupLabel = "velero.io/exclude-from-backup"
csiBIAPluginName = "velero.io/csi-pvc-backupper"
vsphereBIAPluginName = "velero.io/vsphere-pvc-backupper"
csiBIAPluginName = "velero.io/csi-pvc-backupper"
vsphereBIAPluginName = "velero.io/vsphere-pvc-backupper"
)
// itemBackupper can back up individual items to a tar writer.
@ -129,9 +124,9 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti
if mustInclude {
log.Infof("Skipping the exclusion checks for this resource")
} else {
if metadata.GetLabels()[excludeFromBackupLabel] == "true" {
log.Infof("Excluding item because it has label %s=true", excludeFromBackupLabel)
ib.trackSkippedPV(obj, groupResource, "", fmt.Sprintf("item has label %s=true", excludeFromBackupLabel), log)
if metadata.GetLabels()[velerov1api.ExcludeFromBackupLabel] == "true" {
log.Infof("Excluding item because it has label %s=true", velerov1api.ExcludeFromBackupLabel)
ib.trackSkippedPV(obj, groupResource, "", fmt.Sprintf("item has label %s=true", velerov1api.ExcludeFromBackupLabel), log)
return false, itemFiles, nil
}
// NOTE: we have to re-check namespace & resource includes/excludes because it's possible that
@ -384,18 +379,18 @@ func (ib *itemBackupper) executeActions(
return nil, itemFiles, errors.Wrapf(err, "error executing custom action (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name)
}
u := &unstructured.Unstructured{Object: updatedItem.UnstructuredContent()}
if actionName == csiBIAPluginName && additionalItemIdentifiers == nil && u.GetAnnotations()[skippedNoCSIPVAnnotation] == "true" {
if actionName == csiBIAPluginName && additionalItemIdentifiers == nil && u.GetAnnotations()[velerov1api.SkippedNoCSIPVAnnotation] == "true" {
// snapshot was skipped by CSI plugin
ib.trackSkippedPV(obj, groupResource, csiSnapshotApproach, "skipped b/c it's not a CSI volume", log)
delete(u.GetAnnotations(), skippedNoCSIPVAnnotation)
delete(u.GetAnnotations(), velerov1api.SkippedNoCSIPVAnnotation)
} else if (actionName == csiBIAPluginName || actionName == vsphereBIAPluginName) && !boolptr.IsSetToFalse(ib.backupRequest.Backup.Spec.SnapshotVolumes) {
// the snapshot has been taken by the BIA plugin
ib.unTrackSkippedPV(obj, groupResource, log)
}
mustInclude := u.GetAnnotations()[mustIncludeAdditionalItemAnnotation] == "true" || finalize
mustInclude := u.GetAnnotations()[velerov1api.MustIncludeAdditionalItemAnnotation] == "true" || finalize
// remove the annotation as it's for communication between BIA and velero server,
// we don't want the resource be restored with this annotation.
delete(u.GetAnnotations(), mustIncludeAdditionalItemAnnotation)
delete(u.GetAnnotations(), velerov1api.MustIncludeAdditionalItemAnnotation)
obj = u
// If async plugin started async operation, add it to the ItemOperations list

View File

@ -195,6 +195,9 @@ func (f *factory) KubebuilderWatchClient() (kbclient.WithWatch, error) {
if err := apiextv1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := snapshotv1api.AddToScheme(scheme); err != nil {
return nil, err
}
kubebuilderWatchClient, err := kbclient.NewWithWatch(clientConfig, kbclient.Options{
Scheme: scheme,
})

View File

@ -23,14 +23,17 @@ import (
"github.com/vmware-tanzu/velero/pkg/datamover"
dia "github.com/vmware-tanzu/velero/internal/delete/actions/csi"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
bia "github.com/vmware-tanzu/velero/pkg/backup/actions"
csibia "github.com/vmware-tanzu/velero/pkg/backup/actions/csi"
"github.com/vmware-tanzu/velero/pkg/client"
velerodiscovery "github.com/vmware-tanzu/velero/pkg/discovery"
"github.com/vmware-tanzu/velero/pkg/features"
veleroplugin "github.com/vmware-tanzu/velero/pkg/plugin/framework"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
ria "github.com/vmware-tanzu/velero/pkg/restore/actions"
csiria "github.com/vmware-tanzu/velero/pkg/restore/actions/csi"
)
func NewCommand(f client.Factory) *cobra.Command {
@ -41,32 +44,142 @@ func NewCommand(f client.Factory) *cobra.Command {
Short: "INTERNAL COMMAND ONLY - not intended to be run directly by users",
Run: func(c *cobra.Command, args []string) {
pluginServer = pluginServer.
RegisterBackupItemAction("velero.io/pv", newPVBackupItemAction).
RegisterBackupItemAction("velero.io/pod", newPodBackupItemAction).
RegisterBackupItemAction("velero.io/service-account", newServiceAccountBackupItemAction(f)).
RegisterRestoreItemAction("velero.io/job", newJobRestoreItemAction).
RegisterRestoreItemAction("velero.io/pod", newPodRestoreItemAction).
RegisterRestoreItemAction("velero.io/pod-volume-restore", newPodVolumeRestoreItemAction(f)).
RegisterRestoreItemAction("velero.io/init-restore-hook", newInitRestoreHookPodAction).
RegisterRestoreItemAction("velero.io/service", newServiceRestoreItemAction).
RegisterRestoreItemAction("velero.io/service-account", newServiceAccountRestoreItemAction).
RegisterRestoreItemAction("velero.io/add-pvc-from-pod", newAddPVCFromPodRestoreItemAction).
RegisterRestoreItemAction("velero.io/add-pv-from-pvc", newAddPVFromPVCRestoreItemAction).
RegisterRestoreItemAction("velero.io/change-storage-class", newChangeStorageClassRestoreItemAction(f)).
RegisterRestoreItemAction("velero.io/change-image-name", newChangeImageNameRestoreItemAction(f)).
RegisterRestoreItemAction("velero.io/role-bindings", newRoleBindingItemAction).
RegisterRestoreItemAction("velero.io/cluster-role-bindings", newClusterRoleBindingItemAction).
RegisterRestoreItemAction("velero.io/crd-preserve-fields", newCRDV1PreserveUnknownFieldsItemAction).
RegisterRestoreItemAction("velero.io/change-pvc-node-selector", newChangePVCNodeSelectorItemAction(f)).
RegisterRestoreItemAction("velero.io/apiservice", newAPIServiceRestoreItemAction).
RegisterRestoreItemAction("velero.io/admission-webhook-configuration", newAdmissionWebhookConfigurationAction).
RegisterRestoreItemAction("velero.io/secret", newSecretRestoreItemAction(f)).
RegisterRestoreItemAction("velero.io/dataupload", newDataUploadRetrieveAction(f)).
RegisterDeleteItemAction("velero.io/dataupload-delete", newDateUploadDeleteItemAction(f))
RegisterBackupItemAction(
"velero.io/pv",
newPVBackupItemAction,
).
RegisterBackupItemAction(
"velero.io/pod",
newPodBackupItemAction,
).
RegisterBackupItemAction(
"velero.io/service-account",
newServiceAccountBackupItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/job",
newJobRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/pod",
newPodRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/pod-volume-restore",
newPodVolumeRestoreItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/init-restore-hook",
newInitRestoreHookPodAction,
).
RegisterRestoreItemAction(
"velero.io/service",
newServiceRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/service-account",
newServiceAccountRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/add-pvc-from-pod",
newAddPVCFromPodRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/add-pv-from-pvc",
newAddPVFromPVCRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/change-storage-class",
newChangeStorageClassRestoreItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/change-image-name",
newChangeImageNameRestoreItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/role-bindings",
newRoleBindingItemAction,
).
RegisterRestoreItemAction(
"velero.io/cluster-role-bindings",
newClusterRoleBindingItemAction,
).
RegisterRestoreItemAction(
"velero.io/crd-preserve-fields",
newCRDV1PreserveUnknownFieldsItemAction,
).
RegisterRestoreItemAction(
"velero.io/change-pvc-node-selector",
newChangePVCNodeSelectorItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/apiservice",
newAPIServiceRestoreItemAction,
).
RegisterRestoreItemAction(
"velero.io/admission-webhook-configuration",
newAdmissionWebhookConfigurationAction,
).
RegisterRestoreItemAction(
"velero.io/secret",
newSecretRestoreItemAction(f),
).
RegisterRestoreItemAction(
"velero.io/dataupload",
newDataUploadRetrieveAction(f),
).
RegisterDeleteItemAction(
"velero.io/dataupload-delete",
newDateUploadDeleteItemAction(f),
).
RegisterDeleteItemAction(
"velero.io/csi-volumesnapshot-delete",
newVolumeSnapshotDeleteItemAction(f),
).
RegisterDeleteItemAction(
"velero.io/csi-volumesnapshotcontent-delete",
newVolumeSnapshotContentDeleteItemAction(f),
).
RegisterBackupItemActionV2(
"velero.io/csi-pvc-backupper",
newPvcBackupItemAction(f),
).
RegisterBackupItemActionV2(
"velero.io/csi-volumesnapshot-backupper",
newVolumeSnapshotBackupItemAction(f),
).
RegisterBackupItemActionV2(
"velero.io/csi-volumesnapshotcontent-backupper",
newVolumeSnapshotContentBackupItemAction,
).
RegisterBackupItemActionV2(
"velero.io/csi-volumesnapshotclass-backupper",
newVolumeSnapshotClassBackupItemAction,
).
RegisterRestoreItemActionV2(
"velero.io/csi-pvc-restorer",
newPvcRestoreItemAction(f),
).
RegisterRestoreItemActionV2(
"velero.io/csi-volumesnapshot-restorer",
newVolumeSnapshotRestoreItemAction(f),
).
RegisterRestoreItemActionV2(
"velero.io/csi-volumesnapshotcontent-restorer",
newVolumeSnapshotContentRestoreItemAction,
).
RegisterRestoreItemActionV2(
"velero.io/csi-volumesnapshotclass-restorer",
newVolumeSnapshotClassRestoreItemAction,
)
if !features.IsEnabled(velerov1api.APIGroupVersionsFeatureFlag) {
// Do not register crd-remap-version BIA if the API Group feature flag is enabled, so that the v1 CRD can be backed up
pluginServer = pluginServer.RegisterBackupItemAction("velero.io/crd-remap-version", newRemapCRDVersionAction(f))
// Do not register crd-remap-version BIA if the API Group feature
// flag is enabled, so that the v1 CRD can be backed up.
pluginServer = pluginServer.RegisterBackupItemAction(
"velero.io/crd-remap-version",
newRemapCRDVersionAction(f),
)
}
pluginServer.Serve()
},
@ -270,3 +383,51 @@ func newDateUploadDeleteItemAction(f client.Factory) plugincommon.HandlerInitial
return datamover.NewDataUploadDeleteAction(logger, client), nil
}
}
// CSI plugin init functions.
// BackupItemAction plugins
func newPvcBackupItemAction(f client.Factory) plugincommon.HandlerInitializer {
return csibia.NewPvcBackupItemAction(f)
}
func newVolumeSnapshotBackupItemAction(f client.Factory) plugincommon.HandlerInitializer {
return csibia.NewVolumeSnapshotBackupItemAction(f)
}
func newVolumeSnapshotContentBackupItemAction(logger logrus.FieldLogger) (interface{}, error) {
return csibia.NewVolumeSnapshotContentBackupItemAction(logger)
}
func newVolumeSnapshotClassBackupItemAction(logger logrus.FieldLogger) (interface{}, error) {
return csibia.NewVolumeSnapshotClassBackupItemAction(logger)
}
// DeleteItemAction plugins
func newVolumeSnapshotDeleteItemAction(f client.Factory) plugincommon.HandlerInitializer {
return dia.NewVolumeSnapshotDeleteItemAction(f)
}
func newVolumeSnapshotContentDeleteItemAction(f client.Factory) plugincommon.HandlerInitializer {
return dia.NewVolumeSnapshotContentDeleteItemAction(f)
}
// RestoreItemAction plugins
func newPvcRestoreItemAction(f client.Factory) plugincommon.HandlerInitializer {
return csiria.NewPvcRestoreItemAction(f)
}
func newVolumeSnapshotRestoreItemAction(f client.Factory) plugincommon.HandlerInitializer {
return csiria.NewVolumeSnapshotRestoreItemAction(f)
}
func newVolumeSnapshotContentRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return csiria.NewVolumeSnapshotContentRestoreItemAction(logger)
}
func newVolumeSnapshotClassRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return csiria.NewVolumeSnapshotClassRestoreItemAction(logger)
}

View File

@ -431,7 +431,7 @@ func (b *backupReconciler) prepareBackupRequest(backup *velerov1api.Backup, logg
// Add namespaces with label velero.io/exclude-from-backup=true into request.Spec.ExcludedNamespaces
// Essentially, adding the label velero.io/exclude-from-backup=true to a namespace would be equivalent to setting spec.ExcludedNamespaces
namespaces := corev1api.NamespaceList{}
if err := b.kbClient.List(context.Background(), &namespaces, kbclient.MatchingLabels{"velero.io/exclude-from-backup": "true"}); err == nil {
if err := b.kbClient.List(context.Background(), &namespaces, kbclient.MatchingLabels{velerov1api.ExcludeFromBackupLabel: "true"}); err == nil {
for _, ns := range namespaces.Items {
request.Spec.ExcludedNamespaces = append(request.Spec.ExcludedNamespaces, ns.Name)
}

View File

@ -20,27 +20,22 @@ import (
"context"
"time"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
snapshotter "github.com/kubernetes-csi/external-snapshotter/client/v7/clientset/versioned/typed/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/vmware-tanzu/velero/pkg/nodeagent"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/vmware-tanzu/velero/pkg/util/csi"
"github.com/vmware-tanzu/velero/pkg/util/kube"
snapshotter "github.com/kubernetes-csi/external-snapshotter/client/v7/clientset/versioned/typed/volumesnapshot/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/vmware-tanzu/velero/pkg/nodeagent"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
"github.com/vmware-tanzu/velero/pkg/util/kube"
)
// CSISnapshotExposeParam define the input param for Expose of CSI snapshots

View File

@ -160,7 +160,7 @@ func TestExpose(t *testing.T) {
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
},
err: "error wait volume snapshot ready: error to get volumesnapshot /fake-vs: volumesnapshots.snapshot.storage.k8s.io \"fake-vs\" not found",
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",

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"encoding/json"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/sirupsen/logrus"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"bytes"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"sort"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"encoding/json"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"encoding/json"

View File

@ -0,0 +1,616 @@
/*
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"
"encoding/json"
"fmt"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/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"
utilrand "k8s.io/apimachinery/pkg/util/rand"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
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/client"
"github.com/vmware-tanzu/velero/pkg/label"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
riav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v2"
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
"github.com/vmware-tanzu/velero/pkg/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
)
const (
AnnBindCompleted = "pv.kubernetes.io/bind-completed"
AnnBoundByController = "pv.kubernetes.io/bound-by-controller"
AnnStorageProvisioner = "volume.kubernetes.io/storage-provisioner"
AnnBetaStorageProvisioner = "volume.beta.kubernetes.io/storage-provisioner"
AnnSelectedNode = "volume.kubernetes.io/selected-node"
)
const (
GenerateNameRandomLength = 5
)
// pvcRestoreItemAction is a restore item action plugin for Velero
type pvcRestoreItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating that the
// PVCRestoreItemAction should be run while restoring PVCs.
func (p *pvcRestoreItemAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"persistentvolumeclaims"},
//TODO: add label selector volumeSnapshotLabel
}, nil
}
func removePVCAnnotations(pvc *corev1api.PersistentVolumeClaim, remove []string) {
if pvc.Annotations == nil {
pvc.Annotations = make(map[string]string)
return
}
for k := range pvc.Annotations {
if util.Contains(remove, k) {
delete(pvc.Annotations, k)
}
}
}
func resetPVCSpec(pvc *corev1api.PersistentVolumeClaim, vsName string) {
// Restore operation for the PVC will use the VolumeSnapshot as the data source.
// So clear out the volume name, which is a ref to the PV
pvc.Spec.VolumeName = ""
dataSource := &corev1api.TypedLocalObjectReference{
APIGroup: &snapshotv1api.SchemeGroupVersion.Group,
Kind: "VolumeSnapshot",
Name: vsName,
}
pvc.Spec.DataSource = dataSource
pvc.Spec.DataSourceRef = nil
}
func setPVCStorageResourceRequest(
pvc *corev1api.PersistentVolumeClaim,
restoreSize resource.Quantity,
log logrus.FieldLogger,
) {
{
if pvc.Spec.Resources.Requests == nil {
pvc.Spec.Resources.Requests = corev1api.ResourceList{}
}
storageReq, exists := pvc.Spec.Resources.Requests[corev1api.ResourceStorage]
if !exists || storageReq.Cmp(restoreSize) < 0 {
pvc.Spec.Resources.Requests[corev1api.ResourceStorage] = restoreSize
rs := pvc.Spec.Resources.Requests[corev1api.ResourceStorage]
log.Infof("Resetting storage requests for PVC %s/%s to %s",
pvc.Namespace, pvc.Name, rs.String())
}
}
}
// Execute modifies the PVC's spec to use the VolumeSnapshot object as the
// data source ensuring that the newly provisioned volume can be pre-populated
// with data from the VolumeSnapshot.
func (p *pvcRestoreItemAction) Execute(
input *velero.RestoreItemActionExecuteInput,
) (*velero.RestoreItemActionExecuteOutput, error) {
var pvc, pvcFromBackup corev1api.PersistentVolumeClaim
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(), &pvc); err != nil {
return nil, errors.WithStack(err)
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.ItemFromBackup.UnstructuredContent(), &pvcFromBackup); err != nil {
return nil, errors.WithStack(err)
}
logger := p.log.WithFields(logrus.Fields{
"Action": "PVCRestoreItemAction",
"PVC": pvc.Namespace + "/" + pvc.Name,
"Restore": input.Restore.Namespace + "/" + input.Restore.Name,
})
logger.Info("Starting PVCRestoreItemAction for PVC")
// If PVC already exists, returns early.
if p.isResourceExist(pvc, *input.Restore) {
logger.Warnf("PVC already exists. Skip restore this PVC.")
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
}, nil
}
// remove the VolumeSnapshot name annotation as well
// clean the DataUploadNameLabel for snapshot data mover case.
removePVCAnnotations(
&pvc,
[]string{
AnnBindCompleted,
AnnBoundByController,
AnnStorageProvisioner,
AnnBetaStorageProvisioner,
AnnSelectedNode,
velerov1api.VolumeSnapshotLabel,
velerov1api.DataUploadNameAnnotation,
},
)
// If cross-namespace restore is configured, change the namespace
// for PVC object to be restored
newNamespace, ok := input.Restore.Spec.NamespaceMapping[pvc.GetNamespace()]
if !ok {
// Use original namespace
newNamespace = pvc.Namespace
}
operationID := ""
if boolptr.IsSetToFalse(input.Restore.Spec.RestorePVs) {
logger.Info("Restore did not request for PVs to be restored from snapshot")
pvc.Spec.VolumeName = ""
pvc.Spec.DataSource = nil
pvc.Spec.DataSourceRef = nil
} else {
backup := new(velerov1api.Backup)
err := p.crClient.Get(
context.TODO(),
crclient.ObjectKey{
Namespace: input.Restore.Namespace,
Name: input.Restore.Spec.BackupName,
},
backup,
)
if err != nil {
logger.Error("Fail to get backup for restore.")
return nil, fmt.Errorf("fail to get backup for restore: %s", err.Error())
}
if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) {
logger.Info("Start DataMover restore.")
// If PVC doesn't have a DataUploadNameLabel, which should be created
// during backup, then CSI cannot handle the volume during to restore,
// so return early to let Velero tries to fall back to Velero native snapshot.
if _, ok := pvcFromBackup.Annotations[velerov1api.DataUploadNameAnnotation]; !ok {
logger.Warnf("PVC doesn't have a DataUpload for data mover. Return.")
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
}, nil
}
operationID = label.GetValidName(
string(velerov1api.AsyncOperationIDPrefixDataDownload) +
string(input.Restore.UID) + "." + string(pvcFromBackup.UID))
dataDownload, err := restoreFromDataUploadResult(
context.Background(), input.Restore, backup, &pvc, newNamespace,
operationID, p.crClient)
if err != nil {
logger.Errorf("Fail to restore from DataUploadResult: %s", err.Error())
return nil, errors.WithStack(err)
}
logger.Infof("DataDownload %s/%s is created successfully.",
dataDownload.Namespace, dataDownload.Name)
} else {
volumeSnapshotName, ok := pvcFromBackup.Annotations[velerov1api.VolumeSnapshotLabel]
if !ok {
logger.Info("Skipping PVCRestoreItemAction for PVC,",
"PVC does not have a CSI VolumeSnapshot.")
// Make no change in the input PVC.
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
}, nil
}
if err := restoreFromVolumeSnapshot(
&pvc, newNamespace, p.crClient, volumeSnapshotName, logger,
); err != nil {
logger.Errorf("Failed to restore PVC from VolumeSnapshot.")
return nil, errors.WithStack(err)
}
}
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pvc)
if err != nil {
return nil, errors.WithStack(err)
}
logger.Info("Returning from PVCRestoreItemAction for PVC")
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: &unstructured.Unstructured{Object: pvcMap},
OperationID: operationID,
}, nil
}
func (p *pvcRestoreItemAction) Name() string {
return "PVCRestoreItemAction"
}
func (p *pvcRestoreItemAction) Progress(
operationID string,
restore *velerov1api.Restore,
) (velero.OperationProgress, error) {
progress := velero.OperationProgress{}
if operationID == "" {
return progress, riav2.InvalidOperationIDError(operationID)
}
logger := p.log.WithFields(logrus.Fields{
"Action": "PVCRestoreItemAction",
"OperationID": operationID,
"Namespace": restore.Namespace,
})
dataDownload, err := getDataDownload(
context.Background(),
restore.Namespace,
operationID,
p.crClient,
)
if err != nil {
logger.Errorf("fail to get DataDownload: %s", err.Error())
return progress, err
}
if dataDownload.Status.Phase == velerov2alpha1.DataDownloadPhaseNew ||
dataDownload.Status.Phase == "" {
logger.Debugf("DataDownload is still not processed yet. Skip progress update.")
return progress, nil
}
progress.Description = string(dataDownload.Status.Phase)
progress.OperationUnits = "Bytes"
progress.NCompleted = dataDownload.Status.Progress.BytesDone
progress.NTotal = dataDownload.Status.Progress.TotalBytes
if dataDownload.Status.StartTimestamp != nil {
progress.Started = dataDownload.Status.StartTimestamp.Time
}
if dataDownload.Status.CompletionTimestamp != nil {
progress.Updated = dataDownload.Status.CompletionTimestamp.Time
}
if dataDownload.Status.Phase == velerov2alpha1.DataDownloadPhaseCompleted {
progress.Completed = true
} else if dataDownload.Status.Phase == velerov2alpha1.DataDownloadPhaseCanceled {
progress.Completed = true
progress.Err = "DataDownload is canceled"
} else if dataDownload.Status.Phase == velerov2alpha1.DataDownloadPhaseFailed {
progress.Completed = true
progress.Err = dataDownload.Status.Message
}
return progress, nil
}
func (p *pvcRestoreItemAction) Cancel(
operationID string, restore *velerov1api.Restore) error {
if operationID == "" {
return riav2.InvalidOperationIDError(operationID)
}
logger := p.log.WithFields(logrus.Fields{
"Action": "PVCRestoreItemAction",
"OperationID": operationID,
"Namespace": restore.Namespace,
})
dataDownload, err := getDataDownload(
context.Background(),
restore.Namespace,
operationID,
p.crClient,
)
if err != nil {
logger.Errorf("fail to get DataDownload: %s", err.Error())
return err
}
err = cancelDataDownload(context.Background(), p.crClient, dataDownload)
if err != nil {
logger.Errorf("fail to cancel DataDownload %s: %s", dataDownload.Name, err.Error())
}
return err
}
func (p *pvcRestoreItemAction) AreAdditionalItemsReady(
additionalItems []velero.ResourceIdentifier,
restore *velerov1api.Restore,
) (bool, error) {
return true, nil
}
func getDataUploadResult(
ctx context.Context,
restore *velerov1api.Restore,
pvc *corev1api.PersistentVolumeClaim,
crClient crclient.Client,
) (*velerov2alpha1.DataUploadResult, error) {
selectorStr := fmt.Sprintf("%s=%s,%s=%s,%s=%s",
velerov1api.PVCNamespaceNameLabel,
label.GetValidName(pvc.Namespace+"."+pvc.Name),
velerov1api.RestoreUIDLabel,
label.GetValidName(string(restore.UID)),
velerov1api.ResourceUsageLabel,
label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)),
)
selector, _ := labels.Parse(selectorStr)
cmList := new(corev1api.ConfigMapList)
if err := crClient.List(
ctx,
cmList,
&crclient.ListOptions{
LabelSelector: selector,
Namespace: restore.Namespace,
}); err != nil {
return nil, errors.Wrapf(err,
"error to get DataUpload result cm with labels %s", selectorStr)
}
if len(cmList.Items) == 0 {
return nil, errors.Errorf(
"no DataUpload result cm found with labels %s", selectorStr)
}
if len(cmList.Items) > 1 {
return nil, errors.Errorf(
"multiple DataUpload result cms found with labels %s", selectorStr)
}
jsonBytes, exist := cmList.Items[0].Data[string(restore.UID)]
if !exist {
return nil, errors.Errorf(
"no DataUpload result found with restore key %s, restore %s",
string(restore.UID), restore.Name)
}
result := velerov2alpha1.DataUploadResult{}
if err := json.Unmarshal([]byte(jsonBytes), &result); err != nil {
return nil, errors.Errorf(
"error to unmarshal DataUploadResult, restore UID %s, restore name %s",
string(restore.UID), restore.Name)
}
return &result, nil
}
func getDataDownload(
ctx context.Context,
namespace string,
operationID string,
crClient crclient.Client,
) (*velerov2alpha1.DataDownload, error) {
dataDownloadList := new(velerov2alpha1.DataDownloadList)
err := crClient.List(ctx, dataDownloadList, &crclient.ListOptions{
LabelSelector: labels.SelectorFromSet(map[string]string{
velerov1api.AsyncOperationIDLabel: operationID,
}),
Namespace: namespace,
})
if err != nil {
return nil, errors.Wrap(err, "fail to list DataDownload")
}
if len(dataDownloadList.Items) == 0 {
return nil, errors.Errorf("didn't find DataDownload")
}
if len(dataDownloadList.Items) > 1 {
return nil, errors.Errorf("find multiple DataDownloads")
}
return &dataDownloadList.Items[0], nil
}
func cancelDataDownload(ctx context.Context, crClient crclient.Client,
dataDownload *velerov2alpha1.DataDownload) error {
updatedDataDownload := dataDownload.DeepCopy()
updatedDataDownload.Spec.Cancel = true
return crClient.Patch(ctx, updatedDataDownload, crclient.MergeFrom(dataDownload))
}
func newDataDownload(
restore *velerov1api.Restore,
backup *velerov1api.Backup,
dataUploadResult *velerov2alpha1.DataUploadResult,
pvc *corev1api.PersistentVolumeClaim,
newNamespace, operationID string,
) *velerov2alpha1.DataDownload {
dataDownload := &velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
Kind: "DataDownload",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: restore.Namespace,
GenerateName: restore.Name + "-",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: velerov1api.SchemeGroupVersion.String(),
Kind: "Restore",
Name: restore.Name,
UID: restore.UID,
Controller: boolptr.True(),
},
},
Labels: map[string]string{
velerov1api.RestoreNameLabel: label.GetValidName(restore.Name),
velerov1api.RestoreUIDLabel: string(restore.UID),
velerov1api.AsyncOperationIDLabel: operationID,
},
},
Spec: velerov2alpha1.DataDownloadSpec{
TargetVolume: velerov2alpha1.TargetVolumeSpec{
PVC: pvc.Name,
Namespace: newNamespace,
},
BackupStorageLocation: dataUploadResult.BackupStorageLocation,
DataMover: dataUploadResult.DataMover,
SnapshotID: dataUploadResult.SnapshotID,
SourceNamespace: dataUploadResult.SourceNamespace,
OperationTimeout: backup.Spec.CSISnapshotTimeout,
},
}
if restore.Spec.UploaderConfig != nil {
dataDownload.Spec.DataMoverConfig = make(map[string]string)
if boolptr.IsSetToTrue(restore.Spec.UploaderConfig.WriteSparseFiles) {
dataDownload.Spec.DataMoverConfig[uploaderUtil.WriteSparseFiles] = "true"
} else {
dataDownload.Spec.DataMoverConfig[uploaderUtil.WriteSparseFiles] = "false"
}
}
return dataDownload
}
func restoreFromVolumeSnapshot(
pvc *corev1api.PersistentVolumeClaim,
newNamespace string,
crClient crclient.Client,
volumeSnapshotName string,
logger logrus.FieldLogger,
) error {
vs := new(snapshotv1api.VolumeSnapshot)
if err := crClient.Get(context.TODO(),
crclient.ObjectKey{
Namespace: newNamespace,
Name: volumeSnapshotName,
},
vs,
); err != nil {
return errors.Wrapf(err,
fmt.Sprintf("Failed to get Volumesnapshot %s/%s to restore PVC %s/%s",
newNamespace, volumeSnapshotName, newNamespace, pvc.Name),
)
}
if _, exists := vs.Annotations[velerov1api.VolumeSnapshotRestoreSize]; exists {
restoreSize, err := resource.ParseQuantity(
vs.Annotations[velerov1api.VolumeSnapshotRestoreSize])
if err != nil {
return errors.Wrapf(err, fmt.Sprintf(
"Failed to parse %s from annotation on Volumesnapshot %s/%s into restore size",
vs.Annotations[velerov1api.VolumeSnapshotRestoreSize], vs.Namespace, vs.Name))
}
// It is possible that the volume provider allocated a larger
// capacity volume than what was requested in the backed up PVC.
// In this scenario the volumesnapshot of the PVC will end being
// larger than its requested storage size. Such a PVC, on restore
// as-is, will be stuck attempting to use a VolumeSnapshot as a
// data source for a PVC that is not large enough.
// To counter that, here we set the storage request on the PVC
// to the larger of the PVC's storage request and the size of the
// VolumeSnapshot
setPVCStorageResourceRequest(pvc, restoreSize, logger)
}
resetPVCSpec(pvc, volumeSnapshotName)
return nil
}
func restoreFromDataUploadResult(
ctx context.Context,
restore *velerov1api.Restore,
backup *velerov1api.Backup,
pvc *corev1api.PersistentVolumeClaim,
newNamespace, operationID string,
crClient crclient.Client,
) (*velerov2alpha1.DataDownload, error) {
dataUploadResult, err := getDataUploadResult(ctx, restore, pvc, crClient)
if err != nil {
return nil, errors.Wrapf(err, "fail get DataUploadResult for restore: %s",
restore.Name)
}
pvc.Spec.VolumeName = ""
if pvc.Spec.Selector == nil {
pvc.Spec.Selector = &metav1.LabelSelector{}
}
if pvc.Spec.Selector.MatchLabels == nil {
pvc.Spec.Selector.MatchLabels = make(map[string]string)
}
pvc.Spec.Selector.MatchLabels[velerov1api.DynamicPVRestoreLabel] = label.
GetValidName(fmt.Sprintf("%s.%s.%s", newNamespace,
pvc.Name, utilrand.String(GenerateNameRandomLength)))
dataDownload := newDataDownload(
restore,
backup,
dataUploadResult,
pvc,
newNamespace,
operationID,
)
err = crClient.Create(ctx, dataDownload)
if err != nil {
return nil, errors.Wrapf(err, "fail to create DataDownload")
}
return dataDownload, nil
}
func (p *pvcRestoreItemAction) isResourceExist(
pvc corev1api.PersistentVolumeClaim,
restore velerov1api.Restore,
) bool {
// get target namespace to restore into, if different from source namespace
targetNamespace := pvc.Namespace
if target, ok := restore.Spec.NamespaceMapping[pvc.Namespace]; ok {
targetNamespace = target
}
tmpPVC := new(corev1api.PersistentVolumeClaim)
if err := p.crClient.Get(
context.Background(),
crclient.ObjectKey{
Name: pvc.Name,
Namespace: targetNamespace,
},
tmpPVC,
); err == nil {
return true
}
return false
}
func NewPvcRestoreItemAction(f client.Factory) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, err
}
return &pvcRestoreItemAction{
log: logger,
crClient: crClient,
}, nil
}
}

View File

@ -0,0 +1,713 @@
/*
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"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/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/validation"
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"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
)
func TestRemovePVCAnnotations(t *testing.T) {
testCases := []struct {
name string
pvc corev1api.PersistentVolumeClaim
removeAnnotations []string
expectedAnnotations map[string]string
}{
{
name: "should create empty annotation map",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Annotations: nil,
},
},
removeAnnotations: []string{"foo"},
expectedAnnotations: map[string]string{},
},
{
name: "should preserve all existing annotations",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"ann1": "ann1-val",
"ann2": "ann2-val",
"ann3": "ann3-val",
"ann4": "ann4-val",
},
},
},
removeAnnotations: []string{},
expectedAnnotations: map[string]string{
"ann1": "ann1-val",
"ann2": "ann2-val",
"ann3": "ann3-val",
"ann4": "ann4-val",
},
},
{
name: "should remove all existing annotations",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"ann1": "ann1-val",
"ann2": "ann2-val",
"ann3": "ann3-val",
"ann4": "ann4-val",
},
},
},
removeAnnotations: []string{"ann1", "ann2", "ann3", "ann4"},
expectedAnnotations: map[string]string{},
},
{
name: "should preserve some existing annotations",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"ann1": "ann1-val",
"ann2": "ann2-val",
"ann3": "ann3-val",
"ann4": "ann4-val",
"ann5": "ann5-val",
"ann6": "ann6-val",
"ann7": "ann7-val",
"ann8": "ann8-val",
},
},
},
removeAnnotations: []string{"ann1", "ann2", "ann3", "ann4"},
expectedAnnotations: map[string]string{
"ann5": "ann5-val",
"ann6": "ann6-val",
"ann7": "ann7-val",
"ann8": "ann8-val",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
removePVCAnnotations(&tc.pvc, tc.removeAnnotations)
assert.Equal(t, tc.expectedAnnotations, tc.pvc.Annotations)
})
}
}
func TestResetPVCSpec(t *testing.T) {
fileMode := corev1api.PersistentVolumeFilesystem
blockMode := corev1api.PersistentVolumeBlock
testCases := []struct {
name string
pvc corev1api.PersistentVolumeClaim
vsName string
}{
{
name: "should reset expected fields in pvc using file mode volumes",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pvc",
Namespace: "test-ns",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadOnlyMany, corev1api.ReadWriteMany, corev1api.ReadWriteOnce},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
"baz": "qux",
},
},
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceCPU: resource.Quantity{
Format: resource.DecimalExponent,
},
},
},
VolumeName: "should-be-removed",
VolumeMode: &fileMode,
},
},
vsName: "test-vs",
},
{
name: "should reset expected fields in pvc using block mode volumes",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pvc",
Namespace: "test-ns",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadOnlyMany, corev1api.ReadWriteMany, corev1api.ReadWriteOnce},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
"baz": "qux",
},
},
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceCPU: resource.Quantity{
Format: resource.DecimalExponent,
},
},
},
VolumeName: "should-be-removed",
VolumeMode: &blockMode,
},
},
vsName: "test-vs",
},
{
name: "should overwrite existing DataSource per reset parameters",
pvc: corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pvc",
Namespace: "test-ns",
},
Spec: corev1api.PersistentVolumeClaimSpec{
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadOnlyMany, corev1api.ReadWriteMany, corev1api.ReadWriteOnce},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
"baz": "qux",
},
},
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceCPU: resource.Quantity{
Format: resource.DecimalExponent,
},
},
},
VolumeName: "should-be-removed",
VolumeMode: &fileMode,
DataSource: &corev1api.TypedLocalObjectReference{
Kind: "something-that-does-not-exist",
Name: "not-found",
},
DataSourceRef: &corev1api.TypedObjectReference{
Kind: "something-that-does-not-exist",
Name: "not-found",
},
},
},
vsName: "test-vs",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
before := tc.pvc.DeepCopy()
resetPVCSpec(&tc.pvc, tc.vsName)
assert.Equalf(t, tc.pvc.Name, before.Name, "unexpected change to Object.Name, Want: %s; Got %s", before.Name, tc.pvc.Name)
assert.Equalf(t, tc.pvc.Namespace, before.Namespace, "unexpected change to Object.Namespace, Want: %s; Got %s", before.Namespace, tc.pvc.Namespace)
assert.Equalf(t, tc.pvc.Spec.AccessModes, before.Spec.AccessModes, "unexpected Spec.AccessModes, Want: %v; Got: %v", before.Spec.AccessModes, tc.pvc.Spec.AccessModes)
assert.Equalf(t, tc.pvc.Spec.Selector, before.Spec.Selector, "unexpected change to Spec.Selector, Want: %s; Got: %s", before.Spec.Selector.String(), tc.pvc.Spec.Selector.String())
assert.Equalf(t, tc.pvc.Spec.Resources, before.Spec.Resources, "unexpected change to Spec.Resources, Want: %s; Got: %s", before.Spec.Resources.String(), tc.pvc.Spec.Resources.String())
assert.Emptyf(t, tc.pvc.Spec.VolumeName, "expected change to Spec.VolumeName missing, Want: \"\"; Got: %s", tc.pvc.Spec.VolumeName)
assert.Equalf(t, *tc.pvc.Spec.VolumeMode, *before.Spec.VolumeMode, "expected change to Spec.VolumeName missing, Want: \"\"; Got: %s", tc.pvc.Spec.VolumeName)
assert.NotNil(t, tc.pvc.Spec.DataSource, "expected change to Spec.DataSource missing")
assert.Equalf(t, tc.pvc.Spec.DataSource.Kind, "VolumeSnapshot", "expected change to Spec.DataSource.Kind missing, Want: VolumeSnapshot, Got: %s", tc.pvc.Spec.DataSource.Kind)
assert.Equalf(t, tc.pvc.Spec.DataSource.Name, tc.vsName, "expected change to Spec.DataSource.Name missing, Want: %s, Got: %s", tc.vsName, tc.pvc.Spec.DataSource.Name)
})
}
}
func TestResetPVCResourceRequest(t *testing.T) {
var storageReq50Mi, storageReq1Gi, cpuQty resource.Quantity
storageReq50Mi, err := resource.ParseQuantity("50Mi")
assert.NoError(t, err)
storageReq1Gi, err = resource.ParseQuantity("1Gi")
assert.NoError(t, err)
cpuQty, err = resource.ParseQuantity("100m")
assert.NoError(t, err)
testCases := []struct {
name string
pvc corev1api.PersistentVolumeClaim
restoreSize resource.Quantity
expectedStorageRequestQty string
}{
{
name: "should set storage resource request from volumesnapshot, pvc has nil resource requests",
pvc: corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
Resources: corev1api.VolumeResourceRequirements{
Requests: nil,
},
},
},
restoreSize: storageReq50Mi,
expectedStorageRequestQty: "50Mi",
},
{
name: "should set storage resource request from volumesnapshot, pvc has empty resource requests",
pvc: corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{},
},
},
},
restoreSize: storageReq50Mi,
expectedStorageRequestQty: "50Mi",
},
{
name: "should merge resource requests from volumesnapshot into pvc with no storage resource requests",
pvc: corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceCPU: cpuQty,
},
},
},
},
restoreSize: storageReq50Mi,
expectedStorageRequestQty: "50Mi",
},
{
name: "should set storage resource request from volumesnapshot, pvc requests less storage",
pvc: corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceStorage: storageReq50Mi,
},
},
},
},
restoreSize: storageReq1Gi,
expectedStorageRequestQty: "1Gi",
},
{
name: "should not set storage resource request from volumesnapshot, pvc requests more storage",
pvc: corev1api.PersistentVolumeClaim{
Spec: corev1api.PersistentVolumeClaimSpec{
Resources: corev1api.VolumeResourceRequirements{
Requests: corev1api.ResourceList{
corev1api.ResourceStorage: storageReq1Gi,
},
},
},
},
restoreSize: storageReq50Mi,
expectedStorageRequestQty: "1Gi",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
log := logrus.New().WithField("unit-test", tc.name)
setPVCStorageResourceRequest(&tc.pvc, tc.restoreSize, log)
expected, err := resource.ParseQuantity(tc.expectedStorageRequestQty)
assert.NoError(t, err)
assert.Equal(t, expected, tc.pvc.Spec.Resources.Requests[corev1api.ResourceStorage])
})
}
}
func TestProgress(t *testing.T) {
currentTime := time.Now()
tests := []struct {
name string
restore *velerov1api.Restore
dataDownload *velerov2alpha1.DataDownload
operationID string
expectedErr string
expectedProgress velero.OperationProgress
}{
{
name: "DataDownload cannot be found",
restore: builder.ForRestore("velero", "test").Result(),
operationID: "testing",
expectedErr: "didn't find DataDownload",
},
{
name: "DataDownload is not in the expected namespace",
restore: builder.ForRestore("velero", "test").Result(),
dataDownload: &velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
Kind: "DataUpload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "invalid-namespace",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
},
operationID: "testing",
expectedErr: "didn't find DataDownload",
},
{
name: "DataUpload is found",
restore: builder.ForRestore("velero", "test").Result(),
dataDownload: &velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
Kind: "DataUpload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
Status: velerov2alpha1.DataDownloadStatus{
Phase: velerov2alpha1.DataDownloadPhaseFailed,
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(*testing.T) {
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t),
}
if tc.dataDownload != nil {
err := pvcRIA.crClient.Create(context.Background(), tc.dataDownload)
require.NoError(t, err)
}
progress, err := pvcRIA.Progress(tc.operationID, tc.restore)
if tc.expectedErr != "" {
require.Equal(t, tc.expectedErr, err.Error())
return
}
require.NoError(t, err)
require.True(t, cmp.Equal(tc.expectedProgress, progress, cmpopts.IgnoreFields(velero.OperationProgress{}, "Started", "Updated")))
})
}
}
func TestCancel(t *testing.T) {
tests := []struct {
name string
restore *velerov1api.Restore
dataDownload *velerov2alpha1.DataDownload
operationID string
expectedErr string
expectedDataDownload velerov2alpha1.DataDownload
}{
{
name: "Cancel DataUpload",
restore: builder.ForRestore("velero", "test").Result(),
dataDownload: &velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
Kind: "DataDownload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
},
operationID: "testing",
expectedErr: "",
expectedDataDownload: velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
Kind: "DataDownload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
Spec: velerov2alpha1.DataDownloadSpec{
Cancel: true,
},
},
},
{
name: "Cannot find DataUpload",
restore: builder.ForRestore("velero", "test").Result(),
dataDownload: nil,
operationID: "testing",
expectedErr: "didn't find DataDownload",
expectedDataDownload: velerov2alpha1.DataDownload{
TypeMeta: metav1.TypeMeta{
Kind: "DataDownload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
Spec: velerov2alpha1.DataDownloadSpec{
Cancel: true,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t),
}
if tc.dataDownload != nil {
err := pvcRIA.crClient.Create(context.Background(), tc.dataDownload)
require.NoError(t, err)
}
err := pvcRIA.Cancel(tc.operationID, tc.restore)
if tc.expectedErr != "" {
require.Equal(t, tc.expectedErr, err.Error())
return
}
require.NoError(t, err)
resultDataDownload := new(velerov2alpha1.DataDownload)
err = pvcRIA.crClient.Get(context.Background(), crclient.ObjectKey{Namespace: tc.dataDownload.Namespace, Name: tc.dataDownload.Name}, resultDataDownload)
require.NoError(t, err)
require.True(t, cmp.Equal(tc.expectedDataDownload, *resultDataDownload, cmpopts.IgnoreFields(velerov2alpha1.DataDownload{}, "ResourceVersion", "Name")))
})
}
}
func TestExecute(t *testing.T) {
tests := []struct {
name string
backup *velerov1api.Backup
restore *velerov1api.Restore
pvc *corev1api.PersistentVolumeClaim
vs *snapshotv1api.VolumeSnapshot
dataUploadResult *corev1api.ConfigMap
expectedErr string
expectedDataDownload *velerov2alpha1.DataDownload
expectedPVC *corev1api.PersistentVolumeClaim
preCreatePVC bool
}{
{
name: "Don't restore PV",
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").RestorePVs(false).Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("").Result(),
},
{
name: "restore's backup cannot be found",
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(),
expectedErr: "fail to get backup for restore: backups.velero.io \"testBackup\" not found",
},
{
name: "VolumeSnapshot cannot be found",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "testVS")).Result(),
expectedErr: "Failed to get Volumesnapshot velero/testVS to restore PVC velero/testPVC: volumesnapshots.snapshot.storage.k8s.io \"testVS\" not found",
},
{
name: "Restore from VolumeSnapshot",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "testVS")).
RequestResource(map[corev1api.ResourceName]resource.Quantity{corev1api.ResourceStorage: resource.MustParse("10Gi")}).
DataSource(&corev1api.TypedLocalObjectReference{APIGroup: &snapshotv1api.SchemeGroupVersion.Group, Kind: "VolumeSnapshot", Name: "testVS"}).
DataSourceRef(&corev1api.TypedObjectReference{APIGroup: &snapshotv1api.SchemeGroupVersion.Group, Kind: "VolumeSnapshot", Name: "testVS"}).
Result(),
vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(),
},
{
name: "Restore from VolumeSnapshot without volume-snapshot-name annotation",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(),
vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(),
},
{
name: "DataUploadResult cannot be found",
backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(),
expectedErr: "fail get DataUploadResult for restore: testRestore: no DataUpload result cm found with labels velero.io/pvc-namespace-name=velero.testPVC,velero.io/restore-uid=,velero.io/resource-usage=DataUpload",
},
{
name: "Restore from DataUploadResult",
backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(),
dataUploadResult: builder.ForConfigMap("velero", "testCM").Data("uid", "{}").ObjectMeta(builder.WithLabels(velerov1api.RestoreUIDLabel, "uid", velerov1api.PVCNamespaceNameLabel, "velero.testPVC", velerov1api.ResourceUsageLabel, label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)))).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations("velero.io/csi-volumesnapshot-restore-size", "10Gi")).Result(),
expectedDataDownload: builder.ForDataDownload("velero", "name").TargetVolume(velerov2alpha1.TargetVolumeSpec{PVC: "testPVC", Namespace: "velero"}).
ObjectMeta(builder.WithOwnerReference([]metav1.OwnerReference{{APIVersion: velerov1api.SchemeGroupVersion.String(), Kind: "Restore", Name: "testRestore", UID: "uid", Controller: boolptr.True()}}),
builder.WithLabelsMap(map[string]string{velerov1api.AsyncOperationIDLabel: "dd-uid.", velerov1api.RestoreNameLabel: "testRestore", velerov1api.RestoreUIDLabel: "uid"}),
builder.WithGenerateName("testRestore-")).Result(),
},
{
name: "Restore from DataUploadResult with long source PVC namespace and name",
backup: builder.ForBackup("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(),
pvc: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(),
dataUploadResult: builder.ForConfigMap("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testCM").Data("uid", "{}").ObjectMeta(builder.WithLabels(velerov1api.RestoreUIDLabel, "uid", velerov1api.PVCNamespaceNameLabel, "migre209d0da-49c7-45ba-8d5a-3e59fd591ec1.kibishii-data-ki152333", velerov1api.ResourceUsageLabel, label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)))).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations("velero.io/csi-volumesnapshot-restore-size", "10Gi")).Result(),
},
{
name: "PVC had no DataUploadNameLabel annotation",
backup: builder.ForBackup("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(),
pvc: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(),
},
{
name: "Restore a PVC that already exists.",
backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(),
preCreatePVC: true,
},
{
name: "Restore a PVC that already exists in the mapping namespace",
backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").NamespaceMappings("velero", "restore").ObjectMeta(builder.WithUID("uid")).Result(),
pvc: builder.ForPersistentVolumeClaim("restore", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(),
preCreatePVC: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
object := make([]runtime.Object, 0)
if tc.backup != nil {
object = append(object, tc.backup)
}
if tc.vs != nil {
object = append(object, tc.vs)
}
input := new(velero.RestoreItemActionExecuteInput)
if tc.pvc != nil {
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvc)
require.NoError(t, err)
input.Item = &unstructured.Unstructured{Object: pvcMap}
input.ItemFromBackup = &unstructured.Unstructured{Object: pvcMap}
input.Restore = tc.restore
}
if tc.preCreatePVC {
object = append(object, tc.pvc)
}
if tc.dataUploadResult != nil {
object = append(object, tc.dataUploadResult)
}
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t, object...),
}
output, err := pvcRIA.Execute(input)
if tc.expectedErr != "" {
require.Equal(t, tc.expectedErr, err.Error())
return
}
require.NoError(t, err)
if tc.expectedPVC != nil {
pvc := new(corev1api.PersistentVolumeClaim)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(output.UpdatedItem.UnstructuredContent(), pvc)
require.NoError(t, err)
require.Equal(t, tc.expectedPVC.GetObjectMeta(), pvc.GetObjectMeta())
if pvc.Spec.Selector != nil && pvc.Spec.Selector.MatchLabels != nil {
// This is used for long name and namespace case.
if len(tc.pvc.Namespace+"."+tc.pvc.Name) >= validation.DNS1035LabelMaxLength {
require.Contains(t, pvc.Spec.Selector.MatchLabels[velerov1api.DynamicPVRestoreLabel], label.GetValidName(tc.pvc.Namespace + "." + tc.pvc.Name)[:56])
} else {
require.Contains(t, pvc.Spec.Selector.MatchLabels[velerov1api.DynamicPVRestoreLabel], tc.pvc.Namespace+"."+tc.pvc.Name)
}
}
}
if tc.expectedDataDownload != nil {
dataDownloadList := new(velerov2alpha1.DataDownloadList)
err := pvcRIA.crClient.List(context.Background(), dataDownloadList, &crclient.ListOptions{
LabelSelector: labels.SelectorFromSet(tc.expectedDataDownload.Labels),
})
require.NoError(t, err)
require.True(t, cmp.Equal(tc.expectedDataDownload, &dataDownloadList.Items[0], cmpopts.IgnoreFields(velerov2alpha1.DataDownload{}, "ResourceVersion", "Name")))
}
})
}
}

View File

@ -0,0 +1,217 @@
/*
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"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
core_v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/label"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
)
// volumeSnapshotRestoreItemAction is a Velero restore item
// action plugin for VolumeSnapshots
type volumeSnapshotRestoreItemAction struct {
log logrus.FieldLogger
crClient crclient.Client
}
// AppliesTo returns information indicating that
// VolumeSnapshotRestoreItemAction should be invoked while
// restoring volumesnapshots.snapshot.storage.k8s.io resources.
func (p *volumeSnapshotRestoreItemAction) AppliesTo() (
velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"},
}, nil
}
func resetVolumeSnapshotSpecForRestore(
vs *snapshotv1api.VolumeSnapshot, vscName *string) {
// Spec of the backed-up object used the PVC as the source
// of the volumeSnapshot. Restore operation will however,
// restore the VolumeSnapshot from the VolumeSnapshotContent
vs.Spec.Source.PersistentVolumeClaimName = nil
vs.Spec.Source.VolumeSnapshotContentName = vscName
}
func resetVolumeSnapshotAnnotation(vs *snapshotv1api.VolumeSnapshot) {
vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation] =
string(snapshotv1api.VolumeSnapshotContentRetain)
}
// Execute uses the data such as CSI driver name, storage
// snapshot handle, snapshot deletion secret (if any) from
// the annotations to recreate a VolumeSnapshotContent object
// and statically bind the VolumeSnapshot object being restored.
func (p *volumeSnapshotRestoreItemAction) Execute(
input *velero.RestoreItemActionExecuteInput,
) (*velero.RestoreItemActionExecuteOutput, error) {
p.log.Info("Starting VolumeSnapshotRestoreItemAction")
if boolptr.IsSetToFalse(input.Restore.Spec.RestorePVs) {
p.log.Infof("Restore %s/%s did not request for PVs to be restored.",
input.Restore.Namespace, input.Restore.Name)
return &velero.RestoreItemActionExecuteOutput{SkipRestore: true}, nil
}
var vs snapshotv1api.VolumeSnapshot
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(), &vs); err != nil {
return &velero.RestoreItemActionExecuteOutput{},
errors.Wrapf(err, "failed to convert input.Item from unstructured")
}
// If cross-namespace restore is configured, change the namespace
// for VolumeSnapshot object to be restored
newNamespace, ok := input.Restore.Spec.NamespaceMapping[vs.GetNamespace()]
if !ok {
// Use original namespace
newNamespace = vs.Namespace
}
if !csi.IsVolumeSnapshotExists(newNamespace, vs.Name, p.crClient) {
snapHandle, exists := vs.Annotations[velerov1api.VolumeSnapshotHandleAnnotation]
if !exists {
return nil, errors.Errorf(
"Volumesnapshot %s/%s does not have a %s annotation",
vs.Namespace,
vs.Name,
velerov1api.VolumeSnapshotHandleAnnotation,
)
}
csiDriverName, exists := vs.Annotations[velerov1api.DriverNameAnnotation]
if !exists {
return nil, errors.Errorf(
"Volumesnapshot %s/%s does not have a %s annotation",
vs.Namespace, vs.Name, velerov1api.DriverNameAnnotation)
}
p.log.Debugf("Set VolumeSnapshotContent %s/%s DeletionPolicy",
"to Retain to make sure VS deletion in namespace will not",
"delete Snapshot on cloud provider.",
newNamespace, vs.Name)
vsc := snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
GenerateName: vs.Name + "-",
Labels: map[string]string{
velerov1api.RestoreNameLabel: label.GetValidName(input.Restore.Name),
},
},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Driver: csiDriverName,
VolumeSnapshotRef: core_v1.ObjectReference{
Kind: "VolumeSnapshot",
Namespace: newNamespace,
Name: vs.Name,
},
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: &snapHandle,
},
},
}
// we create the VolumeSnapshotContent here instead of relying on the
// restore flow because we want to statically bind this VolumeSnapshot
// with a VolumeSnapshotContent that will be used as its source for pre-populating
// the volume that will be created as a result of the restore. To perform
// this static binding, a bi-directional link between the VolumeSnapshotContent
// and VolumeSnapshot objects have to be setup. Further, it is disallowed
// to convert a dynamically created VolumeSnapshotContent for static binding.
// See: https://github.com/kubernetes-csi/external-snapshotter/issues/274
if err := p.crClient.Create(context.TODO(), &vsc); err != nil {
return nil, errors.Wrapf(err,
"failed to create volumesnapshotcontents %s",
vsc.GenerateName)
}
p.log.Infof("Created VolumesnapshotContents %s with static",
"binding to volumesnapshot %s/%s", vsc, newNamespace, vs.Name)
// Reset Spec to convert the VolumeSnapshot from using
// the dynamic VolumeSnapshotContent to the static one.
resetVolumeSnapshotSpecForRestore(&vs, &vsc.Name)
// Reset VolumeSnapshot annotation. By now, only change
// DeletionPolicy to Retain.
resetVolumeSnapshotAnnotation(&vs)
}
vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&vs)
if err != nil {
return nil, errors.WithStack(err)
}
p.log.Infof("Returning from VolumeSnapshotRestoreItemAction with no additionalItems")
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: &unstructured.Unstructured{Object: vsMap},
AdditionalItems: []velero.ResourceIdentifier{},
}, nil
}
func (p *volumeSnapshotRestoreItemAction) Name() string {
return "VolumeSnapshotRestoreItemAction"
}
func (p *volumeSnapshotRestoreItemAction) Progress(
operationID string,
restore *velerov1api.Restore,
) (velero.OperationProgress, error) {
return velero.OperationProgress{}, nil
}
func (p *volumeSnapshotRestoreItemAction) Cancel(
operationID string,
restore *velerov1api.Restore,
) error {
return nil
}
func (p *volumeSnapshotRestoreItemAction) AreAdditionalItemsReady(
additionalItems []velero.ResourceIdentifier,
restore *velerov1api.Restore,
) (bool, error) {
return true, nil
}
func NewVolumeSnapshotRestoreItemAction(
f client.Factory) plugincommon.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
crClient, err := f.KubebuilderClient()
if err != nil {
return nil, err
}
return &volumeSnapshotRestoreItemAction{logger, crClient}, nil
}
}

View File

@ -0,0 +1,89 @@
/*
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 (
"testing"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
testPVC = "test-pvc"
testSnapClass = "snap-class"
randText = "DEADFEED"
)
func TestResetVolumeSnapshotSpecForRestore(t *testing.T) {
testCases := []struct {
name string
vs snapshotv1api.VolumeSnapshot
vscName string
}{
{
name: "should reset spec as expected",
vs: snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vs",
Namespace: "test-ns",
},
Spec: snapshotv1api.VolumeSnapshotSpec{
Source: snapshotv1api.VolumeSnapshotSource{
PersistentVolumeClaimName: &testPVC,
},
VolumeSnapshotClassName: &testSnapClass,
},
},
vscName: "test-vsc",
},
{
name: "should reset spec and overwriting value for Source.VolumeSnapshotContentName",
vs: snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vs",
Namespace: "test-ns",
},
Spec: snapshotv1api.VolumeSnapshotSpec{
Source: snapshotv1api.VolumeSnapshotSource{
VolumeSnapshotContentName: &randText,
},
VolumeSnapshotClassName: &testSnapClass,
},
},
vscName: "test-vsc",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
before := tc.vs.DeepCopy()
resetVolumeSnapshotSpecForRestore(&tc.vs, &tc.vscName)
assert.Equalf(t, tc.vs.Name, before.Name, "unexpected change to Object.Name, Want: %s; Got %s", tc.name, before.Name, tc.vs.Name)
assert.Equal(t, tc.vs.Namespace, before.Namespace, "unexpected change to Object.Namespace, Want: %s; Got %s", tc.name, before.Namespace, tc.vs.Namespace)
assert.NotNil(t, tc.vs.Spec.Source)
assert.Nil(t, tc.vs.Spec.Source.PersistentVolumeClaimName)
assert.NotNil(t, tc.vs.Spec.Source.VolumeSnapshotContentName)
assert.Equal(t, *tc.vs.Spec.Source.VolumeSnapshotContentName, tc.vscName)
assert.Equal(t, *tc.vs.Spec.VolumeSnapshotClassName, *before.Spec.VolumeSnapshotClassName, "unexpected value for Spec.VolumeSnapshotClassName, Want: %s, Got: %s",
*tc.vs.Spec.VolumeSnapshotClassName, *before.Spec.VolumeSnapshotClassName)
assert.Nil(t, tc.vs.Status)
})
}
}

View File

@ -0,0 +1,111 @@
/*
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 (
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
)
// volumeSnapshotClassRestoreItemAction is a Velero restore
// item action plugin for VolumeSnapshotClass
type volumeSnapshotClassRestoreItemAction struct {
log logrus.FieldLogger
}
// AppliesTo returns information indicating that VolumeSnapshotClassRestoreItemAction
// should be invoked while restoring volumesnapshotclass.snapshot.storage.k8s.io resources.
func (p *volumeSnapshotClassRestoreItemAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshotclass.snapshot.storage.k8s.io"},
}, nil
}
// Execute restores VolumeSnapshotClass objects returning any
// snapshotlister secret as additional items to restore
func (p *volumeSnapshotClassRestoreItemAction) Execute(
input *velero.RestoreItemActionExecuteInput,
) (*velero.RestoreItemActionExecuteOutput, error) {
p.log.Info("Starting VolumeSnapshotClassRestoreItemAction")
if boolptr.IsSetToFalse(input.Restore.Spec.RestorePVs) {
p.log.Infof("Restore did not request for PVs to be restored %s/%s",
input.Restore.Namespace, input.Restore.Name)
return &velero.RestoreItemActionExecuteOutput{SkipRestore: true}, nil
}
var snapClass snapshotv1api.VolumeSnapshotClass
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(), &snapClass); err != nil {
return &velero.RestoreItemActionExecuteOutput{}, errors.Wrapf(err,
"failed to convert input.Item from unstructured")
}
additionalItems := []velero.ResourceIdentifier{}
if csi.IsVolumeSnapshotClassHasListerSecret(&snapClass) {
additionalItems = append(additionalItems, velero.ResourceIdentifier{
GroupResource: schema.GroupResource{Group: "", Resource: "secrets"},
Name: snapClass.Annotations[velerov1api.PrefixedListSecretNameAnnotation],
Namespace: snapClass.Annotations[velerov1api.PrefixedListSecretNamespaceAnnotation],
})
}
p.log.Infof("Returning from VolumeSnapshotClassRestoreItemAction with %d additionalItems",
len(additionalItems))
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: additionalItems,
}, nil
}
func (p *volumeSnapshotClassRestoreItemAction) Name() string {
return "VolumeSnapshotClassRestoreItemAction"
}
func (p *volumeSnapshotClassRestoreItemAction) Progress(
operationID string,
restore *velerov1api.Restore,
) (velero.OperationProgress, error) {
return velero.OperationProgress{}, nil
}
func (p *volumeSnapshotClassRestoreItemAction) Cancel(
operationID string,
restore *velerov1api.Restore,
) error {
return nil
}
func (p *volumeSnapshotClassRestoreItemAction) AreAdditionalItemsReady(
additionalItems []velero.ResourceIdentifier,
restore *velerov1api.Restore,
) (bool, error) {
return true, nil
}
func NewVolumeSnapshotClassRestoreItemAction(
logger logrus.FieldLogger) (interface{}, error) {
return &volumeSnapshotClassRestoreItemAction{logger}, nil
}

View File

@ -0,0 +1,113 @@
/*
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 (
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/csi"
)
// volumeSnapshotContentRestoreItemAction is a restore item action
// plugin for Velero
type volumeSnapshotContentRestoreItemAction struct {
log logrus.FieldLogger
}
// AppliesTo returns information indicating VolumeSnapshotContentRestoreItemAction
// action should be invoked while restoring
// volumesnapshotcontent.snapshot.storage.k8s.io resources
func (p *volumeSnapshotContentRestoreItemAction) AppliesTo() (
velero.ResourceSelector, error,
) {
return velero.ResourceSelector{
IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"},
}, nil
}
// Execute restores a VolumeSnapshotContent object without modification
// returning the snapshot lister secret, if any, as additional items to restore.
func (p *volumeSnapshotContentRestoreItemAction) Execute(
input *velero.RestoreItemActionExecuteInput,
) (*velero.RestoreItemActionExecuteOutput, error) {
p.log.Info("Starting VolumeSnapshotContentRestoreItemAction")
var snapCont snapshotv1api.VolumeSnapshotContent
if boolptr.IsSetToFalse(input.Restore.Spec.RestorePVs) {
p.log.Infof("Restore did not request for PVs to be restored %s/%s",
input.Restore.Namespace, input.Restore.Name)
return &velero.RestoreItemActionExecuteOutput{SkipRestore: true}, nil
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(
input.Item.UnstructuredContent(), &snapCont); err != nil {
return &velero.RestoreItemActionExecuteOutput{},
errors.Wrapf(err, "failed to convert input.Item from unstructured")
}
additionalItems := []velero.ResourceIdentifier{}
if csi.IsVolumeSnapshotContentHasDeleteSecret(&snapCont) {
additionalItems = append(additionalItems,
velero.ResourceIdentifier{
GroupResource: schema.GroupResource{Group: "", Resource: "secrets"},
Name: snapCont.Annotations[velerov1api.DeleteSecretNameAnnotation],
Namespace: snapCont.Annotations[velerov1api.DeleteSecretNamespaceAnnotation],
},
)
}
p.log.Infof("Returning from VolumeSnapshotContentRestoreItemAction",
"with %d additionalItems", len(additionalItems))
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: additionalItems,
}, nil
}
func (p *volumeSnapshotContentRestoreItemAction) Name() string {
return "VolumeSnapshotContentRestoreItemAction"
}
func (p *volumeSnapshotContentRestoreItemAction) Progress(
operationID string,
restore *velerov1api.Restore,
) (velero.OperationProgress, error) {
return velero.OperationProgress{}, nil
}
func (p *volumeSnapshotContentRestoreItemAction) Cancel(
operationID string,
restore *velerov1api.Restore,
) error {
return nil
}
func (p *volumeSnapshotContentRestoreItemAction) AreAdditionalItemsReady(
additionalItems []velero.ResourceIdentifier,
restore *velerov1api.Restore,
) (bool, error) {
return true, nil
}
func NewVolumeSnapshotContentRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return &volumeSnapshotContentRestoreItemAction{logger}, nil
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -120,8 +119,6 @@ func TestDataUploadRetrieveActionExectue(t *testing.T) {
})
require.NoError(t, err)
// debug
fmt.Printf("CM: %s\n", &cmList.Items[0])
require.Equal(t, tc.expectedDataUploadResult.Labels, cmList.Items[0].Labels)
require.Equal(t, tc.expectedDataUploadResult.Data, cmList.Items[0].Data)
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"strings"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"sort"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"github.com/pkg/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"sort"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"context"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"testing"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"strings"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"sort"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"encoding/json"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
package actions
import (
"encoding/json"

View File

@ -23,6 +23,7 @@ import (
"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/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
k8sfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
@ -33,34 +34,33 @@ import (
func NewFakeControllerRuntimeClientBuilder(t *testing.T) *k8sfake.ClientBuilder {
scheme := runtime.NewScheme()
err := velerov1api.AddToScheme(scheme)
require.NoError(t, err)
err = velerov2alpha1api.AddToScheme(scheme)
require.NoError(t, err)
err = corev1api.AddToScheme(scheme)
require.NoError(t, err)
err = appsv1api.AddToScheme(scheme)
require.NoError(t, err)
err = snapshotv1api.AddToScheme(scheme)
require.NoError(t, err)
require.NoError(t, velerov1api.AddToScheme(scheme))
require.NoError(t, velerov2alpha1api.AddToScheme(scheme))
require.NoError(t, corev1api.AddToScheme(scheme))
require.NoError(t, appsv1api.AddToScheme(scheme))
require.NoError(t, snapshotv1api.AddToScheme(scheme))
require.NoError(t, storagev1api.AddToScheme(scheme))
return k8sfake.NewClientBuilder().WithScheme(scheme)
}
func NewFakeControllerRuntimeClient(t *testing.T, initObjs ...runtime.Object) client.Client {
scheme := runtime.NewScheme()
err := velerov1api.AddToScheme(scheme)
require.NoError(t, err)
err = velerov2alpha1api.AddToScheme(scheme)
require.NoError(t, err)
err = corev1api.AddToScheme(scheme)
require.NoError(t, err)
err = appsv1api.AddToScheme(scheme)
require.NoError(t, err)
err = snapshotv1api.AddToScheme(scheme)
require.NoError(t, err)
require.NoError(t, velerov1api.AddToScheme(scheme))
require.NoError(t, velerov2alpha1api.AddToScheme(scheme))
require.NoError(t, corev1api.AddToScheme(scheme))
require.NoError(t, appsv1api.AddToScheme(scheme))
require.NoError(t, snapshotv1api.AddToScheme(scheme))
require.NoError(t, storagev1api.AddToScheme(scheme))
return k8sfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjs...).Build()
}
func NewFakeControllerRuntimeWatchClient(t *testing.T, initObjs ...runtime.Object) client.WithWatch {
func NewFakeControllerRuntimeWatchClient(
t *testing.T,
initObjs ...runtime.Object,
) client.WithWatch {
return NewFakeControllerRuntimeClientBuilder(t).WithRuntimeObjects(initObjs...).Build()
}

View File

@ -20,25 +20,26 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
jsonpatch "github.com/evanphx/json-patch"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
snapshotter "github.com/kubernetes-csi/external-snapshotter/client/v7/clientset/versioned/typed/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/stringptr"
"github.com/vmware-tanzu/velero/pkg/util/stringslice"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1"
snapshotter "github.com/kubernetes-csi/external-snapshotter/client/v7/clientset/versioned/typed/volumesnapshot/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
const (
@ -47,51 +48,80 @@ const (
)
// WaitVolumeSnapshotReady waits a VS to become ready to use until the timeout reaches
func WaitVolumeSnapshotReady(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
volumeSnapshot string, volumeSnapshotNS string, timeout time.Duration, log logrus.FieldLogger) (*snapshotv1api.VolumeSnapshot, error) {
func WaitVolumeSnapshotReady(
ctx context.Context,
snapshotClient snapshotter.SnapshotV1Interface,
volumeSnapshot string,
volumeSnapshotNS string,
timeout time.Duration,
log logrus.FieldLogger,
) (*snapshotv1api.VolumeSnapshot, error) {
var updated *snapshotv1api.VolumeSnapshot
errMessage := sets.NewString()
err := wait.PollUntilContextTimeout(ctx, waitInternal, timeout, true, func(ctx context.Context) (bool, error) {
tmpVS, err := snapshotClient.VolumeSnapshots(volumeSnapshotNS).Get(ctx, volumeSnapshot, metav1.GetOptions{})
if err != nil {
return false, errors.Wrapf(err, fmt.Sprintf("error to get volumesnapshot %s/%s", volumeSnapshotNS, volumeSnapshot))
}
err := wait.PollUntilContextTimeout(
ctx,
waitInternal,
timeout,
true,
func(ctx context.Context) (bool, error) {
tmpVS, err := snapshotClient.VolumeSnapshots(volumeSnapshotNS).Get(
ctx, volumeSnapshot, metav1.GetOptions{})
if err != nil {
return false, errors.Wrapf(
err,
fmt.Sprintf("error to get VolumeSnapshot %s/%s",
volumeSnapshotNS, volumeSnapshot),
)
}
if tmpVS.Status == nil {
return false, nil
}
if tmpVS.Status == nil {
return false, nil
}
if tmpVS.Status.Error != nil {
errMessage.Insert(stringptr.GetString(tmpVS.Status.Error.Message))
}
if tmpVS.Status.Error != nil {
errMessage.Insert(stringptr.GetString(tmpVS.Status.Error.Message))
}
if !boolptr.IsSetToTrue(tmpVS.Status.ReadyToUse) {
return false, nil
}
if !boolptr.IsSetToTrue(tmpVS.Status.ReadyToUse) {
return false, nil
}
updated = tmpVS
return true, nil
})
updated = tmpVS
return true, nil
},
)
if wait.Interrupted(err) {
err = errors.Errorf("volume snapshot is not ready until timeout, errors: %v", errMessage.List())
err = errors.Errorf(
"volume snapshot is not ready until timeout, errors: %v",
errMessage.List(),
)
}
if errMessage.Len() > 0 {
log.Warnf("Some errors happened during waiting for ready snapshot, errors: %v", errMessage.List())
log.Warnf("Some errors happened during waiting for ready snapshot, errors: %v",
errMessage.List())
}
return updated, err
}
// GetVolumeSnapshotContentForVolumeSnapshot returns the volumesnapshotcontent object associated with the volumesnapshot
func GetVolumeSnapshotContentForVolumeSnapshot(volSnap *snapshotv1api.VolumeSnapshot, snapshotClient snapshotter.SnapshotV1Interface) (*snapshotv1api.VolumeSnapshotContent, error) {
// GetVolumeSnapshotContentForVolumeSnapshot returns the VolumeSnapshotContent
// object associated with the VolumeSnapshot.
func GetVolumeSnapshotContentForVolumeSnapshot(
volSnap *snapshotv1api.VolumeSnapshot,
snapshotClient snapshotter.SnapshotV1Interface,
) (*snapshotv1api.VolumeSnapshotContent, error) {
if volSnap.Status == nil || volSnap.Status.BoundVolumeSnapshotContentName == nil {
return nil, errors.Errorf("invalid snapshot info in volume snapshot %s", volSnap.Name)
}
vsc, err := snapshotClient.VolumeSnapshotContents().Get(context.TODO(), *volSnap.Status.BoundVolumeSnapshotContentName, metav1.GetOptions{})
vsc, err := snapshotClient.VolumeSnapshotContents().Get(
context.TODO(),
*volSnap.Status.BoundVolumeSnapshotContentName,
metav1.GetOptions{},
)
if err != nil {
return nil, errors.Wrap(err, "error getting volume snapshot content from API")
}
@ -99,7 +129,8 @@ func GetVolumeSnapshotContentForVolumeSnapshot(volSnap *snapshotv1api.VolumeSnap
return vsc, nil
}
// RetainVSC updates the VSC's deletion policy to Retain and add a finalier and then return the update VSC
// RetainVSC updates the VSC's deletion policy to Retain and add a
// finalizer and then return the update VSC
func RetainVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vsc *snapshotv1api.VolumeSnapshotContent) (*snapshotv1api.VolumeSnapshotContent, error) {
if vsc.Spec.DeletionPolicy == snapshotv1api.VolumeSnapshotContentRetain {
@ -112,8 +143,13 @@ func RetainVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interfa
})
}
// DeleteVolumeSnapshotContentIfAny deletes a VSC by name if it exists, and log an error when the deletion fails
func DeleteVolumeSnapshotContentIfAny(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface, vscName string, log logrus.FieldLogger) {
// DeleteVolumeSnapshotContentIfAny deletes a VSC by name if it exists,
// and log an error when the deletion fails.
func DeleteVolumeSnapshotContentIfAny(
ctx context.Context,
snapshotClient snapshotter.SnapshotV1Interface,
vscName string, log logrus.FieldLogger,
) {
err := snapshotClient.VolumeSnapshotContents().Delete(ctx, vscName, metav1.DeleteOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
@ -124,7 +160,8 @@ func DeleteVolumeSnapshotContentIfAny(ctx context.Context, snapshotClient snapsh
}
}
// EnsureDeleteVS asserts the existence of a VS by name, deletes it and waits for its disappearance and returns errors on any failure
// EnsureDeleteVS asserts the existence of a VS by name, deletes it and waits for its
// disappearance and returns errors on any failure.
func EnsureDeleteVS(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vsName string, vsNamespace string, timeout time.Duration) error {
err := snapshotClient.VolumeSnapshots(vsNamespace).Delete(ctx, vsName, metav1.DeleteOptions{})
@ -176,7 +213,8 @@ func RemoveVSCProtect(ctx context.Context, snapshotClient snapshotter.SnapshotV1
return err
}
// EnsureDeleteVSC asserts the existence of a VSC by name, deletes it and waits for its disappearance and returns errors on any failure
// EnsureDeleteVSC asserts the existence of a VSC by name, deletes it and waits for its
// disappearance and returns errors on any failure.
func EnsureDeleteVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vscName string, timeout time.Duration) error {
err := snapshotClient.VolumeSnapshotContents().Delete(ctx, vscName, metav1.DeleteOptions{})
@ -203,20 +241,34 @@ func EnsureDeleteVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1I
return nil
}
// DeleteVolumeSnapshotIfAny deletes a VS by name if it exists, and log an error when the deletion fails
func DeleteVolumeSnapshotIfAny(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface, vsName string, vsNamespace string, log logrus.FieldLogger) {
// DeleteVolumeSnapshotIfAny deletes a VS by name if it exists,
// and log an error when the deletion fails
func DeleteVolumeSnapshotIfAny(
ctx context.Context,
snapshotClient snapshotter.SnapshotV1Interface,
vsName string,
vsNamespace string,
log logrus.FieldLogger,
) {
err := snapshotClient.VolumeSnapshots(vsNamespace).Delete(ctx, vsName, metav1.DeleteOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
log.WithError(err).Debugf("Abort deleting volume snapshot, it doesn't exist %s/%s", vsNamespace, vsName)
log.WithError(err).Debugf(
"Abort deleting volume snapshot, it doesn't exist %s/%s",
vsNamespace, vsName)
} else {
log.WithError(err).Errorf("Failed to delete volume snapshot %s/%s", vsNamespace, vsName)
log.WithError(err).Errorf(
"Failed to delete volume snapshot %s/%s", vsNamespace, vsName)
}
}
}
func patchVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vsc *snapshotv1api.VolumeSnapshotContent, updateFunc func(*snapshotv1api.VolumeSnapshotContent)) (*snapshotv1api.VolumeSnapshotContent, error) {
func patchVSC(
ctx context.Context,
snapshotClient snapshotter.SnapshotV1Interface,
vsc *snapshotv1api.VolumeSnapshotContent,
updateFunc func(*snapshotv1api.VolumeSnapshotContent),
) (*snapshotv1api.VolumeSnapshotContent, error) {
origBytes, err := json.Marshal(vsc)
if err != nil {
return nil, errors.Wrap(err, "error marshaling original VSC")
@ -242,3 +294,493 @@ func patchVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interfac
return patched, nil
}
func GetVolumeSnapshotClass(
provisioner string,
backup *velerov1api.Backup,
pvc *corev1api.PersistentVolumeClaim,
log logrus.FieldLogger,
crClient crclient.Client,
) (*snapshotv1api.VolumeSnapshotClass, error) {
snapshotClasses := new(snapshotv1api.VolumeSnapshotClassList)
err := crClient.List(context.TODO(), snapshotClasses)
if err != nil {
return nil, errors.Wrap(err, "error listing VolumeSnapshotClass")
}
// If a snapshot class is set for provider in PVC annotations, use that
snapshotClass, err := GetVolumeSnapshotClassFromPVCAnnotationsForDriver(
pvc, provisioner, snapshotClasses,
)
if err != nil {
log.Debugf("Didn't find VolumeSnapshotClass from PVC annotations: %v", err)
}
if snapshotClass != nil {
return snapshotClass, nil
}
// If there is no annotation in PVC, attempt to fetch it from backup annotations
snapshotClass, err = GetVolumeSnapshotClassFromBackupAnnotationsForDriver(
backup, provisioner, snapshotClasses)
if err != nil {
log.Debugf("Didn't find VolumeSnapshotClass from Backup annotations: %v", err)
}
if snapshotClass != nil {
return snapshotClass, nil
}
// fallback to default behavior of fetching snapshot class based on label
snapshotClass, err = GetVolumeSnapshotClassForStorageClass(
provisioner, snapshotClasses)
if err != nil || snapshotClass == nil {
return nil, errors.Wrap(err, "error getting VolumeSnapshotClass")
}
return snapshotClass, nil
}
func GetVolumeSnapshotClassFromPVCAnnotationsForDriver(
pvc *corev1api.PersistentVolumeClaim,
provisioner string,
snapshotClasses *snapshotv1api.VolumeSnapshotClassList,
) (*snapshotv1api.VolumeSnapshotClass, error) {
annotationKey := velerov1api.VolumeSnapshotClassDriverPVCAnnotation
snapshotClassName, ok := pvc.ObjectMeta.Annotations[annotationKey]
if !ok {
return nil, nil
}
for _, sc := range snapshotClasses.Items {
if strings.EqualFold(snapshotClassName, sc.ObjectMeta.Name) {
if !strings.EqualFold(sc.Driver, provisioner) {
return nil, errors.Errorf(
"Incorrect VolumeSnapshotClass %s is not for driver %s",
sc.ObjectMeta.Name, provisioner,
)
}
return &sc, nil
}
}
return nil, errors.Errorf(
"No CSI VolumeSnapshotClass found with name %s for provisioner %s for PVC %s",
snapshotClassName, provisioner, pvc.Name,
)
}
// GetVolumeSnapshotClassFromAnnotationsForDriver returns a
// VolumeSnapshotClass for the supplied volume provisioner/driver
// name from the annotation of the backup.
func GetVolumeSnapshotClassFromBackupAnnotationsForDriver(
backup *velerov1api.Backup,
provisioner string,
snapshotClasses *snapshotv1api.VolumeSnapshotClassList,
) (*snapshotv1api.VolumeSnapshotClass, error) {
annotationKey := fmt.Sprintf(
"%s_%s",
velerov1api.VolumeSnapshotClassDriverBackupAnnotationPrefix,
strings.ToLower(provisioner),
)
snapshotClassName, ok := backup.ObjectMeta.Annotations[annotationKey]
if !ok {
return nil, nil
}
for _, sc := range snapshotClasses.Items {
if strings.EqualFold(snapshotClassName, sc.ObjectMeta.Name) {
if !strings.EqualFold(sc.Driver, provisioner) {
return nil, errors.Errorf(
"Incorrect VolumeSnapshotClass %s is not for driver %s for backup %s",
sc.ObjectMeta.Name, provisioner, backup.Name,
)
}
return &sc, nil
}
}
return nil, errors.Errorf(
"No CSI VolumeSnapshotClass found with name %s for driver %s for backup %s",
snapshotClassName, provisioner, backup.Name,
)
}
// GetVolumeSnapshotClassForStorageClass returns a VolumeSnapshotClass
// for the supplied volume provisioner/ driver name.
func GetVolumeSnapshotClassForStorageClass(
provisioner string,
snapshotClasses *snapshotv1api.VolumeSnapshotClassList,
) (*snapshotv1api.VolumeSnapshotClass, error) {
n := 0
var vsClass snapshotv1api.VolumeSnapshotClass
// We pick the VolumeSnapshotClass that matches the CSI driver name
// and has a 'velero.io/csi-volumesnapshot-class' label. This allows
// multiple VolumeSnapshotClasses for the same driver with different
// values for the other fields in the spec.
for _, sc := range snapshotClasses.Items {
_, hasLabelSelector := sc.Labels[velerov1api.VolumeSnapshotClassSelectorLabel]
if sc.Driver == provisioner {
n += 1
vsClass = sc
if hasLabelSelector {
return &sc, nil
}
}
}
// If there's only one volumesnapshotclass for the driver, return it.
if n == 1 {
return &vsClass, nil
}
return nil, fmt.Errorf(
`failed to get VolumeSnapshotClass for provisioner %s,
ensure that the desired VolumeSnapshot class has the %s label`,
provisioner, velerov1api.VolumeSnapshotClassSelectorLabel)
}
// IsVolumeSnapshotClassHasListerSecret returns whether a volumesnapshotclass has a snapshotlister secret
func IsVolumeSnapshotClassHasListerSecret(vc *snapshotv1api.VolumeSnapshotClass) bool {
// https://github.com/kubernetes-csi/external-snapshotter/blob/master/pkg/utils/util.go#L59-L60
// There is no release w/ these constants exported. Using the strings for now.
_, nameExists := vc.Annotations[velerov1api.PrefixedListSecretNameAnnotation]
_, nsExists := vc.Annotations[velerov1api.PrefixedListSecretNamespaceAnnotation]
return nameExists && nsExists
}
// IsVolumeSnapshotContentHasDeleteSecret returns whether a volumesnapshotcontent has a deletesnapshot secret
func IsVolumeSnapshotContentHasDeleteSecret(vsc *snapshotv1api.VolumeSnapshotContent) bool {
// https://github.com/kubernetes-csi/external-snapshotter/blob/master/pkg/utils/util.go#L56-L57
// use exported constants in the next release
_, nameExists := vsc.Annotations[velerov1api.PrefixedSecretNameAnnotation]
_, nsExists := vsc.Annotations[velerov1api.PrefixedSecretNamespaceAnnotation]
return nameExists && nsExists
}
// IsVolumeSnapshotHasVSCDeleteSecret returns whether a volumesnapshot should set the deletesnapshot secret
// for the static volumesnapshotcontent that is created on restore
func IsVolumeSnapshotHasVSCDeleteSecret(vs *snapshotv1api.VolumeSnapshot) bool {
_, nameExists := vs.Annotations[velerov1api.DeleteSecretNameAnnotation]
_, nsExists := vs.Annotations[velerov1api.DeleteSecretNamespaceAnnotation]
return nameExists && nsExists
}
// IsVolumeSnapshotExists returns whether a specific volumesnapshot object exists.
func IsVolumeSnapshotExists(
ns,
name string,
crClient crclient.Client,
) bool {
vs := new(snapshotv1api.VolumeSnapshot)
err := crClient.Get(
context.TODO(),
crclient.ObjectKey{Namespace: ns, Name: name},
vs,
)
return err == nil
}
func SetVolumeSnapshotContentDeletionPolicy(
vscName string,
crClient crclient.Client,
) error {
vsc := new(snapshotv1api.VolumeSnapshotContent)
if err := crClient.Get(context.TODO(), crclient.ObjectKey{Name: vscName}, vsc); err != nil {
return err
}
originVSC := vsc.DeepCopy()
vsc.Spec.DeletionPolicy = snapshotv1api.VolumeSnapshotContentDelete
return crClient.Patch(context.TODO(), vsc, crclient.MergeFrom(originVSC))
}
func CleanupVolumeSnapshot(
volSnap *snapshotv1api.VolumeSnapshot,
crClient crclient.Client,
log logrus.FieldLogger,
) {
log.Infof("Deleting Volumesnapshot %s/%s", volSnap.Namespace, volSnap.Name)
vs := new(snapshotv1api.VolumeSnapshot)
err := crClient.Get(
context.TODO(),
crclient.ObjectKey{Name: volSnap.Name, Namespace: volSnap.Namespace},
vs,
)
if err != nil {
log.Debugf("Failed to get volumesnapshot %s/%s", volSnap.Namespace, volSnap.Name)
return
}
if vs.Status != nil && vs.Status.BoundVolumeSnapshotContentName != nil {
// we patch the DeletionPolicy of the VolumeSnapshotContent to set it to Delete.
// This ensures that the volume snapshot in the storage provider is also deleted.
err := SetVolumeSnapshotContentDeletionPolicy(
*vs.Status.BoundVolumeSnapshotContentName,
crClient,
)
if err != nil {
log.Debugf("Failed to patch DeletionPolicy of volume snapshot %s/%s",
vs.Namespace, vs.Name)
}
}
err = crClient.Delete(context.TODO(), vs)
if err != nil {
log.Debugf("Failed to delete volumesnapshot %s/%s: %v", vs.Namespace, vs.Name, err)
} else {
log.Infof("Deleted volumesnapshot with volumesnapshotContent %s/%s",
vs.Namespace, vs.Name)
}
}
// DeleteVolumeSnapshot handles the VolumeSnapshot instance deletion.
func DeleteVolumeSnapshot(
vs snapshotv1api.VolumeSnapshot,
vsc snapshotv1api.VolumeSnapshotContent,
backup *velerov1api.Backup,
client crclient.Client,
logger logrus.FieldLogger,
) {
modifyVSCFlag := false
if vs.Status != nil &&
vs.Status.BoundVolumeSnapshotContentName != nil &&
len(*vs.Status.BoundVolumeSnapshotContentName) > 0 &&
vsc.Spec.DeletionPolicy == snapshotv1api.VolumeSnapshotContentDelete {
modifyVSCFlag = true
} else {
logger.Errorf("VolumeSnapshot %s/%s is not ready. This is not expected.",
vs.Namespace, vs.Name)
}
// Change VolumeSnapshotContent's DeletionPolicy to Retain before deleting VolumeSnapshot,
// because VolumeSnapshotContent will be deleted by deleting VolumeSnapshot, when
// DeletionPolicy is set to Delete, but Velero needs VSC for cleaning snapshot on cloud
// in backup deletion.
if modifyVSCFlag {
logger.Debugf("Patching VolumeSnapshotContent %s", vsc.Name)
originVSC := vsc.DeepCopy()
vsc.Spec.DeletionPolicy = snapshotv1api.VolumeSnapshotContentRetain
err := client.Patch(
context.Background(),
&vsc,
crclient.MergeFrom(originVSC),
)
if err != nil {
logger.Errorf(
"fail to modify VolumeSnapshotContent %s DeletionPolicy to Retain: %s",
vsc.Name, err.Error(),
)
return
}
defer func() {
logger.Debugf("Start to recreate VolumeSnapshotContent %s", vsc.Name)
err := recreateVolumeSnapshotContent(vsc, backup, client, logger)
if err != nil {
logger.Errorf(
"fail to recreate VolumeSnapshotContent %s: %s",
vsc.Name,
err.Error(),
)
}
}()
}
// Delete VolumeSnapshot from cluster
logger.Debugf("Deleting VolumeSnapshot %s/%s", vs.Namespace, vs.Name)
err := client.Delete(context.TODO(), &vs)
if err != nil {
logger.Errorf("fail to delete VolumeSnapshot %s/%s: %s",
vs.Namespace, vs.Name, err.Error())
}
}
// recreateVolumeSnapshotContent will delete then re-create VolumeSnapshotContent,
// because some parameter in VolumeSnapshotContent Spec is immutable,
// e.g. VolumeSnapshotRef and Source.
// Source is updated to let csi-controller thinks the VSC is statically
// provisioned with VS.
// Set VolumeSnapshotRef's UID to nil will let the csi-controller finds out
// the related VS is gone, then VSC can be deleted.
func recreateVolumeSnapshotContent(
vsc snapshotv1api.VolumeSnapshotContent,
backup *velerov1api.Backup,
client crclient.Client,
log logrus.FieldLogger,
) error {
// Read resource timeout from backup annotation, if not set, use default value.
timeout, err := time.ParseDuration(
backup.Annotations[velerov1api.ResourceTimeoutAnnotation])
if err != nil {
log.Warnf("fail to parse resource timeout annotation %s: %s",
backup.Annotations[velerov1api.ResourceTimeoutAnnotation], err.Error())
timeout = 10 * time.Minute
}
log.Debugf("resource timeout is set to %s", timeout.String())
interval := 1 * time.Second
if err := client.Delete(context.TODO(), &vsc); err != nil {
return errors.Wrapf(err, "fail to delete VolumeSnapshotContent: %s", vsc.Name)
}
// Check VolumeSnapshotContents is already deleted, before re-creating it.
err = wait.PollUntilContextTimeout(
context.Background(),
interval,
timeout,
true,
func(ctx context.Context) (bool, error) {
tmpVSC := new(snapshotv1api.VolumeSnapshotContent)
if err := client.Get(ctx, crclient.ObjectKeyFromObject(&vsc), tmpVSC); err != nil {
if apierrors.IsNotFound(err) {
return true, nil
}
return false, errors.Wrapf(
err,
fmt.Sprintf("failed to get VolumeSnapshotContent %s", vsc.Name),
)
}
return false, nil
},
)
if err != nil {
return errors.Wrapf(err, "fail to retrieve VolumeSnapshotContent %s info", vsc.Name)
}
// Make the VolumeSnapshotContent static
vsc.Spec.Source = snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: vsc.Status.SnapshotHandle,
}
// Set VolumeSnapshotRef to none exist one, because VolumeSnapshotContent
// validation webhook will check whether name and namespace are nil.
// external-snapshotter needs Source pointing to snapshot and VolumeSnapshot
// reference's UID to nil to determine the VolumeSnapshotContent is deletable.
vsc.Spec.VolumeSnapshotRef = corev1api.ObjectReference{
APIVersion: snapshotv1api.SchemeGroupVersion.String(),
Kind: "VolumeSnapshot",
Namespace: "ns-" + string(vsc.UID),
Name: "name-" + string(vsc.UID),
}
// ResourceVersion shouldn't exist for new creation.
vsc.ResourceVersion = ""
if err := client.Create(context.TODO(), &vsc); err != nil {
return errors.Wrapf(err, "fail to create VolumeSnapshotContent %s", vsc.Name)
}
return nil
}
// WaitUntilVSCHandleIsReady returns the VolumeSnapshotContent
// object associated with the volumesnapshot
func WaitUntilVSCHandleIsReady(
volSnap *snapshotv1api.VolumeSnapshot,
crClient crclient.Client,
log logrus.FieldLogger,
shouldWait bool,
csiSnapshotTimeout time.Duration,
) (*snapshotv1api.VolumeSnapshotContent, error) {
if !shouldWait {
if volSnap.Status == nil ||
volSnap.Status.BoundVolumeSnapshotContentName == nil {
// volumesnapshot hasn't been reconciled and we're
// not waiting for it.
return nil, nil
}
vsc := new(snapshotv1api.VolumeSnapshotContent)
err := crClient.Get(
context.TODO(),
crclient.ObjectKey{
Name: *volSnap.Status.BoundVolumeSnapshotContentName,
},
vsc,
)
if err != nil {
return nil,
errors.Wrap(err,
"error getting volume snapshot content from API")
}
return vsc, nil
}
// We'll wait 10m for the VSC to be reconciled polling
// every 5s unless backup's csiSnapshotTimeout is set
interval := 5 * time.Second
vsc := new(snapshotv1api.VolumeSnapshotContent)
err := wait.PollUntilContextTimeout(
context.Background(),
interval,
csiSnapshotTimeout,
true,
func(ctx context.Context) (bool, error) {
vs := new(snapshotv1api.VolumeSnapshot)
if err := crClient.Get(
ctx,
crclient.ObjectKeyFromObject(volSnap),
vs,
); err != nil {
return false,
errors.Wrapf(err, fmt.Sprintf(
"failed to get volumesnapshot %s/%s",
volSnap.Namespace, volSnap.Name),
)
}
if vs.Status == nil || vs.Status.BoundVolumeSnapshotContentName == nil {
log.Infof("Waiting for CSI driver to reconcile volumesnapshot %s/%s. Retrying in %ds",
volSnap.Namespace, volSnap.Name, interval/time.Second)
return false, nil
}
if err := crClient.Get(
ctx,
crclient.ObjectKey{
Name: *vs.Status.BoundVolumeSnapshotContentName,
},
vsc,
); err != nil {
return false,
errors.Wrapf(
err,
fmt.Sprintf("failed to get VolumeSnapshotContent %s for VolumeSnapshot %s/%s",
*vs.Status.BoundVolumeSnapshotContentName, vs.Namespace, vs.Name),
)
}
// we need to wait for the VolumeSnapshotContent
// to have a snapshot handle because during restore,
// we'll use that snapshot handle as the source for
// the VolumeSnapshotContent so it's statically
// bound to the existing snapshot.
if vsc.Status == nil ||
vsc.Status.SnapshotHandle == nil {
log.Infof(
"Waiting for VolumeSnapshotContents %s to have snapshot handle. Retrying in %ds",
vsc.Name, interval/time.Second)
if vsc.Status != nil &&
vsc.Status.Error != nil {
log.Warnf("VolumeSnapshotContent %s has error: %v",
vsc.Name, *vsc.Status.Error.Message)
}
return false, nil
}
return true, nil
},
)
if err != nil {
if err == wait.ErrorInterrupted(errors.New("timed out waiting for the condition")) {
if vsc != nil &&
vsc.Status != nil &&
vsc.Status.Error != nil {
log.Errorf(
"Timed out awaiting reconciliation of VolumeSnapshot, VolumeSnapshotContent %s has error: %v",
vsc.Name, *vsc.Status.Error.Message)
return nil,
errors.Errorf("CSI got timed out with error: %v",
*vsc.Status.Error.Message)
} else {
log.Errorf(
"Timed out awaiting reconciliation of volumesnapshot %s/%s",
volSnap.Namespace, volSnap.Name)
}
}
return nil, err
}
return vsc, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,11 @@ import (
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
storagev1api "k8s.io/api/storage/v1"
storagev1 "k8s.io/client-go/kubernetes/typed/storage/v1"
@ -358,3 +358,31 @@ func MakePodPVCAttachment(volumeName string, volumeMode *corev1api.PersistentVol
return volumeMounts, volumeDevices
}
func GetPVForPVC(
pvc *corev1api.PersistentVolumeClaim,
crClient crclient.Client,
) (*corev1api.PersistentVolume, error) {
if pvc.Spec.VolumeName == "" {
return nil, errors.Errorf("PVC %s/%s has no volume backing this claim",
pvc.Namespace, pvc.Name)
}
if pvc.Status.Phase != corev1api.ClaimBound {
// TODO: confirm if this PVC should be snapshotted if it has no PV bound
return nil,
errors.Errorf("PVC %s/%s is in phase %v and is not bound to a volume",
pvc.Namespace, pvc.Name, pvc.Status.Phase)
}
pv := &corev1api.PersistentVolume{}
err := crClient.Get(
context.TODO(),
crclient.ObjectKey{Name: pvc.Spec.VolumeName},
pv,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PV %s for PVC %s/%s",
pvc.Spec.VolumeName, pvc.Namespace, pvc.Name)
}
return pv, nil
}

View File

@ -29,6 +29,7 @@ import (
"k8s.io/client-go/kubernetes/fake"
corev1api "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
storagev1api "k8s.io/api/storage/v1"
clientTesting "k8s.io/client-go/testing"
@ -1119,3 +1120,139 @@ func TestIsPVCBound(t *testing.T) {
})
}
}
var (
csiStorageClass = "csi-hostpath-sc"
)
func TestGetPVForPVC(t *testing.T) {
boundPVC := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-csi-pvc",
Namespace: "default",
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{},
},
StorageClassName: &csiStorageClass,
VolumeName: "test-csi-7d28e566-ade7-4ed6-9e15-2e44d2fbcc08",
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimBound,
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Capacity: v1.ResourceList{},
},
}
matchingPV := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "test-csi-7d28e566-ade7-4ed6-9e15-2e44d2fbcc08",
},
Spec: v1.PersistentVolumeSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Capacity: v1.ResourceList{},
ClaimRef: &v1.ObjectReference{
Kind: "PersistentVolumeClaim",
Name: "test-csi-pvc",
Namespace: "default",
ResourceVersion: "1027",
UID: "7d28e566-ade7-4ed6-9e15-2e44d2fbcc08",
},
PersistentVolumeSource: v1.PersistentVolumeSource{
CSI: &v1.CSIPersistentVolumeSource{
Driver: "hostpath.csi.k8s.io",
FSType: "ext4",
VolumeAttributes: map[string]string{
"storage.kubernetes.io/csiProvisionerIdentity": "1582049697841-8081-hostpath.csi.k8s.io",
},
VolumeHandle: "e61f2b48-527a-11ea-b54f-cab6317018f1",
},
},
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
StorageClassName: csiStorageClass,
},
Status: v1.PersistentVolumeStatus{
Phase: v1.VolumeBound,
},
}
pvcWithNoVolumeName := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "no-vol-pvc",
Namespace: "default",
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{},
},
StorageClassName: &csiStorageClass,
},
Status: v1.PersistentVolumeClaimStatus{},
}
unboundPVC := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "unbound-pvc",
Namespace: "default",
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{},
},
StorageClassName: &csiStorageClass,
VolumeName: "test-csi-7d28e566-ade7-4ed6-9e15-2e44d2fbcc08",
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimPending,
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Capacity: v1.ResourceList{},
},
}
testCases := []struct {
name string
inPVC *v1.PersistentVolumeClaim
expectError bool
expectedPV *v1.PersistentVolume
}{
{
name: "should find PV matching the PVC",
inPVC: boundPVC,
expectError: false,
expectedPV: matchingPV,
},
{
name: "should fail to find PV for PVC with no volumeName",
inPVC: pvcWithNoVolumeName,
expectError: true,
expectedPV: nil,
},
{
name: "should fail to find PV for PVC not in bound phase",
inPVC: unboundPVC,
expectError: true,
expectedPV: nil,
},
}
objs := []runtime.Object{boundPVC, matchingPV, pvcWithNoVolumeName, unboundPVC}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualPV, actualError := GetPVForPVC(tc.inPVC, fakeClient)
if tc.expectError {
assert.NotNil(t, actualError, "Want error; Got nil error")
assert.Nilf(t, actualPV, "Want PV: nil; Got PV: %q", actualPV)
return
}
assert.Nilf(t, actualError, "Want: nil error; Got: %v", actualError)
assert.Equalf(t, actualPV.Name, tc.expectedPV.Name, "Want PV with name %q; Got PV with name %q", tc.expectedPV.Name, actualPV.Name)
})
}
}

View File

@ -19,6 +19,7 @@ package kube
import (
"context"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
@ -35,6 +36,8 @@ import (
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/uploader"
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
)
@ -303,3 +306,30 @@ func IsCRDReady(crd *unstructured.Unstructured) (bool, error) {
return false, fmt.Errorf("unable to handle CRD with version %s", ver)
}
}
// AddAnnotations adds the supplied key-values to the annotations on the object
func AddAnnotations(o *metav1.ObjectMeta, vals map[string]string) {
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
for k, v := range vals {
o.Annotations[k] = v
}
}
// AddLabels adds the supplied key-values to the labels on the object
func AddLabels(o *metav1.ObjectMeta, vals map[string]string) {
if o.Labels == nil {
o.Labels = make(map[string]string)
}
for k, v := range vals {
o.Labels[k] = label.GetValidName(v)
}
}
func HasBackupLabel(o *metav1.ObjectMeta, backupName string) bool {
if o.Labels == nil || len(strings.TrimSpace(backupName)) == 0 {
return false
}
return o.Labels[velerov1api.BackupNameLabel] == label.GetValidName(backupName)
}

View File

@ -481,3 +481,187 @@ func TestSinglePathMatch(t *testing.T) {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "expected one matching path")
}
func TestAddAnnotations(t *testing.T) {
annotationValues := map[string]string{
"k1": "v1",
"k2": "v2",
"k3": "v3",
"k4": "v4",
"k5": "v5",
}
testCases := []struct {
name string
o metav1.ObjectMeta
toAdd map[string]string
}{
{
name: "should create a new annotation map when annotation is nil",
o: metav1.ObjectMeta{
Annotations: nil,
},
toAdd: annotationValues,
},
{
name: "should add all supplied annotations into empty annotation",
o: metav1.ObjectMeta{
Annotations: map[string]string{},
},
toAdd: annotationValues,
},
{
name: "should add all supplied annotations to existing annotation",
o: metav1.ObjectMeta{
Annotations: map[string]string{
"k100": "v100",
"k200": "v200",
"k300": "v300",
},
},
toAdd: annotationValues,
},
{
name: "should overwrite some existing annotations",
o: metav1.ObjectMeta{
Annotations: map[string]string{
"k100": "v100",
"k2": "v200",
"k300": "v300",
},
},
toAdd: annotationValues,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
AddAnnotations(&tc.o, tc.toAdd)
for k, v := range tc.toAdd {
actual, exists := tc.o.Annotations[k]
assert.True(t, exists)
assert.Equal(t, v, actual)
}
})
}
}
func TestAddLabels(t *testing.T) {
labelValues := map[string]string{
"l1": "v1",
"l2": "v2",
"l3": "v3",
"l4": "v4",
"l5": "v5",
}
testCases := []struct {
name string
o metav1.ObjectMeta
toAdd map[string]string
}{
{
name: "should create a new labels map when labels is nil",
o: metav1.ObjectMeta{
Labels: nil,
},
toAdd: labelValues,
},
{
name: "should add all supplied labels into empty labels",
o: metav1.ObjectMeta{
Labels: map[string]string{},
},
toAdd: labelValues,
},
{
name: "should add all supplied labels to existing labels",
o: metav1.ObjectMeta{
Labels: map[string]string{
"l100": "v100",
"l200": "v200",
"l300": "v300",
},
},
toAdd: labelValues,
},
{
name: "should overwrite some existing labels",
o: metav1.ObjectMeta{
Labels: map[string]string{
"l100": "v100",
"l2": "v200",
"l300": "v300",
},
},
toAdd: labelValues,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
AddLabels(&tc.o, tc.toAdd)
for k, v := range tc.toAdd {
actual, exists := tc.o.Labels[k]
assert.True(t, exists)
assert.Equal(t, v, actual)
}
})
}
}
func TestHasBackupLabel(t *testing.T) {
testCases := []struct {
name string
o metav1.ObjectMeta
backupName string
expected bool
}{
{
name: "object has no labels",
o: metav1.ObjectMeta{},
expected: false,
},
{
name: "object has no velero backup label",
backupName: "csi-b1",
o: metav1.ObjectMeta{
Labels: map[string]string{
"l100": "v100",
"l2": "v200",
"l300": "v300",
},
},
expected: false,
},
{
name: "object has velero backup label but value not equal to backup name",
backupName: "csi-b1",
o: metav1.ObjectMeta{
Labels: map[string]string{
"velero.io/backup-name": "does-not-match",
"l100": "v100",
"l2": "v200",
"l300": "v300",
},
},
expected: false,
},
{
name: "object has backup label with matching backup name value",
backupName: "does-match",
o: metav1.ObjectMeta{
Labels: map[string]string{
"velero.io/backup-name": "does-match",
"l100": "v100",
"l2": "v200",
"l300": "v300",
},
},
expected: true,
},
}
for _, tc := range testCases {
actual := HasBackupLabel(&tc.o, tc.backupName)
assert.Equal(t, tc.expected, actual)
}
}

View File

@ -17,12 +17,16 @@ limitations under the License.
package podvolume
import (
"context"
"strings"
"github.com/pkg/errors"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/util"
)
// GetVolumesByPod returns a list of volume names to backup for the provided pod.
@ -62,7 +66,7 @@ func GetVolumesByPod(pod *corev1api.Pod, defaultVolumesToFsBackup, backupExclude
continue
}
// don't backup volumes that are included in the exclude list.
if contains(volsToExclude, pv.Name) {
if util.Contains(volsToExclude, pv.Name) {
optedOutVolumes = append(optedOutVolumes, pv.Name)
continue
}
@ -101,11 +105,58 @@ func getVolumesToExclude(obj metav1.Object) []string {
return strings.Split(annotations[velerov1api.VolumesToExcludeAnnotation], ",")
}
func contains(list []string, k string) bool {
for _, i := range list {
if i == k {
return true
func IsPVCDefaultToFSBackup(pvcNamespace, pvcName string, crClient crclient.Client, defaultVolumesToFsBackup bool) (bool, error) {
pods, err := getPodsUsingPVC(pvcNamespace, pvcName, crClient)
if err != nil {
return false, errors.WithStack(err)
}
for index := range pods {
vols, _ := GetVolumesByPod(&pods[index], defaultVolumesToFsBackup, false)
if len(vols) > 0 {
volName, err := getPodVolumeNameForPVC(pods[index], pvcName)
if err != nil {
return false, err
}
if util.Contains(vols, volName) {
return true, nil
}
}
}
return false
return false, nil
}
func getPodVolumeNameForPVC(pod corev1api.Pod, pvcName string) (string, error) {
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == pvcName {
return v.Name, nil
}
}
return "", errors.Errorf("Pod %s/%s does not use PVC %s/%s", pod.Namespace, pod.Name, pod.Namespace, pvcName)
}
func getPodsUsingPVC(
pvcNamespace, pvcName string,
crClient crclient.Client,
) ([]corev1api.Pod, error) {
podsUsingPVC := []corev1api.Pod{}
podList := new(corev1api.PodList)
if err := crClient.List(
context.TODO(),
podList,
&crclient.ListOptions{Namespace: pvcNamespace},
); err != nil {
return nil, err
}
for _, p := range podList.Items {
for _, v := range p.Spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == pvcName {
podsUsingPVC = append(podsUsingPVC, p)
}
}
}
return podsUsingPVC, nil
}

View File

@ -22,9 +22,12 @@ import (
"github.com/stretchr/testify/assert"
corev1api "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestGetVolumesToBackup(t *testing.T) {
@ -378,3 +381,414 @@ func TestGetVolumesByPod(t *testing.T) {
})
}
}
func TestIsPVCDefaultToFSBackup(t *testing.T) {
objs := []runtime.Object{
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod3",
Namespace: "default",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome-pod-1",
Namespace: "awesome-ns",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "awesome-csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "awesome-pod-2",
Namespace: "awesome-ns",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "awesome-csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "uploader-ns",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "uploader-ns",
Annotations: map[string]string{
"backup.velero.io/backup-volumes": "csi-vol1",
},
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
inPVCNamespace string
inPVCName string
expectedIsFSUploaderUsed bool
defaultVolumesToFSBackup bool
}{
{
name: "2 pods using PVC, 1 pod using uploader",
inPVCNamespace: "default",
inPVCName: "csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, 2 pods using uploader",
inPVCNamespace: "uploader-ns",
inPVCName: "csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, 0 pods using uploader",
inPVCNamespace: "awesome-ns",
inPVCName: "awesome-csi-pvc1",
expectedIsFSUploaderUsed: false,
defaultVolumesToFSBackup: false,
},
{
name: "0 pods using PVC",
inPVCNamespace: "default",
inPVCName: "does-not-exist",
expectedIsFSUploaderUsed: false,
defaultVolumesToFSBackup: false,
},
{
name: "2 pods using PVC, using uploader by default",
inPVCNamespace: "awesome-ns",
inPVCName: "awesome-csi-pvc1",
expectedIsFSUploaderUsed: true,
defaultVolumesToFSBackup: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualIsFSUploaderUsed, _ := IsPVCDefaultToFSBackup(tc.inPVCNamespace, tc.inPVCName, fakeClient, tc.defaultVolumesToFSBackup)
assert.Equal(t, tc.expectedIsFSUploaderUsed, actualIsFSUploaderUsed)
})
}
}
func TestGetPodVolumeNameForPVC(t *testing.T) {
testCases := []struct {
name string
pod v1.Pod
pvcName string
expectError bool
expectedVolumeName string
}{
{
name: "should get volume name for pod with multuple PVCs",
pod: v1.Pod{
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
{
Name: "csi-vol2",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc2",
},
},
},
{
Name: "csi-vol3",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc3",
},
},
},
},
},
},
pvcName: "csi-pvc2",
expectedVolumeName: "csi-vol2",
expectError: false,
},
{
name: "should get volume name from pod using exactly one PVC",
pod: v1.Pod{
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
pvcName: "csi-pvc1",
expectedVolumeName: "csi-vol1",
expectError: false,
},
{
name: "should return error for pod with no PVCs",
pod: v1.Pod{
Spec: v1.PodSpec{},
},
pvcName: "csi-pvc2",
expectError: true,
},
{
name: "should return error for pod with no matching PVC",
pod: v1.Pod{
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
pvcName: "mismatch-pvc",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualVolumeName, err := getPodVolumeNameForPVC(tc.pod, tc.pvcName)
if tc.expectError && err == nil {
assert.NotNil(t, err, "Want error; Got nil error")
return
}
assert.Equalf(t, tc.expectedVolumeName, actualVolumeName, "unexpected podVolumename returned. Want %s; Got %s", tc.expectedVolumeName, actualVolumeName)
})
}
}
func TestGetPodsUsingPVC(t *testing.T) {
objs := []runtime.Object{
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "default",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod3",
Namespace: "default",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
},
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "awesome-ns",
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: "csi-vol1",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "csi-pvc1",
},
},
},
},
},
},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objs...)
testCases := []struct {
name string
pvcNamespace string
pvcName string
expectedPodCount int
}{
{
name: "should find exactly 2 pods using the PVC",
pvcNamespace: "default",
pvcName: "csi-pvc1",
expectedPodCount: 2,
},
{
name: "should find exactly 1 pod using the PVC",
pvcNamespace: "awesome-ns",
pvcName: "csi-pvc1",
expectedPodCount: 1,
},
{
name: "should find 0 pods using the PVC",
pvcNamespace: "default",
pvcName: "unused-pvc",
expectedPodCount: 0,
},
{
name: "should find 0 pods in non-existent namespace",
pvcNamespace: "does-not-exist",
pvcName: "csi-pvc1",
expectedPodCount: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualPods, err := getPodsUsingPVC(tc.pvcNamespace, tc.pvcName, fakeClient)
assert.Nilf(t, err, "Want error=nil; Got error=%v", err)
assert.Lenf(t, actualPods, tc.expectedPodCount, "unexpected number of pods in result; Want: %d; Got: %d", tc.expectedPodCount, len(actualPods))
})
}
}

View File

@ -1,3 +1,19 @@
/*
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 util
import (

26
pkg/util/util.go Normal file
View File

@ -0,0 +1,26 @@
/*
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 util
func Contains(slice []string, key string) bool {
for _, i := range slice {
if i == key {
return true
}
}
return false
}

64
pkg/util/util_test.go Normal file
View File

@ -0,0 +1,64 @@
/*
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 util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestContains(t *testing.T) {
testCases := []struct {
name string
inSlice []string
inKey string
expectedResult bool
}{
{
name: "should find the key",
inSlice: []string{"key1", "key2", "key3", "key4", "key5"},
inKey: "key3",
expectedResult: true,
},
{
name: "should not find the key in non-empty slice",
inSlice: []string{"key1", "key2", "key3", "key4", "key5"},
inKey: "key300",
expectedResult: false,
},
{
name: "should not find key in empty slice",
inSlice: []string{},
inKey: "key300",
expectedResult: false,
},
{
name: "should not find key in nil slice",
inSlice: nil,
inKey: "key300",
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualResult := Contains(tc.inSlice, tc.inKey)
assert.Equal(t, tc.expectedResult, actualResult)
})
}
}

View File

@ -26,6 +26,7 @@ import (
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
. "github.com/vmware-tanzu/velero/test/e2e/test"
. "github.com/vmware-tanzu/velero/test/util/k8s"
)
@ -57,9 +58,9 @@ func (e *ExcludeFromBackup) Init() error {
*e.NSIncluded = append(*e.NSIncluded, createNSName)
}
e.labels = map[string]string{
"velero.io/exclude-from-backup": "true",
velerov1api.ExcludeFromBackupLabel: "true",
}
e.labelSelector = "velero.io/exclude-from-backup"
e.labelSelector = velerov1api.ExcludeFromBackupLabel
e.BackupArgs = []string{
"create", "--namespace", e.VeleroCfg.VeleroNamespace, "backup", e.BackupName,
@ -82,7 +83,7 @@ func (e *ExcludeFromBackup) CreateResources() error {
"meaningless-label-resource-to-include": "true",
}
label2 := map[string]string{
"velero.io/exclude-from-backup": "false",
velerov1api.ExcludeFromBackupLabel: "false",
}
fmt.Printf("Creating resources in namespace ...%s\n", namespace)
if err := CreateNamespace(e.Ctx, e.Client, namespace); err != nil {