Merge CSI plugin code.
Signed-off-by: Xun Jiang <blackpigletbruce@gmail.com>pull/7609/head
parent
63fe9f1f1f
commit
31e140919a
|
@ -0,0 +1 @@
|
|||
Merge CSI plugin code into Velero.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
package actions
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue