diff --git a/pkg/backup/backup_new_test.go b/pkg/backup/backup_test.go similarity index 94% rename from pkg/backup/backup_new_test.go rename to pkg/backup/backup_test.go index 6fa893cf1..630506e5e 100644 --- a/pkg/backup/backup_new_test.go +++ b/pkg/backup/backup_test.go @@ -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 { diff --git a/pkg/backup/item_backupper_test.go b/pkg/backup/item_backupper_test.go deleted file mode 100644 index ba8349f20..000000000 --- a/pkg/backup/item_backupper_test.go +++ /dev/null @@ -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 -}