mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-07 05:46:37 +00:00
move pvRestorer and tests to their own files
Signed-off-by: Steve Kriss <krisss@vmware.com>
This commit is contained in:
171
pkg/restore/pv_restorer.go
Normal file
171
pkg/restore/pv_restorer.go
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2019 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 restore
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
listers "github.com/heptio/velero/pkg/generated/listers/velero/v1"
|
||||
"github.com/heptio/velero/pkg/util/boolptr"
|
||||
"github.com/heptio/velero/pkg/volume"
|
||||
)
|
||||
|
||||
type PVRestorer interface {
|
||||
executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
type pvRestorer struct {
|
||||
logger logrus.FieldLogger
|
||||
backup *api.Backup
|
||||
snapshotVolumes *bool
|
||||
restorePVs *bool
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
blockStoreGetter BlockStoreGetter
|
||||
snapshotLocationLister listers.VolumeSnapshotLocationLister
|
||||
}
|
||||
|
||||
func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
pvName := obj.GetName()
|
||||
if pvName == "" {
|
||||
return nil, errors.New("PersistentVolume is missing its name")
|
||||
}
|
||||
|
||||
// It's simpler to just access the spec through the unstructured object than to convert
|
||||
// to structured and back here, especially since the SetVolumeID(...) call below needs
|
||||
// the unstructured representation (and does a conversion internally).
|
||||
res, ok := obj.Object["spec"]
|
||||
if !ok {
|
||||
return nil, errors.New("spec not found")
|
||||
}
|
||||
spec, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Errorf("spec was of type %T, expected map[string]interface{}", res)
|
||||
}
|
||||
|
||||
delete(spec, "claimRef")
|
||||
|
||||
if boolptr.IsSetToFalse(r.snapshotVolumes) {
|
||||
// The backup had snapshots disabled, so we can return early
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
if boolptr.IsSetToFalse(r.restorePVs) {
|
||||
// The restore has pv restores disabled, so we can return early
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
log := r.logger.WithFields(logrus.Fields{"persistentVolume": pvName})
|
||||
|
||||
snapshotInfo, err := getSnapshotInfo(pvName, r.backup, r.volumeSnapshots, r.snapshotLocationLister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if snapshotInfo == nil {
|
||||
log.Infof("No snapshot found for persistent volume")
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
blockStore, err := r.blockStoreGetter.GetBlockStore(snapshotInfo.location.Spec.Provider)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := blockStore.Init(snapshotInfo.location.Spec.Config); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
volumeID, err := blockStore.CreateVolumeFromSnapshot(snapshotInfo.providerSnapshotID, snapshotInfo.volumeType, snapshotInfo.volumeAZ, snapshotInfo.volumeIOPS)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
log.WithField("providerSnapshotID", snapshotInfo.providerSnapshotID).Info("successfully restored persistent volume from snapshot")
|
||||
|
||||
updated1, err := blockStore.SetVolumeID(obj, volumeID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
updated2, ok := updated1.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", updated1)
|
||||
}
|
||||
return updated2, nil
|
||||
}
|
||||
|
||||
type snapshotInfo struct {
|
||||
providerSnapshotID string
|
||||
volumeType string
|
||||
volumeAZ string
|
||||
volumeIOPS *int64
|
||||
location *api.VolumeSnapshotLocation
|
||||
}
|
||||
|
||||
func getSnapshotInfo(pvName string, backup *api.Backup, volumeSnapshots []*volume.Snapshot, snapshotLocationLister listers.VolumeSnapshotLocationLister) (*snapshotInfo, error) {
|
||||
// pre-v0.10 backup
|
||||
if backup.Status.VolumeBackups != nil {
|
||||
volumeBackup := backup.Status.VolumeBackups[pvName]
|
||||
if volumeBackup == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
locations, err := snapshotLocationLister.VolumeSnapshotLocations(backup.Namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if len(locations) != 1 {
|
||||
return nil, errors.Errorf("unable to restore pre-v0.10 volume snapshot because exactly one volume snapshot location must exist, got %d", len(locations))
|
||||
}
|
||||
|
||||
return &snapshotInfo{
|
||||
providerSnapshotID: volumeBackup.SnapshotID,
|
||||
volumeType: volumeBackup.Type,
|
||||
volumeAZ: volumeBackup.AvailabilityZone,
|
||||
volumeIOPS: volumeBackup.Iops,
|
||||
location: locations[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// v0.10+ backup
|
||||
var pvSnapshot *volume.Snapshot
|
||||
for _, snapshot := range volumeSnapshots {
|
||||
if snapshot.Spec.PersistentVolumeName == pvName {
|
||||
pvSnapshot = snapshot
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pvSnapshot == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
loc, err := snapshotLocationLister.VolumeSnapshotLocations(backup.Namespace).Get(pvSnapshot.Spec.Location)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &snapshotInfo{
|
||||
providerSnapshotID: pvSnapshot.Status.ProviderSnapshotID,
|
||||
volumeType: pvSnapshot.Spec.VolumeType,
|
||||
volumeAZ: pvSnapshot.Spec.VolumeAZ,
|
||||
volumeIOPS: pvSnapshot.Spec.VolumeIOPS,
|
||||
location: loc,
|
||||
}, nil
|
||||
}
|
||||
291
pkg/restore/pv_restorer_test.go
Normal file
291
pkg/restore/pv_restorer_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
Copyright 2019 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 restore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
"github.com/heptio/velero/pkg/cloudprovider"
|
||||
cloudprovidermocks "github.com/heptio/velero/pkg/cloudprovider/mocks"
|
||||
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
|
||||
informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
|
||||
velerotest "github.com/heptio/velero/pkg/util/test"
|
||||
"github.com/heptio/velero/pkg/volume"
|
||||
)
|
||||
|
||||
func TestExecutePVAction_NoSnapshotRestores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *unstructured.Unstructured
|
||||
restore *api.Restore
|
||||
backup *api.Backup
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
locations []*api.VolumeSnapshotLocation
|
||||
expectedErr bool
|
||||
expectedRes *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "no name should error",
|
||||
obj: NewTestUnstructured().WithMetadata().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "no spec should error",
|
||||
obj: NewTestUnstructured().WithName("pv-1").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "ensure spec.claimRef is deleted",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
expectedRes: NewTestUnstructured().WithAnnotations("a", "b").WithName("pv-1").WithSpec("someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "ensure spec.storageClassName is retained",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
expectedRes: NewTestUnstructured().WithAnnotations("a", "b").WithName("pv-1").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "if backup.spec.snapshotVolumes is false, ignore restore.spec.restorePVs and return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "storageClassName", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).WithSnapshotVolumes(false).Backup,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "restore.spec.restorePVs=false, return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("pv-1", "loc-1", "gp", "az-1", "snap-1", 1000),
|
||||
},
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "backup.status.volumeBackups non-nil and no entry for PV: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").WithSnapshot("non-matching-pv", "snap").Backup,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "backup.status.volumeBackups has entry for PV, >1 VSLs configured: return error",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").WithSnapshot("pv-1", "snap").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "volumeSnapshots is empty: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{},
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "volumeSnapshots doesn't have a snapshot for PV: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("non-matching-pv-1", "loc-1", "type-1", "az-1", "snap-1", 1),
|
||||
newSnapshot("non-matching-pv-2", "loc-2", "type-2", "az-2", "snap-2", 2),
|
||||
},
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
snapshotLocationInformer = informers.NewSharedInformerFactory(client, 0).Velero().V1().VolumeSnapshotLocations()
|
||||
)
|
||||
|
||||
r := &pvRestorer{
|
||||
logger: velerotest.NewLogger(),
|
||||
restorePVs: tc.restore.Spec.RestorePVs,
|
||||
snapshotLocationLister: snapshotLocationInformer.Lister(),
|
||||
}
|
||||
if tc.backup != nil {
|
||||
r.backup = tc.backup
|
||||
r.snapshotVolumes = tc.backup.Spec.SnapshotVolumes
|
||||
}
|
||||
|
||||
for _, loc := range tc.locations {
|
||||
require.NoError(t, snapshotLocationInformer.Informer().GetStore().Add(loc))
|
||||
}
|
||||
|
||||
res, err := r.executePVAction(tc.obj)
|
||||
switch tc.expectedErr {
|
||||
case true:
|
||||
assert.Nil(t, res)
|
||||
assert.NotNil(t, err)
|
||||
case false:
|
||||
assert.Equal(t, tc.expectedRes, res)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutePVAction_SnapshotRestores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *unstructured.Unstructured
|
||||
restore *api.Restore
|
||||
backup *api.Backup
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
locations []*api.VolumeSnapshotLocation
|
||||
expectedProvider string
|
||||
expectedSnapshotID string
|
||||
expectedVolumeType string
|
||||
expectedVolumeAZ string
|
||||
expectedVolumeIOPS *int64
|
||||
expectedSnapshot *volume.Snapshot
|
||||
}{
|
||||
{
|
||||
name: "pre-v0.10 backup with .status.volumeBackups with entry for PV and single VSL executes restore",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").
|
||||
WithVolumeBackupInfo("pv-1", "snap-1", "type-1", "az-1", int64Ptr(1)).
|
||||
WithVolumeBackupInfo("pv-2", "snap-2", "type-2", "az-2", int64Ptr(2)).
|
||||
Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").WithProvider("provider-1").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedProvider: "provider-1",
|
||||
expectedSnapshotID: "snap-1",
|
||||
expectedVolumeType: "type-1",
|
||||
expectedVolumeAZ: "az-1",
|
||||
expectedVolumeIOPS: int64Ptr(1),
|
||||
},
|
||||
{
|
||||
name: "v0.10+ backup with a matching volume.Snapshot for PV executes restore",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").WithProvider("provider-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").WithProvider("provider-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("pv-1", "loc-1", "type-1", "az-1", "snap-1", 1),
|
||||
newSnapshot("pv-2", "loc-2", "type-2", "az-2", "snap-2", 2),
|
||||
},
|
||||
expectedProvider: "provider-1",
|
||||
expectedSnapshotID: "snap-1",
|
||||
expectedVolumeType: "type-1",
|
||||
expectedVolumeAZ: "az-1",
|
||||
expectedVolumeIOPS: int64Ptr(1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
blockStore = new(cloudprovidermocks.BlockStore)
|
||||
blockStoreGetter = providerToBlockStoreMap(map[string]cloudprovider.BlockStore{
|
||||
tc.expectedProvider: blockStore,
|
||||
})
|
||||
locationsInformer = informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0).Velero().V1().VolumeSnapshotLocations()
|
||||
)
|
||||
|
||||
for _, loc := range tc.locations {
|
||||
require.NoError(t, locationsInformer.Informer().GetStore().Add(loc))
|
||||
}
|
||||
|
||||
r := &pvRestorer{
|
||||
logger: velerotest.NewLogger(),
|
||||
backup: tc.backup,
|
||||
volumeSnapshots: tc.volumeSnapshots,
|
||||
snapshotLocationLister: locationsInformer.Lister(),
|
||||
blockStoreGetter: blockStoreGetter,
|
||||
}
|
||||
|
||||
blockStore.On("Init", mock.Anything).Return(nil)
|
||||
blockStore.On("CreateVolumeFromSnapshot", tc.expectedSnapshotID, tc.expectedVolumeType, tc.expectedVolumeAZ, tc.expectedVolumeIOPS).Return("volume-1", nil)
|
||||
blockStore.On("SetVolumeID", tc.obj, "volume-1").Return(tc.obj, nil)
|
||||
|
||||
_, err := r.executePVAction(tc.obj)
|
||||
assert.NoError(t, err)
|
||||
|
||||
blockStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type providerToBlockStoreMap map[string]cloudprovider.BlockStore
|
||||
|
||||
func (g providerToBlockStoreMap) GetBlockStore(provider string) (cloudprovider.BlockStore, error) {
|
||||
if bs, ok := g[provider]; !ok {
|
||||
return nil, errors.New("block store not found for provider")
|
||||
} else {
|
||||
return bs, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newSnapshot(pvName, location, volumeType, volumeAZ, snapshotID string, volumeIOPS int64) *volume.Snapshot {
|
||||
return &volume.Snapshot{
|
||||
Spec: volume.SnapshotSpec{
|
||||
PersistentVolumeName: pvName,
|
||||
Location: location,
|
||||
VolumeType: volumeType,
|
||||
VolumeAZ: volumeAZ,
|
||||
VolumeIOPS: &volumeIOPS,
|
||||
},
|
||||
Status: volume.SnapshotStatus{
|
||||
ProviderSnapshotID: snapshotID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func int64Ptr(val int) *int64 {
|
||||
r := int64(val)
|
||||
return &r
|
||||
}
|
||||
@@ -53,7 +53,6 @@ import (
|
||||
listers "github.com/heptio/velero/pkg/generated/listers/velero/v1"
|
||||
"github.com/heptio/velero/pkg/kuberesource"
|
||||
"github.com/heptio/velero/pkg/restic"
|
||||
"github.com/heptio/velero/pkg/util/boolptr"
|
||||
"github.com/heptio/velero/pkg/util/collections"
|
||||
"github.com/heptio/velero/pkg/util/filesystem"
|
||||
"github.com/heptio/velero/pkg/util/kube"
|
||||
@@ -1037,148 +1036,6 @@ func waitForReady(
|
||||
}
|
||||
}
|
||||
|
||||
type PVRestorer interface {
|
||||
executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
type pvRestorer struct {
|
||||
logger logrus.FieldLogger
|
||||
backup *api.Backup
|
||||
snapshotVolumes *bool
|
||||
restorePVs *bool
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
blockStoreGetter BlockStoreGetter
|
||||
snapshotLocationLister listers.VolumeSnapshotLocationLister
|
||||
}
|
||||
|
||||
type snapshotInfo struct {
|
||||
providerSnapshotID string
|
||||
volumeType string
|
||||
volumeAZ string
|
||||
volumeIOPS *int64
|
||||
location *api.VolumeSnapshotLocation
|
||||
}
|
||||
|
||||
func getSnapshotInfo(pvName string, backup *api.Backup, volumeSnapshots []*volume.Snapshot, snapshotLocationLister listers.VolumeSnapshotLocationLister) (*snapshotInfo, error) {
|
||||
// pre-v0.10 backup
|
||||
if backup.Status.VolumeBackups != nil {
|
||||
volumeBackup := backup.Status.VolumeBackups[pvName]
|
||||
if volumeBackup == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
locations, err := snapshotLocationLister.VolumeSnapshotLocations(backup.Namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if len(locations) != 1 {
|
||||
return nil, errors.Errorf("unable to restore pre-v0.10 volume snapshot because exactly one volume snapshot location must exist, got %d", len(locations))
|
||||
}
|
||||
|
||||
return &snapshotInfo{
|
||||
providerSnapshotID: volumeBackup.SnapshotID,
|
||||
volumeType: volumeBackup.Type,
|
||||
volumeAZ: volumeBackup.AvailabilityZone,
|
||||
volumeIOPS: volumeBackup.Iops,
|
||||
location: locations[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// v0.10+ backup
|
||||
var pvSnapshot *volume.Snapshot
|
||||
for _, snapshot := range volumeSnapshots {
|
||||
if snapshot.Spec.PersistentVolumeName == pvName {
|
||||
pvSnapshot = snapshot
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pvSnapshot == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
loc, err := snapshotLocationLister.VolumeSnapshotLocations(backup.Namespace).Get(pvSnapshot.Spec.Location)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &snapshotInfo{
|
||||
providerSnapshotID: pvSnapshot.Status.ProviderSnapshotID,
|
||||
volumeType: pvSnapshot.Spec.VolumeType,
|
||||
volumeAZ: pvSnapshot.Spec.VolumeAZ,
|
||||
volumeIOPS: pvSnapshot.Spec.VolumeIOPS,
|
||||
location: loc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
pvName := obj.GetName()
|
||||
if pvName == "" {
|
||||
return nil, errors.New("PersistentVolume is missing its name")
|
||||
}
|
||||
|
||||
// It's simpler to just access the spec through the unstructured object than to convert
|
||||
// to structured and back here, especially since the SetVolumeID(...) call below needs
|
||||
// the unstructured representation (and does a conversion internally).
|
||||
res, ok := obj.Object["spec"]
|
||||
if !ok {
|
||||
return nil, errors.New("spec not found")
|
||||
}
|
||||
spec, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Errorf("spec was of type %T, expected map[string]interface{}", res)
|
||||
}
|
||||
|
||||
delete(spec, "claimRef")
|
||||
|
||||
if boolptr.IsSetToFalse(r.snapshotVolumes) {
|
||||
// The backup had snapshots disabled, so we can return early
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
if boolptr.IsSetToFalse(r.restorePVs) {
|
||||
// The restore has pv restores disabled, so we can return early
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
log := r.logger.WithFields(logrus.Fields{"persistentVolume": pvName})
|
||||
|
||||
snapshotInfo, err := getSnapshotInfo(pvName, r.backup, r.volumeSnapshots, r.snapshotLocationLister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if snapshotInfo == nil {
|
||||
log.Infof("No snapshot found for persistent volume")
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
blockStore, err := r.blockStoreGetter.GetBlockStore(snapshotInfo.location.Spec.Provider)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := blockStore.Init(snapshotInfo.location.Spec.Config); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
volumeID, err := blockStore.CreateVolumeFromSnapshot(snapshotInfo.providerSnapshotID, snapshotInfo.volumeType, snapshotInfo.volumeAZ, snapshotInfo.volumeIOPS)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
log.WithField("providerSnapshotID", snapshotInfo.providerSnapshotID).Info("successfully restored persistent volume from snapshot")
|
||||
|
||||
updated1, err := blockStore.SetVolumeID(obj, volumeID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
updated2, ok := updated1.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", updated1)
|
||||
}
|
||||
return updated2, nil
|
||||
}
|
||||
|
||||
func isPVReady(obj runtime.Unstructured) bool {
|
||||
phase, _, _ := unstructured.NestedString(obj.UnstructuredContent(), "status", "phase")
|
||||
return phase == string(v1.VolumeAvailable)
|
||||
|
||||
@@ -40,7 +40,6 @@ import (
|
||||
|
||||
api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
"github.com/heptio/velero/pkg/cloudprovider"
|
||||
cloudprovidermocks "github.com/heptio/velero/pkg/cloudprovider/mocks"
|
||||
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
|
||||
informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
|
||||
"github.com/heptio/velero/pkg/kuberesource"
|
||||
@@ -1305,262 +1304,6 @@ func TestIsCompleted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newSnapshot(pvName, location, volumeType, volumeAZ, snapshotID string, volumeIOPS int64) *volume.Snapshot {
|
||||
return &volume.Snapshot{
|
||||
Spec: volume.SnapshotSpec{
|
||||
PersistentVolumeName: pvName,
|
||||
Location: location,
|
||||
VolumeType: volumeType,
|
||||
VolumeAZ: volumeAZ,
|
||||
VolumeIOPS: &volumeIOPS,
|
||||
},
|
||||
Status: volume.SnapshotStatus{
|
||||
ProviderSnapshotID: snapshotID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutePVAction_NoSnapshotRestores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *unstructured.Unstructured
|
||||
restore *api.Restore
|
||||
backup *api.Backup
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
locations []*api.VolumeSnapshotLocation
|
||||
expectedErr bool
|
||||
expectedRes *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "no name should error",
|
||||
obj: NewTestUnstructured().WithMetadata().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "no spec should error",
|
||||
obj: NewTestUnstructured().WithName("pv-1").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "ensure spec.claimRef is deleted",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
expectedRes: NewTestUnstructured().WithAnnotations("a", "b").WithName("pv-1").WithSpec("someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "ensure spec.storageClassName is retained",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
expectedRes: NewTestUnstructured().WithAnnotations("a", "b").WithName("pv-1").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "if backup.spec.snapshotVolumes is false, ignore restore.spec.restorePVs and return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "storageClassName", "someOtherField").Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).WithSnapshotVolumes(false).Backup,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("storageClassName", "someOtherField").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "restore.spec.restorePVs=false, return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup1").WithPhase(api.BackupPhaseInProgress).Backup,
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("pv-1", "loc-1", "gp", "az-1", "snap-1", 1000),
|
||||
},
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "backup.status.volumeBackups non-nil and no entry for PV: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").WithSnapshot("non-matching-pv", "snap").Backup,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "backup.status.volumeBackups has entry for PV, >1 VSLs configured: return error",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").WithSnapshot("pv-1", "snap").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "volumeSnapshots is empty: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{},
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "volumeSnapshots doesn't have a snapshot for PV: return early",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("non-matching-pv-1", "loc-1", "type-1", "az-1", "snap-1", 1),
|
||||
newSnapshot("non-matching-pv-2", "loc-2", "type-2", "az-2", "snap-2", 2),
|
||||
},
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
snapshotLocationInformer = informers.NewSharedInformerFactory(client, 0).Velero().V1().VolumeSnapshotLocations()
|
||||
)
|
||||
|
||||
r := &pvRestorer{
|
||||
logger: velerotest.NewLogger(),
|
||||
restorePVs: tc.restore.Spec.RestorePVs,
|
||||
snapshotLocationLister: snapshotLocationInformer.Lister(),
|
||||
}
|
||||
if tc.backup != nil {
|
||||
r.backup = tc.backup
|
||||
r.snapshotVolumes = tc.backup.Spec.SnapshotVolumes
|
||||
}
|
||||
|
||||
for _, loc := range tc.locations {
|
||||
require.NoError(t, snapshotLocationInformer.Informer().GetStore().Add(loc))
|
||||
}
|
||||
|
||||
res, err := r.executePVAction(tc.obj)
|
||||
switch tc.expectedErr {
|
||||
case true:
|
||||
assert.Nil(t, res)
|
||||
assert.NotNil(t, err)
|
||||
case false:
|
||||
assert.Equal(t, tc.expectedRes, res)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func int64Ptr(val int) *int64 {
|
||||
r := int64(val)
|
||||
return &r
|
||||
}
|
||||
|
||||
type providerToBlockStoreMap map[string]cloudprovider.BlockStore
|
||||
|
||||
func (g providerToBlockStoreMap) GetBlockStore(provider string) (cloudprovider.BlockStore, error) {
|
||||
if bs, ok := g[provider]; !ok {
|
||||
return nil, errors.New("block store not found for provider")
|
||||
} else {
|
||||
return bs, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutePVAction_SnapshotRestores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *unstructured.Unstructured
|
||||
restore *api.Restore
|
||||
backup *api.Backup
|
||||
volumeSnapshots []*volume.Snapshot
|
||||
locations []*api.VolumeSnapshotLocation
|
||||
expectedProvider string
|
||||
expectedSnapshotID string
|
||||
expectedVolumeType string
|
||||
expectedVolumeAZ string
|
||||
expectedVolumeIOPS *int64
|
||||
expectedSnapshot *volume.Snapshot
|
||||
}{
|
||||
{
|
||||
name: "pre-v0.10 backup with .status.volumeBackups with entry for PV and single VSL executes restore",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").
|
||||
WithVolumeBackupInfo("pv-1", "snap-1", "type-1", "az-1", int64Ptr(1)).
|
||||
WithVolumeBackupInfo("pv-2", "snap-2", "type-2", "az-2", int64Ptr(2)).
|
||||
Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").WithProvider("provider-1").VolumeSnapshotLocation,
|
||||
},
|
||||
expectedProvider: "provider-1",
|
||||
expectedSnapshotID: "snap-1",
|
||||
expectedVolumeType: "type-1",
|
||||
expectedVolumeAZ: "az-1",
|
||||
expectedVolumeIOPS: int64Ptr(1),
|
||||
},
|
||||
{
|
||||
name: "v0.10+ backup with a matching volume.Snapshot for PV executes restore",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: velerotest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: velerotest.NewTestBackup().WithName("backup-1").Backup,
|
||||
locations: []*api.VolumeSnapshotLocation{
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-1").WithProvider("provider-1").VolumeSnapshotLocation,
|
||||
velerotest.NewTestVolumeSnapshotLocation().WithName("loc-2").WithProvider("provider-2").VolumeSnapshotLocation,
|
||||
},
|
||||
volumeSnapshots: []*volume.Snapshot{
|
||||
newSnapshot("pv-1", "loc-1", "type-1", "az-1", "snap-1", 1),
|
||||
newSnapshot("pv-2", "loc-2", "type-2", "az-2", "snap-2", 2),
|
||||
},
|
||||
expectedProvider: "provider-1",
|
||||
expectedSnapshotID: "snap-1",
|
||||
expectedVolumeType: "type-1",
|
||||
expectedVolumeAZ: "az-1",
|
||||
expectedVolumeIOPS: int64Ptr(1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
blockStore = new(cloudprovidermocks.BlockStore)
|
||||
blockStoreGetter = providerToBlockStoreMap(map[string]cloudprovider.BlockStore{
|
||||
tc.expectedProvider: blockStore,
|
||||
})
|
||||
locationsInformer = informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0).Velero().V1().VolumeSnapshotLocations()
|
||||
)
|
||||
|
||||
for _, loc := range tc.locations {
|
||||
require.NoError(t, locationsInformer.Informer().GetStore().Add(loc))
|
||||
}
|
||||
|
||||
r := &pvRestorer{
|
||||
logger: velerotest.NewLogger(),
|
||||
backup: tc.backup,
|
||||
volumeSnapshots: tc.volumeSnapshots,
|
||||
snapshotLocationLister: locationsInformer.Lister(),
|
||||
blockStoreGetter: blockStoreGetter,
|
||||
}
|
||||
|
||||
blockStore.On("Init", mock.Anything).Return(nil)
|
||||
blockStore.On("CreateVolumeFromSnapshot", tc.expectedSnapshotID, tc.expectedVolumeType, tc.expectedVolumeAZ, tc.expectedVolumeIOPS).Return("volume-1", nil)
|
||||
blockStore.On("SetVolumeID", tc.obj, "volume-1").Return(tc.obj, nil)
|
||||
|
||||
_, err := r.executePVAction(tc.obj)
|
||||
assert.NoError(t, err)
|
||||
|
||||
blockStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPVReady(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user