870 lines
32 KiB
Go
870 lines
32 KiB
Go
/*
|
|
Copyright 2017 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"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
corev1api "k8s.io/api/core/v1"
|
|
"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/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
|
|
v1 "github.com/heptio/velero/pkg/apis/velero/v1"
|
|
"github.com/heptio/velero/pkg/plugin/velero"
|
|
resticmocks "github.com/heptio/velero/pkg/restic/mocks"
|
|
"github.com/heptio/velero/pkg/util/collections"
|
|
velerotest "github.com/heptio/velero/pkg/util/test"
|
|
)
|
|
|
|
func TestBackupItemSkips(t *testing.T) {
|
|
tests := []struct {
|
|
testName string
|
|
namespace string
|
|
name string
|
|
namespaces *collections.IncludesExcludes
|
|
groupResource schema.GroupResource
|
|
resources *collections.IncludesExcludes
|
|
terminating bool
|
|
backedUpItems map[itemKey]struct{}
|
|
}{
|
|
{
|
|
testName: "namespace not in includes list",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
namespaces: collections.NewIncludesExcludes().Includes("a"),
|
|
},
|
|
{
|
|
testName: "namespace in excludes list",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
namespaces: collections.NewIncludesExcludes().Excludes("ns"),
|
|
},
|
|
{
|
|
testName: "resource not in includes list",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
groupResource: schema.GroupResource{Group: "foo", Resource: "bar"},
|
|
namespaces: collections.NewIncludesExcludes(),
|
|
resources: collections.NewIncludesExcludes().Includes("a.b"),
|
|
},
|
|
{
|
|
testName: "resource in excludes list",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
groupResource: schema.GroupResource{Group: "foo", Resource: "bar"},
|
|
namespaces: collections.NewIncludesExcludes(),
|
|
resources: collections.NewIncludesExcludes().Excludes("bar.foo"),
|
|
},
|
|
{
|
|
testName: "resource already backed up",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
groupResource: schema.GroupResource{Group: "foo", Resource: "bar"},
|
|
namespaces: collections.NewIncludesExcludes(),
|
|
resources: collections.NewIncludesExcludes(),
|
|
backedUpItems: map[itemKey]struct{}{
|
|
{resource: "bar.foo", namespace: "ns", name: "foo"}: {},
|
|
},
|
|
},
|
|
{
|
|
testName: "terminating resource",
|
|
namespace: "ns",
|
|
name: "foo",
|
|
groupResource: schema.GroupResource{Group: "foo", Resource: "bar"},
|
|
namespaces: collections.NewIncludesExcludes(),
|
|
resources: collections.NewIncludesExcludes(),
|
|
terminating: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.testName, func(t *testing.T) {
|
|
req := &Request{
|
|
NamespaceIncludesExcludes: test.namespaces,
|
|
ResourceIncludesExcludes: test.resources,
|
|
}
|
|
|
|
ib := &defaultItemBackupper{
|
|
backupRequest: req,
|
|
backedUpItems: test.backedUpItems,
|
|
}
|
|
|
|
pod := &corev1api.Pod{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: test.namespace, Name: test.name},
|
|
}
|
|
|
|
if test.terminating {
|
|
pod.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()}
|
|
}
|
|
unstructuredObj, unmarshalErr := runtime.DefaultUnstructuredConverter.ToUnstructured(pod)
|
|
require.NoError(t, unmarshalErr)
|
|
u := &unstructured.Unstructured{Object: unstructuredObj}
|
|
err := ib.backupItem(velerotest.NewLogger(), u, test.groupResource)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackupItemSkipsClusterScopedResourceWhenIncludeClusterResourcesFalse(t *testing.T) {
|
|
f := false
|
|
ib := &defaultItemBackupper{
|
|
backupRequest: &Request{
|
|
Backup: &v1.Backup{
|
|
Spec: v1.BackupSpec{
|
|
IncludeClusterResources: &f,
|
|
},
|
|
},
|
|
NamespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
ResourceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
},
|
|
}
|
|
|
|
u := velerotest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Foo","metadata":{"name":"bar"}}`)
|
|
err := ib.backupItem(velerotest.NewLogger(), u, schema.GroupResource{Group: "foo", Resource: "bar"})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestBackupItemNoSkips(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
item string
|
|
namespaceIncludesExcludes *collections.IncludesExcludes
|
|
expectError bool
|
|
expectExcluded bool
|
|
expectedTarHeaderName string
|
|
tarWriteError bool
|
|
tarHeaderWriteError bool
|
|
customAction bool
|
|
expectedActionID string
|
|
customActionAdditionalItemIdentifiers []velero.ResourceIdentifier
|
|
customActionAdditionalItems []runtime.Unstructured
|
|
groupResource string
|
|
snapshottableVolumes map[string]v1.VolumeBackupInfo
|
|
snapshotError error
|
|
additionalItemError error
|
|
trackedPVCs sets.String
|
|
expectedTrackedPVCs sets.String
|
|
}{
|
|
{
|
|
name: "explicit namespace include",
|
|
item: `{"metadata":{"namespace":"foo","name":"bar"}}`,
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo"),
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json",
|
|
},
|
|
{
|
|
name: "* namespace include",
|
|
item: `{"metadata":{"namespace":"foo","name":"bar"}}`,
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json",
|
|
},
|
|
{
|
|
name: "cluster-scoped",
|
|
item: `{"metadata":{"name":"bar"}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/cluster/bar.json",
|
|
},
|
|
{
|
|
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: "action invoked - cluster-scoped",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"metadata":{"name":"bar"}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/cluster/bar.json",
|
|
customAction: true,
|
|
expectedActionID: "bar",
|
|
},
|
|
{
|
|
name: "action invoked - namespaced",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"metadata":{"namespace": "myns", "name":"bar"}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/namespaces/myns/bar.json",
|
|
customAction: true,
|
|
expectedActionID: "myns/bar",
|
|
},
|
|
{
|
|
name: "action invoked - additional items",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"metadata":{"namespace": "myns", "name":"bar"}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/namespaces/myns/bar.json",
|
|
customAction: true,
|
|
expectedActionID: "myns/bar",
|
|
customActionAdditionalItemIdentifiers: []velero.ResourceIdentifier{
|
|
{
|
|
GroupResource: schema.GroupResource{Group: "g1", Resource: "r1"},
|
|
Namespace: "ns1",
|
|
Name: "n1",
|
|
},
|
|
{
|
|
GroupResource: schema.GroupResource{Group: "g2", Resource: "r2"},
|
|
Namespace: "ns2",
|
|
Name: "n2",
|
|
},
|
|
},
|
|
customActionAdditionalItems: []runtime.Unstructured{
|
|
velerotest.UnstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
|
velerotest.UnstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
|
},
|
|
},
|
|
{
|
|
name: "action invoked - additional items - error",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"metadata":{"namespace": "myns", "name":"bar"}}`,
|
|
expectError: true,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/resource.group/namespaces/myns/bar.json",
|
|
customAction: true,
|
|
expectedActionID: "myns/bar",
|
|
customActionAdditionalItemIdentifiers: []velero.ResourceIdentifier{
|
|
{
|
|
GroupResource: schema.GroupResource{Group: "g1", Resource: "r1"},
|
|
Namespace: "ns1",
|
|
Name: "n1",
|
|
},
|
|
{
|
|
GroupResource: schema.GroupResource{Group: "g2", Resource: "r2"},
|
|
Namespace: "ns2",
|
|
Name: "n2",
|
|
},
|
|
},
|
|
customActionAdditionalItems: []runtime.Unstructured{
|
|
velerotest.UnstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
|
velerotest.UnstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
|
},
|
|
additionalItemError: errors.New("foo"),
|
|
},
|
|
{
|
|
name: "takePVSnapshot is not invoked for PVs when blockStore == nil",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/persistentvolumes/cluster/mypv.json",
|
|
groupResource: "persistentvolumes",
|
|
},
|
|
{
|
|
name: "takePVSnapshot is invoked for PVs when blockStore != nil",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: false,
|
|
expectExcluded: false,
|
|
expectedTarHeaderName: "resources/persistentvolumes/cluster/mypv.json",
|
|
groupResource: "persistentvolumes",
|
|
snapshottableVolumes: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {SnapshotID: "snapshot-1", AvailabilityZone: "us-east-1c"},
|
|
},
|
|
},
|
|
{
|
|
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 blockStore to be created, but no
|
|
// snapshots are expected to be taken.
|
|
snapshottableVolumes: map[string]v1.VolumeBackupInfo{},
|
|
trackedPVCs: sets.NewString(key("pvc-ns", "pvc"), key("another-pvc-ns", "another-pvc")),
|
|
},
|
|
{
|
|
name: "takePVSnapshot is invoked for PVs when their claim is not 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",
|
|
snapshottableVolumes: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {SnapshotID: "snapshot-1", AvailabilityZone: "us-east-1c"},
|
|
},
|
|
trackedPVCs: sets.NewString(key("another-pvc-ns", "another-pvc")),
|
|
},
|
|
{
|
|
name: "backup fails when takePVSnapshot fails",
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
|
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: true,
|
|
groupResource: "persistentvolumes",
|
|
snapshottableVolumes: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {SnapshotID: "snapshot-1", AvailabilityZone: "us-east-1c"},
|
|
},
|
|
snapshotError: fmt.Errorf("failure"),
|
|
},
|
|
{
|
|
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 (
|
|
action *fakeAction
|
|
backup = new(Request)
|
|
groupResource = schema.ParseGroupResource("resource.group")
|
|
backedUpItems = make(map[itemKey]struct{})
|
|
w = &fakeTarWriter{}
|
|
)
|
|
|
|
backup.Backup = new(v1.Backup)
|
|
backup.NamespaceIncludesExcludes = collections.NewIncludesExcludes()
|
|
backup.ResourceIncludesExcludes = collections.NewIncludesExcludes()
|
|
backup.SnapshotLocations = []*v1.VolumeSnapshotLocation{
|
|
new(v1.VolumeSnapshotLocation),
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
if test.customAction {
|
|
action = &fakeAction{
|
|
additionalItems: test.customActionAdditionalItemIdentifiers,
|
|
}
|
|
backup.ResolvedActions = []resolvedAction{
|
|
{
|
|
BackupItemAction: action,
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes(groupResource.String()),
|
|
selector: labels.Everything(),
|
|
},
|
|
}
|
|
}
|
|
|
|
podCommandExecutor := &velerotest.MockPodCommandExecutor{}
|
|
defer podCommandExecutor.AssertExpectations(t)
|
|
|
|
dynamicFactory := &velerotest.FakeDynamicFactory{}
|
|
defer dynamicFactory.AssertExpectations(t)
|
|
|
|
discoveryHelper := velerotest.NewFakeDiscoveryHelper(true, nil)
|
|
|
|
blockStoreGetter := &blockStoreGetter{}
|
|
|
|
b := (&defaultItemBackupperFactory{}).newItemBackupper(
|
|
backup,
|
|
backedUpItems,
|
|
podCommandExecutor,
|
|
w,
|
|
dynamicFactory,
|
|
discoveryHelper,
|
|
nil, // restic backupper
|
|
newPVCSnapshotTracker(),
|
|
blockStoreGetter,
|
|
).(*defaultItemBackupper)
|
|
|
|
var blockStore *velerotest.FakeBlockStore
|
|
if test.snapshottableVolumes != nil {
|
|
blockStore = &velerotest.FakeBlockStore{
|
|
SnapshottableVolumes: test.snapshottableVolumes,
|
|
VolumeID: "vol-abc123",
|
|
Error: test.snapshotError,
|
|
}
|
|
|
|
blockStoreGetter.blockStore = blockStore
|
|
}
|
|
|
|
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
|
|
|
|
additionalItemBackupper := &mockItemBackupper{}
|
|
defer additionalItemBackupper.AssertExpectations(t)
|
|
b.additionalItemBackupper = additionalItemBackupper
|
|
|
|
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)
|
|
|
|
for i, item := range test.customActionAdditionalItemIdentifiers {
|
|
if test.additionalItemError != nil && i > 0 {
|
|
break
|
|
}
|
|
itemClient := &velerotest.FakeDynamicClient{}
|
|
defer itemClient.AssertExpectations(t)
|
|
|
|
dynamicFactory.On("ClientForGroupVersionResource", item.GroupResource.WithVersion("").GroupVersion(), metav1.APIResource{Name: item.Resource}, item.Namespace).Return(itemClient, nil)
|
|
|
|
itemClient.On("Get", item.Name, metav1.GetOptions{}).Return(test.customActionAdditionalItems[i], nil)
|
|
|
|
additionalItemBackupper.On("backupItem", mock.AnythingOfType("*logrus.Entry"), test.customActionAdditionalItems[i], item.GroupResource).Return(test.additionalItemError)
|
|
}
|
|
|
|
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.customAction {
|
|
if len(action.ids) != 1 {
|
|
t.Errorf("unexpected custom action ids: %v", action.ids)
|
|
} else if e, a := test.expectedActionID, action.ids[0]; e != a {
|
|
t.Errorf("action.ids[0]: expected %s, got %s", e, a)
|
|
}
|
|
|
|
require.Equal(t, 1, len(action.backups), "unexpected custom action backups: %#v", action.backups)
|
|
assert.Equal(t, backup.Backup, &(action.backups[0]), "backup")
|
|
}
|
|
|
|
if test.snapshottableVolumes != nil {
|
|
require.Equal(t, len(test.snapshottableVolumes), len(blockStore.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 blockStoreGetter struct {
|
|
blockStore velero.BlockStore
|
|
}
|
|
|
|
func (b *blockStoreGetter) GetBlockStore(name string) (velero.BlockStore, error) {
|
|
if b.blockStore != nil {
|
|
return b.blockStore, nil
|
|
}
|
|
return nil, errors.New("plugin not found")
|
|
}
|
|
|
|
type addAnnotationAction struct{}
|
|
|
|
func (a *addAnnotationAction) Execute(item runtime.Unstructured, backup *v1.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")
|
|
}
|
|
|
|
func TestItemActionModificationsToItemPersist(t *testing.T) {
|
|
var (
|
|
w = &fakeTarWriter{}
|
|
obj = &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"namespace": "myns",
|
|
"name": "bar",
|
|
},
|
|
},
|
|
}
|
|
req = &Request{
|
|
NamespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
ResourceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
ResolvedActions: []resolvedAction{
|
|
{
|
|
BackupItemAction: &addAnnotationAction{},
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
resourceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
selector: labels.Everything(),
|
|
},
|
|
},
|
|
}
|
|
|
|
b = (&defaultItemBackupperFactory{}).newItemBackupper(
|
|
req,
|
|
make(map[itemKey]struct{}),
|
|
nil,
|
|
w,
|
|
&velerotest.FakeDynamicFactory{},
|
|
velerotest.NewFakeDiscoveryHelper(true, nil),
|
|
nil,
|
|
newPVCSnapshotTracker(),
|
|
nil,
|
|
).(*defaultItemBackupper)
|
|
)
|
|
|
|
// our expected backed-up object is the passed-in object plus the annotation
|
|
// that the backup item action adds.
|
|
expected := obj.DeepCopy()
|
|
expected.SetAnnotations(map[string]string{"foo": "bar"})
|
|
|
|
// method under test
|
|
require.NoError(t, b.backupItem(velerotest.NewLogger(), obj, schema.ParseGroupResource("resource.group")))
|
|
|
|
// get the actual backed-up item
|
|
require.Len(t, w.data, 1)
|
|
actual, err := velerotest.GetAsMap(string(w.data[0]))
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, expected.Object, actual)
|
|
}
|
|
|
|
func TestResticAnnotationsPersist(t *testing.T) {
|
|
var (
|
|
w = &fakeTarWriter{}
|
|
obj = &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"namespace": "myns",
|
|
"name": "bar",
|
|
"annotations": map[string]interface{}{
|
|
"backup.velero.io/backup-volumes": "volume-1,volume-2",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
req = &Request{
|
|
NamespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
ResourceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
ResolvedActions: []resolvedAction{
|
|
{
|
|
BackupItemAction: &addAnnotationAction{},
|
|
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
resourceIncludesExcludes: collections.NewIncludesExcludes(),
|
|
selector: labels.Everything(),
|
|
},
|
|
},
|
|
}
|
|
resticBackupper = &resticmocks.Backupper{}
|
|
b = (&defaultItemBackupperFactory{}).newItemBackupper(
|
|
req,
|
|
make(map[itemKey]struct{}),
|
|
nil,
|
|
w,
|
|
&velerotest.FakeDynamicFactory{},
|
|
velerotest.NewFakeDiscoveryHelper(true, nil),
|
|
resticBackupper,
|
|
newPVCSnapshotTracker(),
|
|
nil,
|
|
).(*defaultItemBackupper)
|
|
)
|
|
|
|
resticBackupper.
|
|
On("BackupPodVolumes", mock.Anything, mock.Anything, mock.Anything).
|
|
Return(map[string]string{"volume-1": "snapshot-1", "volume-2": "snapshot-2"}, nil)
|
|
|
|
// our expected backed-up object is the passed-in object, plus the annotation
|
|
// that the backup item action adds, plus the annotations that the restic
|
|
// backupper adds
|
|
expected := obj.DeepCopy()
|
|
annotations := expected.GetAnnotations()
|
|
if annotations == nil {
|
|
annotations = make(map[string]string)
|
|
}
|
|
annotations["foo"] = "bar"
|
|
annotations["snapshot.velero.io/volume-1"] = "snapshot-1"
|
|
annotations["snapshot.velero.io/volume-2"] = "snapshot-2"
|
|
expected.SetAnnotations(annotations)
|
|
|
|
// method under test
|
|
require.NoError(t, b.backupItem(velerotest.NewLogger(), obj, schema.ParseGroupResource("pods")))
|
|
|
|
// get the actual backed-up item
|
|
require.Len(t, w.data, 1)
|
|
actual, err := velerotest.GetAsMap(string(w.data[0]))
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, expected.Object, actual)
|
|
}
|
|
|
|
func TestTakePVSnapshot(t *testing.T) {
|
|
iops := int64(1000)
|
|
|
|
tests := []struct {
|
|
name string
|
|
snapshotEnabled bool
|
|
pv string
|
|
ttl time.Duration
|
|
expectError bool
|
|
expectedVolumeID string
|
|
expectedSnapshotsTaken int
|
|
volumeInfo map[string]v1.VolumeBackupInfo
|
|
}{
|
|
{
|
|
name: "snapshot disabled",
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
|
|
snapshotEnabled: false,
|
|
},
|
|
{
|
|
name: "unsupported PV source type",
|
|
snapshotEnabled: true,
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"unsupportedPVSource": {}}}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "without iops",
|
|
snapshotEnabled: true,
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: false,
|
|
expectedSnapshotsTaken: 1,
|
|
expectedVolumeID: "vol-abc123",
|
|
ttl: 5 * time.Minute,
|
|
volumeInfo: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
|
},
|
|
},
|
|
{
|
|
name: "with iops",
|
|
snapshotEnabled: true,
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: false,
|
|
expectedSnapshotsTaken: 1,
|
|
expectedVolumeID: "vol-abc123",
|
|
ttl: 5 * time.Minute,
|
|
volumeInfo: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {Type: "io1", Iops: &iops, SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
|
},
|
|
},
|
|
{
|
|
name: "create snapshot error",
|
|
snapshotEnabled: true,
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
|
expectedVolumeID: "pd-abc123",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "PV with label metadata but no failureDomainZone",
|
|
snapshotEnabled: true,
|
|
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/region": "us-east-1"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
|
expectError: false,
|
|
expectedSnapshotsTaken: 1,
|
|
expectedVolumeID: "vol-abc123",
|
|
ttl: 5 * time.Minute,
|
|
volumeInfo: map[string]v1.VolumeBackupInfo{
|
|
"vol-abc123": {Type: "gp", SnapshotID: "snap-1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
backup := &v1.Backup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: v1.DefaultNamespace,
|
|
Name: "mybackup",
|
|
},
|
|
Spec: v1.BackupSpec{
|
|
SnapshotVolumes: &test.snapshotEnabled,
|
|
TTL: metav1.Duration{Duration: test.ttl},
|
|
},
|
|
}
|
|
|
|
blockStore := &velerotest.FakeBlockStore{
|
|
SnapshottableVolumes: test.volumeInfo,
|
|
VolumeID: test.expectedVolumeID,
|
|
}
|
|
|
|
ib := &defaultItemBackupper{
|
|
backupRequest: &Request{
|
|
Backup: backup,
|
|
SnapshotLocations: []*v1.VolumeSnapshotLocation{new(v1.VolumeSnapshotLocation)},
|
|
},
|
|
blockStoreGetter: &blockStoreGetter{blockStore: blockStore},
|
|
}
|
|
|
|
pv, err := velerotest.GetAsMap(test.pv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// method under test
|
|
err = ib.takePVSnapshot(&unstructured.Unstructured{Object: pv}, velerotest.NewLogger())
|
|
|
|
gotErr := err != nil
|
|
|
|
if e, a := test.expectError, gotErr; e != a {
|
|
t.Errorf("error: expected %v, got %v", e, a)
|
|
}
|
|
if test.expectError {
|
|
return
|
|
}
|
|
|
|
if !test.snapshotEnabled {
|
|
// don't need to check anything else if snapshots are disabled
|
|
return
|
|
}
|
|
|
|
// we should have exactly one snapshot taken
|
|
require.Equal(t, test.expectedSnapshotsTaken, blockStore.SnapshotsTaken.Len())
|
|
|
|
if test.expectedSnapshotsTaken > 0 {
|
|
require.Len(t, ib.backupRequest.VolumeSnapshots, 1)
|
|
snapshot := ib.backupRequest.VolumeSnapshots[0]
|
|
|
|
snapshotID, _ := blockStore.SnapshotsTaken.PopAny()
|
|
assert.Equal(t, snapshotID, snapshot.Status.ProviderSnapshotID)
|
|
assert.Equal(t, test.volumeInfo[test.expectedVolumeID].Type, snapshot.Spec.VolumeType)
|
|
assert.Equal(t, test.volumeInfo[test.expectedVolumeID].Iops, snapshot.Spec.VolumeIOPS)
|
|
assert.Equal(t, test.volumeInfo[test.expectedVolumeID].AvailabilityZone, snapshot.Spec.VolumeAZ)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type mockItemBackupper struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (ib *mockItemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) error {
|
|
args := ib.Called(logger, obj, groupResource)
|
|
return args.Error(0)
|
|
}
|