Migrate restic backup tests (#1703)

* migrate pkg/backup restic tests to new structure

Signed-off-by: Steve Kriss <krisss@vmware.com>

* rename backup_new_test.go to backup_test.go

Signed-off-by: Steve Kriss <krisss@vmware.com>

* use pod volume backup builder

Signed-off-by: Steve Kriss <krisss@vmware.com>
pull/1706/head
Steve Kriss 2019-07-29 16:29:44 -06:00 committed by Adnan Abdulhussein
parent b24e940399
commit bf247836e6
2 changed files with 104 additions and 288 deletions

View File

@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"io"
"io/ioutil"
@ -44,6 +45,7 @@ import (
"github.com/heptio/velero/pkg/discovery"
"github.com/heptio/velero/pkg/kuberesource"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/restic"
"github.com/heptio/velero/pkg/test"
kubeutil "github.com/heptio/velero/pkg/util/kube"
testutil "github.com/heptio/velero/pkg/util/test"
@ -2017,6 +2019,108 @@ func TestBackupWithHooks(t *testing.T) {
}
}
type fakeResticBackupperFactory struct {
podVolumeBackups []*velerov1.PodVolumeBackup
}
func (f *fakeResticBackupperFactory) NewBackupper(context.Context, *velerov1.Backup) (restic.Backupper, error) {
return &fakeResticBackupper{
podVolumeBackups: f.podVolumeBackups,
}, nil
}
type fakeResticBackupper struct {
podVolumeBackups []*velerov1.PodVolumeBackup
}
func (b *fakeResticBackupper) BackupPodVolumes(backup *velerov1.Backup, pod *corev1.Pod, _ logrus.FieldLogger) ([]*velerov1.PodVolumeBackup, []error) {
return b.podVolumeBackups, nil
}
// TestBackupWithRestic runs backups of pods that are annotated for restic backup,
// and ensures that the restic backupper is called, that the returned PodVolumeBackups
// are added to the Request object, and that when PVCs are backed up with restic, the
// claimed PVs are not also snapshotted using a VolumeSnapshotter.
func TestBackupWithRestic(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*test.APIResource
vsl *velerov1.VolumeSnapshotLocation
snapshotterGetter volumeSnapshotterGetter
want []*velerov1.PodVolumeBackup
}{
{
name: "a pod annotated for restic backup should result in pod volume backups being returned",
backup: defaultBackup().Backup(),
apiResources: []*test.APIResource{
test.Pods(
test.NewPod("ns-1", "pod-1", test.WithAnnotations("backup.velero.io/backup-volumes", "foo")),
),
},
want: []*velerov1.PodVolumeBackup{
NewNamedPodVolumeBackupBuilder("velero", "pvb-1").PodVolumeBackup(),
},
},
{
name: "when PVC pod volumes are backed up using restic, their claimed PVs are not also snapshotted",
backup: defaultBackup().Backup(),
apiResources: []*test.APIResource{
test.Pods(
test.NewPod("ns-1", "pod-1",
test.WithAnnotations("backup.velero.io/backup-volumes", "vol-1,vol-2"),
test.WithVolume(test.NewVolume("vol-1", test.WithPVCSource("pvc-1"))),
test.WithVolume(test.NewVolume("vol-2", test.WithPVCSource("pvc-2"))),
),
),
test.PVCs(
test.NewPVC("ns-1", "pvc-1", test.WithPVName("pv-1")),
test.NewPVC("ns-1", "pvc-2", test.WithPVName("pv-2")),
),
test.PVs(
test.NewPV("pv-1", test.WithClaimRef("ns-1", "pvc-1")),
test.NewPV("pv-2", test.WithClaimRef("ns-1", "pvc-2")),
),
},
vsl: newSnapshotLocation("velero", "default", "default"),
snapshotterGetter: map[string]velero.VolumeSnapshotter{
"default": new(fakeVolumeSnapshotter).
WithVolume("pv-1", "vol-1", "", "type-1", 100, false).
WithVolume("pv-2", "vol-2", "", "type-1", 100, false),
},
want: []*velerov1.PodVolumeBackup{
NewNamedPodVolumeBackupBuilder("velero", "pvb-1").PodVolumeBackup(),
NewNamedPodVolumeBackupBuilder("velero", "pvb-2").PodVolumeBackup(),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup, SnapshotLocations: []*velerov1.VolumeSnapshotLocation{tc.vsl}}
backupFile = bytes.NewBuffer([]byte{})
)
h.backupper.resticBackupperFactory = &fakeResticBackupperFactory{
podVolumeBackups: tc.want,
}
for _, resource := range tc.apiResources {
h.addItems(t, resource)
}
require.NoError(t, h.backupper.Backup(h.log, req, backupFile, nil, tc.snapshotterGetter))
assert.Equal(t, tc.want, req.PodVolumeBackups)
// this assumes that we don't have any test cases where some PVs should be snapshotted using a VolumeSnapshotter
assert.Nil(t, req.VolumeSnapshots)
})
}
}
// pluggableAction is a backup item action that can be plugged with an Execute
// function body at runtime.
type pluggableAction struct {

View File

@ -1,288 +0,0 @@
/*
Copyright 2017, 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 backup
import (
"archive/tar"
"encoding/json"
"reflect"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/util/collections"
velerotest "github.com/heptio/velero/pkg/util/test"
)
func TestBackupItemNoSkips(t *testing.T) {
tests := []struct {
name string
item string
namespaceIncludesExcludes *collections.IncludesExcludes
expectError bool
expectExcluded bool
expectedTarHeaderName string
tarWriteError bool
tarHeaderWriteError bool
groupResource string
snapshottableVolumes map[string]velerotest.VolumeBackupInfo
snapshotError error
trackedPVCs sets.String
expectedTrackedPVCs sets.String
}{
{
name: "tar header write error",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: true,
tarHeaderWriteError: true,
},
{
name: "tar write error",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: true,
tarWriteError: true,
},
{
name: "takePVSnapshot is not invoked for PVs when their claim is tracked in the restic PVC tracker",
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"claimRef": {"namespace": "pvc-ns", "name": "pvc"}, "awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "resources/persistentvolumes/cluster/mypv.json",
groupResource: "persistentvolumes",
// empty snapshottableVolumes causes a volumeSnapshotter to be created, but no
// snapshots are expected to be taken.
snapshottableVolumes: map[string]velerotest.VolumeBackupInfo{},
trackedPVCs: sets.NewString(key("pvc-ns", "pvc"), key("another-pvc-ns", "another-pvc")),
},
{
name: "pod's restic PVC volume backups (only) are tracked",
item: `{"apiVersion": "v1", "kind": "Pod", "spec": {"volumes": [{"name": "volume-1", "persistentVolumeClaim": {"claimName": "bar"}},{"name": "volume-2", "persistentVolumeClaim": {"claimName": "baz"}},{"name": "volume-1", "emptyDir": {}}]}, "metadata":{"namespace":"foo","name":"bar", "annotations": {"backup.velero.io/backup-volumes": "volume-1,volume-2"}}}`,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
groupResource: "pods",
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "resources/pods/namespaces/foo/bar.json",
expectedTrackedPVCs: sets.NewString(key("foo", "bar"), key("foo", "baz")),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
backup = new(Request)
groupResource = schema.ParseGroupResource("resource.group")
backedUpItems = make(map[itemKey]struct{})
w = &fakeTarWriter{}
)
backup.Backup = new(velerov1api.Backup)
backup.NamespaceIncludesExcludes = collections.NewIncludesExcludes()
backup.ResourceIncludesExcludes = collections.NewIncludesExcludes()
backup.SnapshotLocations = []*velerov1api.VolumeSnapshotLocation{
newSnapshotLocation("velero", "default", "default"),
}
if test.groupResource != "" {
groupResource = schema.ParseGroupResource(test.groupResource)
}
item, err := velerotest.GetAsMap(test.item)
if err != nil {
t.Fatal(err)
}
namespaces := test.namespaceIncludesExcludes
if namespaces == nil {
namespaces = collections.NewIncludesExcludes()
}
if test.tarHeaderWriteError {
w.writeHeaderError = errors.New("error")
}
if test.tarWriteError {
w.writeError = errors.New("error")
}
podCommandExecutor := &velerotest.MockPodCommandExecutor{}
defer podCommandExecutor.AssertExpectations(t)
dynamicFactory := &velerotest.FakeDynamicFactory{}
defer dynamicFactory.AssertExpectations(t)
discoveryHelper := velerotest.NewFakeDiscoveryHelper(true, nil)
volumeSnapshotterGetter := make(volumeSnapshotterGetter)
b := (&defaultItemBackupperFactory{}).newItemBackupper(
backup,
backedUpItems,
podCommandExecutor,
w,
dynamicFactory,
discoveryHelper,
nil, // restic backupper
newPVCSnapshotTracker(),
volumeSnapshotterGetter,
).(*defaultItemBackupper)
var volumeSnapshotter *velerotest.FakeVolumeSnapshotter
if test.snapshottableVolumes != nil {
volumeSnapshotter = &velerotest.FakeVolumeSnapshotter{
SnapshottableVolumes: test.snapshottableVolumes,
VolumeID: "vol-abc123",
Error: test.snapshotError,
}
volumeSnapshotterGetter["default"] = volumeSnapshotter
}
if test.trackedPVCs != nil {
b.resticSnapshotTracker.pvcs = test.trackedPVCs
}
// make sure the podCommandExecutor was set correctly in the real hook handler
assert.Equal(t, podCommandExecutor, b.itemHookHandler.(*defaultItemHookHandler).podCommandExecutor)
itemHookHandler := &mockItemHookHandler{}
defer itemHookHandler.AssertExpectations(t)
b.itemHookHandler = itemHookHandler
obj := &unstructured.Unstructured{Object: item}
itemHookHandler.On("handleHooks", mock.Anything, groupResource, obj, backup.ResourceHooks, hookPhasePre).Return(nil)
itemHookHandler.On("handleHooks", mock.Anything, groupResource, obj, backup.ResourceHooks, hookPhasePost).Return(nil)
err = b.backupItem(velerotest.NewLogger(), obj, groupResource)
gotError := err != nil
if e, a := test.expectError, gotError; e != a {
t.Fatalf("error: expected %t, got %t: %v", e, a, err)
}
if test.expectError {
return
}
if test.expectExcluded {
if len(w.headers) > 0 {
t.Errorf("unexpected header write")
}
if len(w.data) > 0 {
t.Errorf("unexpected data write")
}
return
}
// Convert to JSON for comparing number of bytes to the tar header
itemJSON, err := json.Marshal(&item)
if err != nil {
t.Fatal(err)
}
require.Equal(t, 1, len(w.headers), "headers")
assert.Equal(t, test.expectedTarHeaderName, w.headers[0].Name, "header.name")
assert.Equal(t, int64(len(itemJSON)), w.headers[0].Size, "header.size")
assert.Equal(t, byte(tar.TypeReg), w.headers[0].Typeflag, "header.typeflag")
assert.Equal(t, int64(0755), w.headers[0].Mode, "header.mode")
assert.False(t, w.headers[0].ModTime.IsZero(), "header.modTime set")
assert.Equal(t, 1, len(w.data), "# of data")
actual, err := velerotest.GetAsMap(string(w.data[0]))
if err != nil {
t.Fatal(err)
}
if e, a := item, actual; !reflect.DeepEqual(e, a) {
t.Errorf("data: expected %s, got %s", e, a)
}
if test.snapshottableVolumes != nil {
require.Equal(t, len(test.snapshottableVolumes), len(volumeSnapshotter.SnapshotsTaken))
}
if len(test.snapshottableVolumes) > 0 {
require.Len(t, backup.VolumeSnapshots, 1)
snapshot := backup.VolumeSnapshots[0]
assert.Equal(t, test.snapshottableVolumes["vol-abc123"].SnapshotID, snapshot.Status.ProviderSnapshotID)
assert.Equal(t, test.snapshottableVolumes["vol-abc123"].Type, snapshot.Spec.VolumeType)
assert.Equal(t, test.snapshottableVolumes["vol-abc123"].Iops, snapshot.Spec.VolumeIOPS)
assert.Equal(t, test.snapshottableVolumes["vol-abc123"].AvailabilityZone, snapshot.Spec.VolumeAZ)
}
if test.expectedTrackedPVCs != nil {
require.Equal(t, len(test.expectedTrackedPVCs), len(b.resticSnapshotTracker.pvcs))
for key := range test.expectedTrackedPVCs {
assert.True(t, b.resticSnapshotTracker.pvcs.Has(key))
}
}
})
}
}
type addAnnotationAction struct{}
func (a *addAnnotationAction) Execute(item runtime.Unstructured, backup *velerov1api.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
// since item actions run out-of-proc, do a deep-copy here to simulate passing data
// across a process boundary.
copy := item.(*unstructured.Unstructured).DeepCopy()
metadata, err := meta.Accessor(copy)
if err != nil {
return copy, nil, nil
}
annotations := metadata.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations["foo"] = "bar"
metadata.SetAnnotations(annotations)
return copy, nil, nil
}
func (a *addAnnotationAction) AppliesTo() (velero.ResourceSelector, error) {
panic("not implemented")
}
type fakeTarWriter struct {
closeCalled bool
headers []*tar.Header
data [][]byte
writeHeaderError error
writeError error
}
func (w *fakeTarWriter) Close() error { return nil }
func (w *fakeTarWriter) Write(data []byte) (int, error) {
w.data = append(w.data, data)
return 0, w.writeError
}
func (w *fakeTarWriter) WriteHeader(header *tar.Header) error {
w.headers = append(w.headers, header)
return w.writeHeaderError
}