Files
velero/pkg/backup/backup_new_test.go
T
Steve KrissandKubeKween 9e7ff4e3d9 Refactoring for backup item action tests (#1545)
* migrate and enhance backup item action tests

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

* migrate terminating resource test

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

* add godoc for test functions

Signed-off-by: Steve Kriss <krisss@vmware.com>
2019-06-09 21:02:49 -07:00

1542 lines
43 KiB
Go

/*
Copyright 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"
"bytes"
"compress/gzip"
"encoding/json"
"io"
"io/ioutil"
"sort"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
discoveryfake "k8s.io/client-go/discovery/fake"
dynamicfake "k8s.io/client-go/dynamic/fake"
kubefake "k8s.io/client-go/kubernetes/fake"
velerov1 "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/client"
"github.com/heptio/velero/pkg/discovery"
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
"github.com/heptio/velero/pkg/kuberesource"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/test"
)
// TestBackupResourceFiltering runs backups with different combinations
// of resource filters (included/excluded resources, included/excluded
// namespaces, label selectors, "include cluster resources" flag), and
// verifies that the set of items written to the backup tarball are
// correct. Validation is done by looking at the names of the files in
// the backup tarball; the contents of the files are not checked.
func TestBackupResourceFiltering(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
want []string
}{
{
name: "no filters backs up everything",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
},
},
{
name: "included resources filter only backs up resources of those types",
backup: defaultBackup().
IncludedResources("pods").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
},
},
{
name: "excluded resources filter only backs up resources not of those types",
backup: defaultBackup().
ExcludedResources("deployments").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
},
},
{
name: "included namespaces filter only backs up resources in those namespaces",
backup: defaultBackup().
IncludedNamespaces("foo").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/foo/bar.json",
},
},
{
name: "excluded namespaces filter only backs up resources not in those namespaces",
backup: defaultBackup().
ExcludedNamespaces("zoo").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/foo/bar.json",
},
},
{
name: "IncludeClusterResources=false only backs up namespaced resources",
backup: defaultBackup().
IncludeClusterResources(false).
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
pvs(
newPV("bar"),
newPV("baz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
},
},
{
name: "label selector only backs up matching resources",
backup: defaultBackup().
LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}).
Backup(),
apiResources: []*apiResource{
pods(
withLabel(newPod("foo", "bar"), "a", "b"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
withLabel(newDeployment("zoo", "raz"), "a", "b"),
),
pvs(
withLabel(newPV("bar"), "a", "b"),
withLabel(newPV("baz"), "a", "c"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
"resources/persistentvolumes/cluster/bar.json",
},
},
{
name: "should include cluster-scoped resources if backing up subset of namespaces and IncludeClusterResources=true",
backup: defaultBackup().
IncludedNamespaces("ns-1", "ns-2").
IncludeClusterResources(true).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
"resources/persistentvolumes/cluster/pv-1.json",
"resources/persistentvolumes/cluster/pv-2.json",
},
},
{
name: "should not include cluster-scoped resource if backing up subset of namespaces and IncludeClusterResources=false",
backup: defaultBackup().
IncludedNamespaces("ns-1", "ns-2").
IncludeClusterResources(false).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
},
},
{
name: "should not include cluster-scoped resource if backing up subset of namespaces and IncludeClusterResources=nil",
backup: defaultBackup().
IncludedNamespaces("ns-1", "ns-2").
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
},
},
{
name: "should include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=true",
backup: defaultBackup().
IncludeClusterResources(true).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
"resources/pods/namespaces/ns-3/pod-1.json",
"resources/persistentvolumes/cluster/pv-1.json",
"resources/persistentvolumes/cluster/pv-2.json",
},
},
{
name: "should not include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=false",
backup: defaultBackup().
IncludeClusterResources(false).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
"resources/pods/namespaces/ns-3/pod-1.json",
},
},
{
name: "should include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=nil",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-1"),
newPod("ns-3", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-1.json",
"resources/pods/namespaces/ns-3/pod-1.json",
"resources/persistentvolumes/cluster/pv-1.json",
"resources/persistentvolumes/cluster/pv-2.json",
},
},
{
name: "when a wildcard and a specific resource are included, the wildcard takes precedence",
backup: defaultBackup().
IncludedResources("*", "pods").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
},
},
{
name: "wildcard excludes are ignored",
backup: defaultBackup().
ExcludedResources("*").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
},
},
{
name: "unresolvable included resources are ignored",
backup: defaultBackup().
IncludedResources("pods", "unresolvable").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
},
},
{
name: "unresolvable excluded resources are ignored",
backup: defaultBackup().
ExcludedResources("deployments", "unresolvable").
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/zoo/raz.json",
},
},
{
name: "terminating resources are not backed up",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-2", Name: "pod-2", DeletionTimestamp: &metav1.Time{Time: time.Now()}}},
),
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
h.backupper.Backup(h.log, req, backupFile, nil, nil)
assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...)
})
}
}
// TestBackupResourceCohabitation runs backups for resources that "cohabitate",
// meaning they exist in multiple API groups (e.g. deployments.extensions and
// deployments.apps), and verifies that only one copy of each resource is backed
// up, with preference for the non-"extensions" API group.
func TestBackupResourceCohabitation(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
want []string
}{
{
name: "when deployments exist only in extensions, they're backed up",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
extensionsDeployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/deployments.extensions/namespaces/foo/bar.json",
"resources/deployments.extensions/namespaces/zoo/raz.json",
},
},
{
name: "when deployments exist in both apps and extensions, only apps/deployments are backed up",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
extensionsDeployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
deployments(
newDeployment("foo", "bar"),
newDeployment("zoo", "raz"),
),
},
want: []string{
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
h.backupper.Backup(h.log, req, backupFile, nil, nil)
assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...)
})
}
}
// TestBackupUsesNewCohabitatingResourcesForEachBackup ensures that when two backups are
// run that each include cohabitating resources, one copy of the relevant resources is
// backed up in each backup. Verification is done by looking at the contents of the backup
// tarball. This covers a specific issue that was fixed by https://github.com/heptio/velero/pull/485.
func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
h := newHarness(t)
// run and verify backup 1
backup1 := &Request{
Backup: defaultBackup().Backup(),
}
backup1File := bytes.NewBuffer([]byte{})
h.addItems(t, "apps", "v1", "deployments", "deploys", true, newDeployment("ns-1", "deploy-1"))
h.addItems(t, "extensions", "v1", "deployments", "deploys", true, newDeployment("ns-1", "deploy-1"))
h.backupper.Backup(h.log, backup1, backup1File, nil, nil)
assertTarballContents(t, backup1File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json")
// run and verify backup 2
backup2 := &Request{
Backup: defaultBackup().Backup(),
}
backup2File := bytes.NewBuffer([]byte{})
h.backupper.Backup(h.log, backup2, backup2File, nil, nil)
assertTarballContents(t, backup2File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json")
}
// TestBackupResourceOrdering runs backups of the core API group and ensures that items are backed
// up in the expected order (pods, PVCs, PVs, everything else). Verification is done by looking
// at the order of files written to the backup tarball.
func TestBackupResourceOrdering(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
}{
{
name: "core API group: pods come before pvcs, pvcs come before pvs, pvs come before anything else",
backup: defaultBackup().
SnapshotVolumes(false).
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
pvcs(
newPVC("foo", "bar"),
newPVC("zoo", "raz"),
),
pvs(
newPV("bar"),
newPV("baz"),
),
secrets(
newSecret("foo", "bar"),
newSecret("zoo", "raz"),
),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
h.backupper.Backup(h.log, req, backupFile, nil, nil)
assertTarballOrdering(t, backupFile, "pods", "persistentvolumeclaims", "persistentvolumes")
})
}
}
// TestBackupActionsRunsForCorrectItems runs backups with backup item actions, and
// verifies that each backup item action is run for the correct set of resources based on its
// AppliesTo() resource selector. Verification is done by using the recordResourcesAction struct,
// which records which resources it's executed for.
func TestBackupActionsRunForCorrectItems(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
// actions is a map from a recordResourcesAction (which will record the items it was called for)
// to a slice of expected items, formatted as {namespace}/{name}.
actions map[*recordResourcesAction][]string
}{
{
name: "single action with no selector runs for all items",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction): {"ns-1/pod-1", "ns-2/pod-2", "pv-1", "pv-2"},
},
},
{
name: "single action with a resource selector for namespaced resources runs only for matching resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForResource("pods"): {"ns-1/pod-1", "ns-2/pod-2"},
},
},
{
name: "single action with a resource selector for cluster-scoped resources runs only for matching resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForResource("persistentvolumes"): {"pv-1", "pv-2"},
},
},
{
// TODO this seems like a bug
name: "single action with a namespace selector runs for resources in that namespace plus cluster-scoped resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvcs(
newPVC("ns-1", "pvc-1"),
newPVC("ns-2", "pvc-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForNamespace("ns-1"): {"ns-1/pod-1", "ns-1/pvc-1", "pv-1", "pv-2"},
},
},
{
name: "single action with a resource and namespace selector runs only for matching resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForResource("pods").ForNamespace("ns-1"): {"ns-1/pod-1"},
},
},
{
name: "multiple actions, each with a different resource selector using short name, run for matching resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForResource("po"): {"ns-1/pod-1", "ns-2/pod-2"},
new(recordResourcesAction).ForResource("pv"): {"pv-1", "pv-2"},
},
},
{
name: "actions with selectors that don't match anything don't run for any resources",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
pvcs(
newPVC("ns-2", "pvc-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForNamespace("ns-1").ForResource("persistentvolumeclaims"): nil,
new(recordResourcesAction).ForNamespace("ns-2").ForResource("pods"): nil,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
actions := []velero.BackupItemAction{}
for action := range tc.actions {
actions = append(actions, action)
}
err := h.backupper.Backup(h.log, req, backupFile, actions, nil)
assert.NoError(t, err)
for action, want := range tc.actions {
assert.Equal(t, want, action.ids)
}
})
}
}
// TestBackupWithInvalidActions runs backups with backup item actions that are invalid
// in some way (e.g. an invalid label selector returned from AppliesTo(), an error returned
// from AppliesTo()) and verifies that this causes the backupper.Backup(...) method to
// return an error.
func TestBackupWithInvalidActions(t *testing.T) {
// all test cases in this function are expected to cause the method under test
// to return an error, so no expected results need to be set up.
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
actions []velero.BackupItemAction
}{
{
name: "action with invalid label selector results in an error",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
pvs(
newPV("bar"),
newPV("baz"),
),
},
actions: []velero.BackupItemAction{
new(recordResourcesAction).ForLabelSelector("=invalid-selector"),
},
},
{
name: "action returning an error from AppliesTo results in an error",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
newPod("zoo", "raz"),
),
pvs(
newPV("bar"),
newPV("baz"),
),
},
actions: []velero.BackupItemAction{
&appliesToErrorAction{},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
assert.Error(t, h.backupper.Backup(h.log, req, backupFile, tc.actions, nil))
})
}
}
// appliesToErrorAction is a backup item action that always returns
// an error when AppliesTo() is called.
type appliesToErrorAction struct{}
func (a *appliesToErrorAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{}, errors.New("error calling AppliesTo")
}
func (a *appliesToErrorAction) Execute(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
panic("not implemented")
}
// TestBackupActionModifications runs backups with backup item actions that make modifications
// to items in their Execute(...) methods and verifies that these modifications are
// persisted to the backup tarball. Verification is done by inspecting the file contents
// of the tarball.
func TestBackupActionModifications(t *testing.T) {
// modifyingActionGetter is a helper function that returns a *pluggableAction, whose Execute(...)
// method modifies the item being passed in by calling the 'modify' function on it.
modifyingActionGetter := func(modify func(*unstructured.Unstructured)) *pluggableAction {
return &pluggableAction{
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
obj, ok := item.(*unstructured.Unstructured)
if !ok {
return nil, nil, errors.Errorf("unexpected type %T", item)
}
res := obj.DeepCopy()
modify(res)
return res, nil, nil
},
}
}
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
actions []velero.BackupItemAction
want map[string]unstructuredObject
}{
{
name: "action that adds a label to item gets persisted",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
},
actions: []velero.BackupItemAction{
modifyingActionGetter(func(item *unstructured.Unstructured) {
item.SetLabels(map[string]string{"updated": "true"})
}),
},
want: map[string]unstructuredObject{
"resources/pods/namespaces/ns-1/pod-1.json": toUnstructuredOrFail(t, withLabel(newPod("ns-1", "pod-1"), "updated", "true")),
},
},
{
name: "action that removes labels from item gets persisted",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
withLabel(newPod("ns-1", "pod-1"), "should-be-removed", "true"),
),
},
actions: []velero.BackupItemAction{
modifyingActionGetter(func(item *unstructured.Unstructured) {
item.SetLabels(nil)
}),
},
want: map[string]unstructuredObject{
"resources/pods/namespaces/ns-1/pod-1.json": toUnstructuredOrFail(t, newPod("ns-1", "pod-1")),
},
},
{
name: "action that sets a spec field on item gets persisted",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
},
actions: []velero.BackupItemAction{
modifyingActionGetter(func(item *unstructured.Unstructured) {
item.Object["spec"].(map[string]interface{})["nodeName"] = "foo"
}),
},
want: map[string]unstructuredObject{
"resources/pods/namespaces/ns-1/pod-1.json": toUnstructuredOrFail(t, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-1", Name: "pod-1"}, Spec: corev1.PodSpec{NodeName: "foo"}}),
},
},
{
// TODO this seems like a bug
name: "modifications to name and namespace in an action are persisted in JSON but not in filename",
backup: defaultBackup().
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
},
actions: []velero.BackupItemAction{
modifyingActionGetter(func(item *unstructured.Unstructured) {
item.SetName(item.GetName() + "-updated")
item.SetNamespace(item.GetNamespace() + "-updated")
}),
},
want: map[string]unstructuredObject{
"resources/pods/namespaces/ns-1/pod-1.json": toUnstructuredOrFail(t, newPod("ns-1-updated", "pod-1-updated")),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
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, req, backupFile, tc.actions, nil)
assert.NoError(t, err)
assertTarballFileContents(t, backupFile, tc.want)
})
}
}
// TestBackupActionAdditionalItems runs backups with backup item actions that return
// additional items to be backed up, and verifies that those items are included in the
// backup tarball as appropriate. Verification is done by looking at the files that exist
// in the backup tarball.
func TestBackupActionAdditionalItems(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
actions []velero.BackupItemAction
want []string
}{
{
name: "additional items that are already being backed up are not backed up twice",
backup: defaultBackup().Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
newPod("ns-3", "pod-3"),
),
},
actions: []velero.BackupItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedNamespaces: []string{"ns-1"}},
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
additionalItems := []velero.ResourceIdentifier{
{GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"},
{GroupResource: kuberesource.Pods, Namespace: "ns-3", Name: "pod-3"},
}
return item, additionalItems, nil
},
},
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-2.json",
"resources/pods/namespaces/ns-3/pod-3.json",
},
},
{
name: "when using a backup namespace filter, additional items that are in a non-included namespace are not backed up",
backup: defaultBackup().IncludedNamespaces("ns-1").Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
newPod("ns-3", "pod-3"),
),
},
actions: []velero.BackupItemAction{
&pluggableAction{
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
additionalItems := []velero.ResourceIdentifier{
{GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"},
{GroupResource: kuberesource.Pods, Namespace: "ns-3", Name: "pod-3"},
}
return item, additionalItems, nil
},
},
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
},
},
{
name: "when using a backup namespace filter, additional items that are cluster-scoped are backed up",
backup: defaultBackup().IncludedNamespaces("ns-1").Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: []velero.BackupItemAction{
&pluggableAction{
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
additionalItems := []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-2"},
}
return item, additionalItems, nil
},
},
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/persistentvolumes/cluster/pv-1.json",
"resources/persistentvolumes/cluster/pv-2.json",
},
},
{
name: "when using a backup resource filter, additional items that are non-included resources are not backed up",
backup: defaultBackup().IncludedResources("pods").Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: []velero.BackupItemAction{
&pluggableAction{
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
additionalItems := []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-2"},
}
return item, additionalItems, nil
},
},
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
},
},
{
name: "when IncludeClusterResources=false, additional items that are cluster-scoped are not backed up",
backup: defaultBackup().IncludeClusterResources(false).Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
pvs(
newPV("pv-1"),
newPV("pv-2"),
),
},
actions: []velero.BackupItemAction{
&pluggableAction{
executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
additionalItems := []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-2"},
}
return item, additionalItems, nil
},
},
},
want: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-2.json",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
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, req, backupFile, tc.actions, nil)
assert.NoError(t, err)
assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...)
})
}
}
// pluggableAction is a backup item action that can be plugged with an Execute
// function body at runtime.
type pluggableAction struct {
selector velero.ResourceSelector
executeFunc func(runtime.Unstructured, *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error)
}
func (a *pluggableAction) Execute(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
if a.executeFunc == nil {
return item, nil, nil
}
return a.executeFunc(item, backup)
}
func (a *pluggableAction) AppliesTo() (velero.ResourceSelector, error) {
return a.selector, nil
}
type apiResource struct {
group string
version string
name string
shortName string
namespaced bool
items []metav1.Object
}
func pods(items ...metav1.Object) *apiResource {
return &apiResource{
group: "",
version: "v1",
name: "pods",
shortName: "po",
namespaced: true,
items: items,
}
}
func pvcs(items ...metav1.Object) *apiResource {
return &apiResource{
group: "",
version: "v1",
name: "persistentvolumeclaims",
shortName: "pvc",
namespaced: true,
items: items,
}
}
func secrets(items ...metav1.Object) *apiResource {
return &apiResource{
group: "",
version: "v1",
name: "secrets",
shortName: "secrets",
namespaced: true,
items: items,
}
}
func deployments(items ...metav1.Object) *apiResource {
return &apiResource{
group: "apps",
version: "v1",
name: "deployments",
shortName: "deploy",
namespaced: true,
items: items,
}
}
func extensionsDeployments(items ...metav1.Object) *apiResource {
return &apiResource{
group: "extensions",
version: "v1",
name: "deployments",
shortName: "deploy",
namespaced: true,
items: items,
}
}
func pvs(items ...metav1.Object) *apiResource {
return &apiResource{
group: "",
version: "v1",
name: "persistentvolumes",
shortName: "pv",
namespaced: false,
items: items,
}
}
type harness struct {
veleroClient *fake.Clientset
kubeClient *kubefake.Clientset
dynamicClient *dynamicfake.FakeDynamicClient
discoveryClient *test.DiscoveryClient
backupper *kubernetesBackupper
log logrus.FieldLogger
}
func (h *harness) addItems(t *testing.T, group, version, resource, shortName string, namespaced bool, items ...metav1.Object) {
t.Helper()
h.discoveryClient.WithResource(group, version, resource, namespaced, shortName)
require.NoError(t, h.backupper.discoveryHelper.Refresh())
gvr := schema.GroupVersionResource{
Group: group,
Version: version,
Resource: resource,
}
for _, item := range items {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
require.NoError(t, err)
unstructuredObj := &unstructured.Unstructured{Object: obj}
if namespaced {
_, err = h.dynamicClient.Resource(gvr).Namespace(item.GetNamespace()).Create(unstructuredObj, metav1.CreateOptions{})
} else {
_, err = h.dynamicClient.Resource(gvr).Create(unstructuredObj, metav1.CreateOptions{})
}
require.NoError(t, err)
}
}
func newHarness(t *testing.T) *harness {
t.Helper()
// API server fakes
var (
veleroClient = fake.NewSimpleClientset()
kubeClient = kubefake.NewSimpleClientset()
dynamicClient = dynamicfake.NewSimpleDynamicClient(runtime.NewScheme())
discoveryClient = &test.DiscoveryClient{FakeDiscovery: kubeClient.Discovery().(*discoveryfake.FakeDiscovery)}
)
log := logrus.StandardLogger()
discoveryHelper, err := discovery.NewHelper(discoveryClient, log)
require.NoError(t, err)
return &harness{
veleroClient: veleroClient,
kubeClient: kubeClient,
dynamicClient: dynamicClient,
discoveryClient: discoveryClient,
backupper: &kubernetesBackupper{
dynamicFactory: client.NewDynamicFactory(dynamicClient),
discoveryHelper: discoveryHelper,
groupBackupperFactory: new(defaultGroupBackupperFactory),
// unsupported
podCommandExecutor: nil,
resticBackupperFactory: nil,
resticTimeout: 0,
},
log: log,
}
}
func withLabel(obj metav1.Object, key, val string) metav1.Object {
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels[key] = val
obj.SetLabels(labels)
return obj
}
func newPod(ns, name string) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
}
func newPVC(ns, name string) *corev1.PersistentVolumeClaim {
return &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
}
func newSecret(ns, name string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
}
func newDeployment(ns, name string) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
}
func newPV(name string) *corev1.PersistentVolume {
return &corev1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
}
func defaultBackup() *Builder {
return NewNamedBuilder(velerov1.DefaultNamespace, "backup-1")
}
func toUnstructuredOrFail(t *testing.T, obj interface{}) map[string]interface{} {
t.Helper()
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
require.NoError(t, err)
return res
}
// assertTarballContents verifies that the gzipped tarball stored in the provided
// backupFile contains exactly the file names specified.
func assertTarballContents(t *testing.T, backupFile io.Reader, items ...string) {
t.Helper()
gzr, err := gzip.NewReader(backupFile)
require.NoError(t, err)
r := tar.NewReader(gzr)
var files []string
for {
hdr, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
files = append(files, hdr.Name)
}
sort.Strings(files)
sort.Strings(items)
assert.Equal(t, items, files)
}
// unstructuredObject is a type alias to improve readability.
type unstructuredObject map[string]interface{}
// assertTarballFileContents verifies that the gzipped tarball stored in the provided
// backupFile contains the files specified as keys in 'want', and for each of those
// files verifies that the content of the file is JSON and is equivalent to the JSON
// content stored as values in 'want'.
func assertTarballFileContents(t *testing.T, backupFile io.Reader, want map[string]unstructuredObject) {
t.Helper()
gzr, err := gzip.NewReader(backupFile)
require.NoError(t, err)
r := tar.NewReader(gzr)
items := make(map[string][]byte)
for {
hdr, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
bytes, err := ioutil.ReadAll(r)
require.NoError(t, err)
items[hdr.Name] = bytes
}
for name, wantItem := range want {
gotData, ok := items[name]
assert.True(t, ok, "did not find item %s in tarball", name)
if !ok {
continue
}
// json-unmarshal the data from the tarball
var got unstructuredObject
err := json.Unmarshal(gotData, &got)
assert.NoError(t, err)
if err != nil {
continue
}
assert.Equal(t, wantItem, got)
}
}
// assertTarballOrdering ensures that resources were written to the tarball in the expected
// order. Any resources *not* in orderedResources are required to come *after* all resources
// in orderedResources, in any order.
func assertTarballOrdering(t *testing.T, backupFile io.Reader, orderedResources ...string) {
t.Helper()
gzr, err := gzip.NewReader(backupFile)
require.NoError(t, err)
r := tar.NewReader(gzr)
// lastSeen tracks the index in 'orderedResources' of the last resource type
// we saw in the tarball. Once we've seen a resource in 'orderedResources',
// we should never see another instance of a prior resource.
lastSeen := 0
for {
hdr, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
// ignore files like metadata/version
if !strings.HasPrefix(hdr.Name, "resources/") {
continue
}
// get the resource name
parts := strings.Split(hdr.Name, "/")
require.True(t, len(parts) >= 2)
resourceName := parts[1]
// Find the index in 'orderedResources' of the resource type for
// the current tar item, if it exists. This index ('current') *must*
// be greater than or equal to 'lastSeen', which was the last resource
// we saw, since otherwise the current resource would be out of order. By
// initializing current to len(ordered), we're saying that if the resource
// is not explicitly in orederedResources, then it must come *after*
// all orderedResources.
current := len(orderedResources)
for i, item := range orderedResources {
if item == resourceName {
current = i
break
}
}
// the index of the current resource must be the same as or greater than the index of
// the last resource we saw for the backed-up order to be correct.
assert.True(t, current >= lastSeen, "%s was backed up out of order", resourceName)
lastSeen = current
}
}