From e2bf39a027417efc3d075097144b431ae5b85282 Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Tue, 4 Jun 2019 13:31:34 -0600 Subject: [PATCH] migrate PV snapshot tests Signed-off-by: Steve Kriss --- pkg/backup/backup_new_test.go | 394 +++++++++++++++++++++++++++++- pkg/backup/item_backupper_test.go | 199 +-------------- 2 files changed, 394 insertions(+), 199 deletions(-) diff --git a/pkg/backup/backup_new_test.go b/pkg/backup/backup_new_test.go index 4550f5c69..639622e15 100644 --- a/pkg/backup/backup_new_test.go +++ b/pkg/backup/backup_new_test.go @@ -43,7 +43,6 @@ import ( dynamicfake "k8s.io/client-go/dynamic/fake" kubefake "k8s.io/client-go/kubernetes/fake" - v1 "github.com/heptio/velero/pkg/apis/velero/v1" velerov1 "github.com/heptio/velero/pkg/apis/velero/v1" "github.com/heptio/velero/pkg/client" "github.com/heptio/velero/pkg/discovery" @@ -52,6 +51,7 @@ import ( "github.com/heptio/velero/pkg/plugin/velero" "github.com/heptio/velero/pkg/test" kubeutil "github.com/heptio/velero/pkg/util/kube" + "github.com/heptio/velero/pkg/volume" ) // TestBackupResourceFiltering runs backups with different combinations @@ -627,11 +627,11 @@ func TestBackupResourceOrdering(t *testing.T) { type recordResourcesAction struct { selector velero.ResourceSelector ids []string - backups []v1.Backup + backups []velerov1.Backup additionalItems []velero.ResourceIdentifier } -func (a *recordResourcesAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) { +func (a *recordResourcesAction) Execute(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) { metadata, err := meta.Accessor(item) if err != nil { return item, a.additionalItems, err @@ -1254,6 +1254,382 @@ func TestBackupActionAdditionalItems(t *testing.T) { } } +// volumeSnapshotterGetter is a simple implementation of the VolumeSnapshotterGetter +// interface that returns velero.VolumeSnapshotters from a map if they exist. +type volumeSnapshotterGetter map[string]velero.VolumeSnapshotter + +func (vsg volumeSnapshotterGetter) GetVolumeSnapshotter(name string) (velero.VolumeSnapshotter, error) { + snapshotter, ok := vsg[name] + if !ok { + return nil, errors.New("volume snapshotter not found") + } + + return snapshotter, nil +} + +func int64Ptr(val int) *int64 { + i := int64(val) + return &i +} + +type volumeIdentifier struct { + volumeID string + volumeAZ string +} + +type volumeInfo struct { + volumeType string + iops *int64 + snapshotErr bool +} + +// fakeVolumeSnapshotter is a test fake for the velero.VolumeSnapshotter interface. +type fakeVolumeSnapshotter struct { + // PVVolumeNames is a map from PV name to volume ID, used as the basis + // for the GetVolumeID method. + PVVolumeNames map[string]string + + // Volumes is a map from volume identifier (volume ID + AZ) to a struct + // of volume info, used for the GetVolumeInfo and CreateSnapshot methods. + Volumes map[volumeIdentifier]*volumeInfo +} + +// WithVolume is a test helper for registering persistent volumes that the +// fakeVolumeSnapshotter should handle. +func (vs *fakeVolumeSnapshotter) WithVolume(pvName, id, az, volumeType string, iops int, snapshotErr bool) *fakeVolumeSnapshotter { + if vs.PVVolumeNames == nil { + vs.PVVolumeNames = make(map[string]string) + } + vs.PVVolumeNames[pvName] = id + + if vs.Volumes == nil { + vs.Volumes = make(map[volumeIdentifier]*volumeInfo) + } + + identifier := volumeIdentifier{ + volumeID: id, + volumeAZ: az, + } + + vs.Volumes[identifier] = &volumeInfo{ + volumeType: volumeType, + iops: int64Ptr(iops), + snapshotErr: snapshotErr, + } + + return vs +} + +// Init is a no-op. +func (*fakeVolumeSnapshotter) Init(config map[string]string) error { + return nil +} + +// GetVolumeID looks up the PV name in the PVVolumeNames map and returns the result +// if found, or an error otherwise. +func (vs *fakeVolumeSnapshotter) GetVolumeID(pv runtime.Unstructured) (string, error) { + obj := pv.(*unstructured.Unstructured) + + volumeID, ok := vs.PVVolumeNames[obj.GetName()] + if !ok { + return "", errors.New("unsupported volume type") + } + + return volumeID, nil +} + +// CreateSnapshot looks up the volume in the Volume map. If it's not found, an error is +// returned; if snapshotErr is true on the result, an error is returned; otherwise, +// a snapshotID of "-snapshot" is returned. +func (vs *fakeVolumeSnapshotter) CreateSnapshot(volumeID, volumeAZ string, tags map[string]string) (snapshotID string, err error) { + vi, ok := vs.Volumes[volumeIdentifier{volumeID: volumeID, volumeAZ: volumeAZ}] + if !ok { + return "", errors.New("volume not found") + } + + if vi.snapshotErr { + return "", errors.New("error calling CreateSnapshot") + } + + return volumeID + "-snapshot", nil +} + +// GetVolumeInfo returns volume info if it exists in the Volumes map +// for the specified volume ID and AZ, or an error otherwise. +func (vs *fakeVolumeSnapshotter) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) { + vi, ok := vs.Volumes[volumeIdentifier{volumeID: volumeID, volumeAZ: volumeAZ}] + if !ok { + return "", nil, errors.New("volume not found") + } + + return vi.volumeType, vi.iops, nil +} + +// CreateVolumeFromSnapshot panics because it's not expected to be used for backups. +func (*fakeVolumeSnapshotter) CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (volumeID string, err error) { + panic("CreateVolumeFromSnapshot should not be used for backups") +} + +// SetVolumeID panics because it's not expected to be used for backups. +func (*fakeVolumeSnapshotter) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) { + panic("SetVolumeID should not be used for backups") +} + +// DeleteSnapshot panics because it's not expected to be used for backups. +func (*fakeVolumeSnapshotter) DeleteSnapshot(snapshotID string) error { + panic("DeleteSnapshot should not be used for backups") +} + +// TestBackupWithSnapshots runs backups with volume snapshot locations and volume snapshotters +// configured and verifies that snapshots are created as appropriate. Verification is done by +// looking at the backup request's VolumeSnapshots field. This test uses the fakeVolumeSnapshotter +// struct in place of real volume snapshotters. +func TestBackupWithSnapshots(t *testing.T) { + tests := []struct { + name string + req *Request + vsls []*velerov1.VolumeSnapshotLocation + apiResources []*apiResource + snapshotterGetter volumeSnapshotterGetter + want []*volume.Snapshot + }{ + { + name: "persistent volume with no zone annotation creates a snapshot", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "", "type-1", 100, false), + }, + want: []*volume.Snapshot{ + { + Spec: volume.SnapshotSpec{ + BackupName: "backup-1", + Location: "default", + PersistentVolumeName: "pv-1", + ProviderVolumeID: "vol-1", + VolumeType: "type-1", + VolumeIOPS: int64Ptr(100), + }, + Status: volume.SnapshotStatus{ + Phase: volume.SnapshotPhaseCompleted, + ProviderSnapshotID: "vol-1-snapshot", + }, + }, + }, + }, + { + name: "persistent volume with zone annotation creates a snapshot", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + withLabel(newPV("pv-1"), "failure-domain.beta.kubernetes.io/zone", "zone-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "zone-1", "type-1", 100, false), + }, + want: []*volume.Snapshot{ + { + Spec: volume.SnapshotSpec{ + BackupName: "backup-1", + Location: "default", + PersistentVolumeName: "pv-1", + ProviderVolumeID: "vol-1", + VolumeAZ: "zone-1", + VolumeType: "type-1", + VolumeIOPS: int64Ptr(100), + }, + Status: volume.SnapshotStatus{ + Phase: volume.SnapshotPhaseCompleted, + ProviderSnapshotID: "vol-1-snapshot", + }, + }, + }, + }, + { + name: "error returned from CreateSnapshot results in a failed snapshot", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "", "type-1", 100, true), + }, + want: []*volume.Snapshot{ + { + Spec: volume.SnapshotSpec{ + BackupName: "backup-1", + Location: "default", + PersistentVolumeName: "pv-1", + ProviderVolumeID: "vol-1", + VolumeType: "type-1", + VolumeIOPS: int64Ptr(100), + }, + Status: volume.SnapshotStatus{ + Phase: volume.SnapshotPhaseFailed, + }, + }, + }, + }, + { + name: "backup with SnapshotVolumes=false does not create any snapshots", + req: &Request{ + Backup: defaultBackup().SnapshotVolumes(false).Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "", "type-1", 100, false), + }, + want: nil, + }, + { + name: "backup with no volume snapshot locations does not create any snapshots", + req: &Request{ + Backup: defaultBackup().Backup(), + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "", "type-1", 100, false), + }, + want: nil, + }, + { + name: "backup with no volume snapshotters does not create any snapshots", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{}, + want: nil, + }, + { + name: "unsupported persistent volume type does not create any snapshots", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter), + }, + want: nil, + }, + { + name: "when there are multiple volumes, snapshot locations, and snapshotters, volumes are matched to the right snapshotters", + req: &Request{ + Backup: defaultBackup().Backup(), + SnapshotLocations: []*velerov1.VolumeSnapshotLocation{ + newSnapshotLocation("velero", "default", "default"), + newSnapshotLocation("velero", "another", "another"), + }, + }, + apiResources: []*apiResource{ + pvs( + newPV("pv-1"), + newPV("pv-2"), + ), + }, + snapshotterGetter: map[string]velero.VolumeSnapshotter{ + "default": new(fakeVolumeSnapshotter).WithVolume("pv-1", "vol-1", "", "type-1", 100, false), + "another": new(fakeVolumeSnapshotter).WithVolume("pv-2", "vol-2", "", "type-2", 100, false), + }, + want: []*volume.Snapshot{ + { + Spec: volume.SnapshotSpec{ + BackupName: "backup-1", + Location: "default", + PersistentVolumeName: "pv-1", + ProviderVolumeID: "vol-1", + VolumeType: "type-1", + VolumeIOPS: int64Ptr(100), + }, + Status: volume.SnapshotStatus{ + Phase: volume.SnapshotPhaseCompleted, + ProviderSnapshotID: "vol-1-snapshot", + }, + }, + { + Spec: volume.SnapshotSpec{ + BackupName: "backup-1", + Location: "another", + PersistentVolumeName: "pv-2", + ProviderVolumeID: "vol-2", + VolumeType: "type-2", + VolumeIOPS: int64Ptr(100), + }, + Status: volume.SnapshotStatus{ + Phase: volume.SnapshotPhaseCompleted, + ProviderSnapshotID: "vol-2-snapshot", + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var ( + h = newHarness(t) + backupFile = bytes.NewBuffer([]byte{}) + ) + + for _, resource := range tc.apiResources { + h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...) + } + + err := h.backupper.Backup(h.log, tc.req, backupFile, nil, tc.snapshotterGetter) + assert.NoError(t, err) + + assert.Equal(t, tc.want, tc.req.VolumeSnapshots) + }) + } +} + // pluggableAction is a backup item action that can be plugged with an Execute // function body at runtime. type pluggableAction struct { @@ -1474,6 +1850,18 @@ func newPV(name string) *corev1.PersistentVolume { } } +func newSnapshotLocation(ns, name, provider string) *velerov1.VolumeSnapshotLocation { + return &velerov1.VolumeSnapshotLocation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Spec: velerov1.VolumeSnapshotLocationSpec{ + Provider: provider, + }, + } +} + func defaultBackup() *Builder { return NewNamedBuilder(velerov1.DefaultNamespace, "backup-1") } diff --git a/pkg/backup/item_backupper_test.go b/pkg/backup/item_backupper_test.go index 20d0b42a2..5900a5890 100644 --- a/pkg/backup/item_backupper_test.go +++ b/pkg/backup/item_backupper_test.go @@ -19,17 +19,14 @@ package backup import ( "archive/tar" "encoding/json" - "fmt" "reflect" "testing" - "time" "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" - 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" @@ -71,27 +68,6 @@ func TestBackupItemNoSkips(t *testing.T) { expectError: true, tarWriteError: true, }, - { - name: "takePVSnapshot is not invoked for PVs when volumeSnapshotter == 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 volumeSnapshotter != 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]velerotest.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("*"), @@ -105,30 +81,6 @@ func TestBackupItemNoSkips(t *testing.T) { snapshottableVolumes: map[string]velerotest.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]velerotest.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]velerotest.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"}}}`, @@ -154,7 +106,7 @@ func TestBackupItemNoSkips(t *testing.T) { backup.NamespaceIncludesExcludes = collections.NewIncludesExcludes() backup.ResourceIncludesExcludes = collections.NewIncludesExcludes() backup.SnapshotLocations = []*v1.VolumeSnapshotLocation{ - new(v1.VolumeSnapshotLocation), + newSnapshotLocation("velero", "default", "default"), } if test.groupResource != "" { @@ -186,7 +138,7 @@ func TestBackupItemNoSkips(t *testing.T) { discoveryHelper := velerotest.NewFakeDiscoveryHelper(true, nil) - volumeSnapshotterGetter := &volumeSnapshotterGetter{} + volumeSnapshotterGetter := make(volumeSnapshotterGetter) b := (&defaultItemBackupperFactory{}).newItemBackupper( backup, @@ -208,7 +160,7 @@ func TestBackupItemNoSkips(t *testing.T) { Error: test.snapshotError, } - volumeSnapshotterGetter.volumeSnapshotter = volumeSnapshotter + volumeSnapshotterGetter["default"] = volumeSnapshotter } if test.trackedPVCs != nil { @@ -291,17 +243,6 @@ func TestBackupItemNoSkips(t *testing.T) { } } -type volumeSnapshotterGetter struct { - volumeSnapshotter velero.VolumeSnapshotter -} - -func (b *volumeSnapshotterGetter) GetVolumeSnapshotter(name string) (velero.VolumeSnapshotter, error) { - if b.volumeSnapshotter != nil { - return b.volumeSnapshotter, 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) { @@ -396,140 +337,6 @@ func TestResticAnnotationsPersist(t *testing.T) { 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]velerotest.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]velerotest.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]velerotest.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]velerotest.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}, - }, - } - - volumeSnapshotter := &velerotest.FakeVolumeSnapshotter{ - SnapshottableVolumes: test.volumeInfo, - VolumeID: test.expectedVolumeID, - } - - ib := &defaultItemBackupper{ - backupRequest: &Request{ - Backup: backup, - SnapshotLocations: []*v1.VolumeSnapshotLocation{new(v1.VolumeSnapshotLocation)}, - }, - volumeSnapshotterGetter: &volumeSnapshotterGetter{volumeSnapshotter: volumeSnapshotter}, - } - - 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, volumeSnapshotter.SnapshotsTaken.Len()) - - if test.expectedSnapshotsTaken > 0 { - require.Len(t, ib.backupRequest.VolumeSnapshots, 1) - snapshot := ib.backupRequest.VolumeSnapshots[0] - - snapshotID, _ := volumeSnapshotter.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