Unit tests for restic restore (#1747)

* Add unit tests for PVB restore functionality

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Add tests for restore action

Signed-off-by: Carlisia <carlisiac@vmware.com>

* TestRestoreWithRestic wip

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Fix build

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Mockery

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Cleanup mocks

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Remove unused mock

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Use consistent pattern for test building

Signed-off-by: Carlisia <carlisia@vmware.com>

* Test cleanup

Signed-off-by: Carlisia <carlisia@vmware.com>

* Better godoc

Signed-off-by: Carlisia <carlisia@vmware.com>

* Improve test cases

Signed-off-by: Carlisia <carlisia@vmware.com>

* Fix build

Signed-off-by: Carlisia <carlisia@vmware.com>

* Minor test cleanup

Signed-off-by: Carlisia <carlisia@vmware.com>

* New pvb test input names

Signed-off-by: Carlisia <carlisia@vmware.com>
This commit is contained in:
KubeKween
2019-08-27 15:49:23 -07:00
committed by Adnan Abdulhussein
parent 7ea065a94f
commit 6b66a49a21
6 changed files with 308 additions and 93 deletions

View File

@@ -62,3 +62,21 @@ func (b *PodVolumeBackupBuilder) Phase(phase velerov1api.PodVolumeBackupPhase) *
b.object.Status.Phase = phase
return b
}
// SnapshotID sets the PodVolumeBackup's snapshot ID.
func (b *PodVolumeBackupBuilder) SnapshotID(snapshotID string) *PodVolumeBackupBuilder {
b.object.Status.SnapshotID = snapshotID
return b
}
// PodName sets the name of the pod associated with this PodVolumeBackup.
func (b *PodVolumeBackupBuilder) PodName(name string) *PodVolumeBackupBuilder {
b.object.Spec.Pod.Name = name
return b
}
// Volume sets the name of the volume associated with this PodVolumeBackup.
func (b *PodVolumeBackupBuilder) Volume(volume string) *PodVolumeBackupBuilder {
b.object.Spec.Volume = volume
return b
}

View File

@@ -28,54 +28,90 @@ import (
"k8s.io/client-go/tools/cache"
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/builder"
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
velerotest "github.com/heptio/velero/pkg/test"
)
func TestGetPodSnapshotAnnotations(t *testing.T) {
func TestGetVolumeBackupsForPod(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected map[string]string
name string
podVolumeBackups []*velerov1api.PodVolumeBackup
podAnnotations map[string]string
podName string
expected map[string]string
}{
{
name: "nil annotations",
annotations: nil,
expected: nil,
name: "nil annotations",
podAnnotations: nil,
expected: nil,
},
{
name: "empty annotations",
annotations: make(map[string]string),
expected: nil,
name: "empty annotations",
podAnnotations: make(map[string]string),
expected: nil,
},
{
name: "non-empty map, no snapshot annotation",
annotations: map[string]string{"foo": "bar"},
expected: nil,
name: "non-empty map, no snapshot annotation",
podAnnotations: map[string]string{"foo": "bar"},
expected: nil,
},
{
name: "has snapshot annotation only, no suffix",
annotations: map[string]string{podAnnotationPrefix: "bar"},
expected: map[string]string{"": "bar"},
name: "has snapshot annotation only, no suffix",
podAnnotations: map[string]string{podAnnotationPrefix: "bar"},
expected: map[string]string{"": "bar"},
},
{
name: "has snapshot annotation only, with suffix",
annotations: map[string]string{podAnnotationPrefix + "foo": "bar"},
expected: map[string]string{"foo": "bar"},
name: "has snapshot annotation only, with suffix",
podAnnotations: map[string]string{podAnnotationPrefix + "foo": "bar"},
expected: map[string]string{"foo": "bar"},
},
{
name: "has snapshot annotation, with suffix",
annotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
expected: map[string]string{"foo": "bar", "abc": "123"},
name: "has snapshot annotation, with suffix",
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
expected: map[string]string{"foo": "bar", "abc": "123"},
},
{
name: "has snapshot annotation, with suffix, and also PVBs",
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").SnapshotID("bar").Volume("pvbtest1-foo").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").SnapshotID("123").Volume("pvbtest2-abc").Result(),
},
podName: "TestPod",
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
expected: map[string]string{"pvbtest1-foo": "bar", "pvbtest2-abc": "123"},
},
{
name: "no snapshot annotation, no suffix, but with PVBs",
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").SnapshotID("bar").Volume("pvbtest1-foo").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").SnapshotID("123").Volume("pvbtest2-abc").Result(),
},
podName: "TestPod",
expected: map[string]string{"pvbtest1-foo": "bar", "pvbtest2-abc": "123"},
},
{
name: "has snapshot annotation, with suffix, and with PVBs from current pod and a PVB from another pod",
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").SnapshotID("bar").Volume("pvbtest1-foo").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").SnapshotID("123").Volume("pvbtest2-abc").Result(),
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestAnotherPod").SnapshotID("xyz").Volume("pvbtest3-xyz").Result(),
},
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
podName: "TestPod",
expected: map[string]string{"pvbtest1-foo": "bar", "pvbtest2-abc": "123"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pod := &corev1api.Pod{}
pod.Annotations = test.annotations
assert.Equal(t, test.expected, getPodSnapshotAnnotations(pod))
pod.Annotations = test.podAnnotations
pod.Name = test.podName
res := GetVolumeBackupsForPod(test.podVolumeBackups, pod)
assert.Equal(t, test.expected, res)
})
}
}

View File

@@ -1,41 +0,0 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
logrus "github.com/sirupsen/logrus"
mock "github.com/stretchr/testify/mock"
corev1 "k8s.io/api/core/v1"
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
)
// Backupper is an autogenerated mock type for the Backupper type
type Backupper struct {
mock.Mock
}
// BackupPodVolumes provides a mock function with given fields: backup, pod, log
func (_m *Backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1.Pod, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) {
ret := _m.Called(backup, pod, log)
var r0 []*velerov1api.PodVolumeBackup
if rf, ok := ret.Get(0).(func(*velerov1api.Backup, *corev1.Pod, logrus.FieldLogger) []*velerov1api.PodVolumeBackup); ok {
r0 = rf(backup, pod, log)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*velerov1api.PodVolumeBackup)
}
}
var r1 []error
if rf, ok := ret.Get(1).(func(*velerov1api.Backup, *corev1.Pod, logrus.FieldLogger) []error); ok {
r1 = rf(backup, pod, log)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]error)
}
}
return r0, r1
}

View File

@@ -0,0 +1,27 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
import restic "github.com/heptio/velero/pkg/restic"
// Restorer is an autogenerated mock type for the Restorer type
type Restorer struct {
mock.Mock
}
// RestorePodVolumes provides a mock function with given fields: _a0
func (_m *Restorer) RestorePodVolumes(_a0 restic.RestoreData) []error {
ret := _m.Called(_a0)
var r0 []error
if rf, ok := ret.Get(0).(func(restic.RestoreData) []error); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]error)
}
}
return r0
}

View File

@@ -28,7 +28,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
api "github.com/heptio/velero/pkg/apis/velero/v1"
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/builder"
"github.com/heptio/velero/pkg/buildinfo"
velerofake "github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
@@ -98,17 +98,24 @@ func TestResticRestoreActionExecute(t *testing.T) {
defaultCPURequestLimit, defaultMemRequestLimit, // limits
)
var (
restoreName = "my-restore"
backupName = "test-backup"
veleroNs = "velero"
)
tests := []struct {
name string
pod *corev1api.Pod
want *corev1api.Pod
name string
pod *corev1api.Pod
podVolumeBackups []*velerov1api.PodVolumeBackup
want *corev1api.Pod
}{
{
name: "Restoring pod with no other initContainers adds the restic initContainer",
pod: builder.ForPod("ns-1", "pod").ObjectMeta(
pod: builder.ForPod("ns-1", "my-pod").ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/myvol", "")).
Result(),
want: builder.ForPod("ns-1", "pod").
want: builder.ForPod("ns-1", "my-pod").
ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/myvol", "")).
InitContainers(
@@ -119,11 +126,12 @@ func TestResticRestoreActionExecute(t *testing.T) {
},
{
name: "Restoring pod with other initContainers adds the restic initContainer as the first one",
pod: builder.ForPod("ns-1", "pod").ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/myvol", "")).
pod: builder.ForPod("ns-1", "my-pod").
ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/myvol", "")).
InitContainers(builder.ForContainer("first-container", "").Result()).
Result(),
want: builder.ForPod("ns-1", "pod").
want: builder.ForPod("ns-1", "my-pod").
ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/myvol", "")).
InitContainers(
@@ -133,10 +141,55 @@ func TestResticRestoreActionExecute(t *testing.T) {
builder.ForContainer("first-container", "").Result()).
Result(),
},
{
name: "Restoring pod with other initContainers adds the restic initContainer as the first one using PVB to identify the volumes and not annotations",
pod: builder.ForPod("ns-1", "my-pod").
Volumes(
builder.ForVolume("vol-1").PersistentVolumeClaimSource("pvc-1").Result(),
builder.ForVolume("vol-2").PersistentVolumeClaimSource("pvc-2").Result(),
).
ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/not-used", "")).
InitContainers(builder.ForContainer("first-container", "").Result()).
Result(),
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
PodName("my-pod").
Volume("vol-1").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
Result(),
builder.ForPodVolumeBackup(veleroNs, "pvb-2").
PodName("my-pod").
Volume("vol-2").
ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)).
Result(),
},
want: builder.ForPod("ns-1", "my-pod").
Volumes(
builder.ForVolume("vol-1").PersistentVolumeClaimSource("pvc-1").Result(),
builder.ForVolume("vol-2").PersistentVolumeClaimSource("pvc-2").Result(),
).
ObjectMeta(
builder.WithAnnotations("snapshot.velero.io/not-used", "")).
InitContainers(
newResticInitContainerBuilder(initContainerImage(defaultImageBase), "").
Resources(&resourceReqs).
VolumeMounts(builder.ForVolumeMount("vol-1", "/restores/vol-1").Result(), builder.ForVolumeMount("vol-2", "/restores/vol-2").Result()).Result(),
builder.ForContainer("first-container", "").Result()).
Result(),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
clientset := fake.NewSimpleClientset()
clientsetVelero := velerofake.NewSimpleClientset()
for _, podVolumeBackup := range tc.podVolumeBackups {
_, err := clientsetVelero.VeleroV1().PodVolumeBackups(veleroNs).Create(podVolumeBackup)
require.NoError(t, err)
}
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pod)
require.NoError(t, err)
@@ -144,22 +197,20 @@ func TestResticRestoreActionExecute(t *testing.T) {
Item: &unstructured.Unstructured{
Object: unstructuredMap,
},
Restore: builder.ForRestore("velero", "my-restore").
Phase(api.RestorePhaseInProgress).
Restore: builder.ForRestore(veleroNs, restoreName).
Backup(backupName).
Phase(velerov1api.RestorePhaseInProgress).
Result(),
}
clientset := fake.NewSimpleClientset()
clientsetVelero := velerofake.NewSimpleClientset()
a := NewResticRestoreAction(
logrus.StandardLogger(),
clientset.CoreV1().ConfigMaps("velero"),
clientsetVelero.VeleroV1().PodVolumeBackups("velero"),
clientset.CoreV1().ConfigMaps(veleroNs),
clientsetVelero.VeleroV1().PodVolumeBackups(veleroNs),
)
// method under test
res, err := a.Execute(input)
assert.NoError(t, err)
wantUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.want)

View File

@@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
ctx "context"
"encoding/json"
"fmt"
"io"
@@ -50,6 +51,8 @@ import (
velerov1informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
"github.com/heptio/velero/pkg/kuberesource"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/restic"
resticmocks "github.com/heptio/velero/pkg/restic/mocks"
"github.com/heptio/velero/pkg/test"
testutil "github.com/heptio/velero/pkg/test"
"github.com/heptio/velero/pkg/util/collections"
@@ -1739,7 +1742,6 @@ func TestRestorePersistentVolumes(t *testing.T) {
volumeSnapshots []*volume.Snapshot
volumeSnapshotLocations []*velerov1api.VolumeSnapshotLocation
volumeSnapshotterGetter volumeSnapshotterGetter
podVolumeBackups []*velerov1api.PodVolumeBackup
want []*test.APIResource
}{
{
@@ -2000,7 +2002,7 @@ func TestRestorePersistentVolumes(t *testing.T) {
},
{
name: "include podvolumebackups, and when a PV with a reclaim policy of retain has a snapshot and exists in-cluster, neither the snapshot nor the PV are restored",
name: "when a PV with a reclaim policy of retain has a snapshot and exists in-cluster, neither the snapshot nor the PV are restored",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
tarball: newTarWriter(t).
@@ -2050,10 +2052,7 @@ func TestRestorePersistentVolumes(t *testing.T) {
// restored, we'd get an error of "snapshot not found".
"provider-1": &volumeSnapshotter{},
},
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").Result(),
},
want: []*test.APIResource{
test.PVs(
builder.ForPersistentVolume("pv-1").
@@ -2092,12 +2091,11 @@ func TestRestorePersistentVolumes(t *testing.T) {
}
data := Request{
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
PodVolumeBackups: tc.podVolumeBackups,
VolumeSnapshots: tc.volumeSnapshots,
BackupReader: tc.tarball,
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
VolumeSnapshots: tc.volumeSnapshots,
BackupReader: tc.tarball,
}
warnings, errs := h.restorer.Restore(
data,
@@ -2113,6 +2111,132 @@ func TestRestorePersistentVolumes(t *testing.T) {
}
}
type fakeResticRestorerFactory struct {
restorer *resticmocks.Restorer
}
func (f *fakeResticRestorerFactory) NewRestorer(ctx.Context, *velerov1api.Restore) (restic.Restorer, error) {
return f.restorer, nil
}
// TestRestoreWithRestic verifies that a call to RestorePodVolumes was made as and when
// expected for the given pods by using a mock for the restic restorer.
func TestRestoreWithRestic(t *testing.T) {
tests := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
apiResources []*test.APIResource
podVolumeBackups []*velerov1api.PodVolumeBackup
podWithPVBs, podWithoutPVBs []*corev1api.Pod
want map[*test.APIResource][]string
}{
{
name: "a pod that exists in given backup and contains associated PVBs should have should have RestorePodVolumes called",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
apiResources: []*test.APIResource{test.Pods()},
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("pod-1").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("pod-2").Result(),
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("pod-4").Result(),
},
podWithPVBs: []*corev1api.Pod{
builder.ForPod("ns-1", "pod-2").
Result(),
builder.ForPod("ns-2", "pod-4").
Result(),
},
podWithoutPVBs: []*corev1api.Pod{
builder.ForPod("ns-2", "pod-3").
Result(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-2", "ns-2/pod-3", "ns-2/pod-4"},
},
},
{
name: "a pod that exists in given backup but does not contain associated PVBs should not have should have RestorePodVolumes called",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
apiResources: []*test.APIResource{test.Pods()},
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("pod-1").Result(),
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("pod-2").Result(),
},
podWithPVBs: []*corev1api.Pod{},
podWithoutPVBs: []*corev1api.Pod{
builder.ForPod("ns-1", "pod-3").
Result(),
builder.ForPod("ns-2", "pod-4").
Result(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-3", "ns-2/pod-4"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
restorer := new(resticmocks.Restorer)
defer restorer.AssertExpectations(t)
h.restorer.resticRestorerFactory = &fakeResticRestorerFactory{
restorer: restorer,
}
// needed only to indicate resource types that can be restored, in this case, pods
for _, resource := range tc.apiResources {
h.addItems(t, resource)
}
tarball := newTarWriter(t)
// these backed up pods don't have any PVBs associated with them, so a call to RestorePodVolumes is not expected to be made for them
for _, pod := range tc.podWithoutPVBs {
tarball.addItems("pods", pod)
}
// these backed up pods have PVBs associated with them, so a call to RestorePodVolumes will be made for each of them
for _, pod := range tc.podWithPVBs {
tarball.addItems("pods", pod)
// the restore process adds these labels before restoring, so we must add them here too otherwise they won't match
pod.Labels = map[string]string{"velero.io/backup-name": tc.backup.Name, "velero.io/restore-name": tc.restore.Name}
expectedArgs := restic.RestoreData{
Restore: tc.restore,
Pod: pod,
PodVolumeBackups: tc.podVolumeBackups,
SourceNamespace: pod.Namespace,
BackupLocation: "",
}
restorer.
On("RestorePodVolumes", expectedArgs).
Return(nil)
}
data := Request{
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
PodVolumeBackups: tc.podVolumeBackups,
BackupReader: tarball.done(),
}
warnings, errs := h.restorer.Restore(
data,
nil, // actions
nil, // snapshot location lister
nil, // volume snapshotter getter
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, tc.want)
})
}
}
func TestPrioritizeResources(t *testing.T) {
tests := []struct {
name string