Refactor pkg/restore tests (part 2) (#1606)

* update TestPrioritizeResources to use real discovery helper

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

* migrate invalid tarball contents tests

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

* migrate item restore tests

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

* migrate restore item action tests

Signed-off-by: Steve Kriss <krisss@vmware.com>
This commit is contained in:
Steve Kriss
2019-06-27 12:57:47 -06:00
committed by Adnan Abdulhussein
parent dd96aa76db
commit 81c2adc059
4 changed files with 553 additions and 600 deletions

View File

@@ -25,6 +25,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/pkg/errors"
@@ -733,7 +734,7 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (R
fullPath := filepath.Join(resourcePath, file.Name())
obj, err := ctx.unmarshal(fullPath)
if err != nil {
addToResult(&errs, namespace, fmt.Errorf("error decoding %q: %v", fullPath, err))
addToResult(&errs, namespace, fmt.Errorf("error decoding %q: %v", strings.Replace(fullPath, ctx.restoreDir+"/", "", -1), err))
continue
}

View File

@@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"io"
"sort"
"testing"
"time"
@@ -32,15 +33,20 @@ import (
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/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
kubetesting "k8s.io/client-go/testing"
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/backup"
"github.com/heptio/velero/pkg/client"
"github.com/heptio/velero/pkg/discovery"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/test"
"github.com/heptio/velero/pkg/util/encode"
kubeutil "github.com/heptio/velero/pkg/util/kube"
testutil "github.com/heptio/velero/pkg/util/test"
)
@@ -646,6 +652,479 @@ func TestRestoreResourcePriorities(t *testing.T) {
}
}
// TestInvalidTarballContents runs restores for tarballs that are invalid in some way, and
// verifies that the set of items created in the API and the errors returned are correct.
// Validation is done by looking at the namespaces/names of the items in the API and the
// Result objects returned from the restorer.
func TestInvalidTarballContents(t *testing.T) {
tests := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
apiResources []*test.APIResource
tarball io.Reader
want map[*test.APIResource][]string
wantErrs Result
}{
{
name: "empty tarball returns an error",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
done(),
wantErrs: Result{
Velero: []string{"backup does not contain top level resources directory"},
},
},
{
name: "invalid JSON is reported as an error and restore continues",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
add("resources/pods/namespaces/ns-1/pod-1.json", []byte("invalid JSON")).
addItems("pods",
test.NewPod("ns-1", "pod-2"),
).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-2"},
},
wantErrs: Result{
Namespaces: map[string][]string{
"ns-1": {"error decoding \"resources/pods/namespaces/ns-1/pod-1.json\": invalid character 'i' looking for beginning of value"},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
for _, r := range tc.apiResources {
h.DiscoveryClient.WithAPIResource(r)
}
require.NoError(t, h.restorer.discoveryHelper.Refresh())
warnings, errs := h.restorer.Restore(
h.log,
tc.restore,
tc.backup,
nil, // volume snapshots
tc.tarball,
nil, // actions
nil, // snapshot location lister
nil, // volume snapshotter getter
)
assertEmptyResults(t, warnings)
assert.Equal(t, tc.wantErrs, errs)
assertAPIContents(t, h, tc.want)
})
}
}
// TestRestoreItems runs restores of specific items and validates that they are created
// with the expected metadata/spec/status in the API.
func TestRestoreItems(t *testing.T) {
tests := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
apiResources []*test.APIResource
tarball io.Reader
want []*test.APIResource
}{
{
name: "metadata other than namespace/name/labels/annotations gets removed",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods",
test.NewPod("ns-1", "pod-1",
test.WithLabels("key-1", "val-1"),
test.WithAnnotations("key-1", "val-1"),
test.WithClusterName("cluster-1"),
test.WithFinalizers("finalizer-1")),
).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: []*test.APIResource{
test.Pods(
test.NewPod("ns-1", "pod-1",
test.WithLabels("key-1", "val-1", "velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1"),
test.WithAnnotations("key-1", "val-1"),
),
),
},
},
{
name: "status gets removed",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods",
&corev1api.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Status: corev1api.PodStatus{
Message: "a non-empty status",
},
},
).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: []*test.APIResource{
test.Pods(
test.NewPod("ns-1", "pod-1", test.WithLabels("velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1")),
),
},
},
{
name: "object gets labeled with full backup and restore names when they're both shorter than 63 characters",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1")).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: []*test.APIResource{
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels("velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1"))),
},
},
{
name: "object gets labeled with full backup and restore names when they're both equal to 63 characters",
restore: NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-exactly-63-characters").
Backup("the-really-long-kube-service-name-that-is-exactly-63-characters").
Restore(),
backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-exactly-63-characters").Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1")).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: []*test.APIResource{
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels(
"velero.io/backup-name", "the-really-long-kube-service-name-that-is-exactly-63-characters",
"velero.io/restore-name", "the-really-long-kube-service-name-that-is-exactly-63-characters",
))),
},
},
{
name: "object gets labeled with shortened backup and restore names when they're both longer than 63 characters",
restore: NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-much-greater-than-63-characters").
Backup("the-really-long-kube-service-name-that-is-much-greater-than-63-characters").
Restore(),
backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-much-greater-than-63-characters").Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1")).
done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: []*test.APIResource{
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels(
"velero.io/backup-name", "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
"velero.io/restore-name", "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
))),
},
},
{
name: "no error when service account already exists in cluster and is identical to the backed up one",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("serviceaccounts", test.NewServiceAccount("ns-1", "sa-1")).
done(),
apiResources: []*test.APIResource{
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
},
want: []*test.APIResource{
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
},
},
{
name: "service account secrets and image pull secrets are restored when service account already exists in cluster",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("serviceaccounts", &corev1api.ServiceAccount{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ServiceAccount",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "sa-1",
},
Secrets: []corev1api.ObjectReference{{Name: "secret-1"}},
ImagePullSecrets: []corev1api.LocalObjectReference{{Name: "pull-secret-1"}},
}).
done(),
apiResources: []*test.APIResource{
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
},
want: []*test.APIResource{
test.ServiceAccounts(&corev1api.ServiceAccount{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ServiceAccount",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "sa-1",
},
Secrets: []corev1api.ObjectReference{{Name: "secret-1"}},
ImagePullSecrets: []corev1api.LocalObjectReference{{Name: "pull-secret-1"}},
}),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
for _, r := range tc.apiResources {
h.addItems(t, r)
}
warnings, errs := h.restorer.Restore(
h.log,
tc.restore,
tc.backup,
nil, // volume snapshots
tc.tarball,
nil, // actions
nil, // snapshot location lister
nil, // volume snapshotter getter
)
assertEmptyResults(t, warnings, errs)
for _, resource := range tc.want {
resourceClient := h.DynamicClient.Resource(resource.GVR())
for _, item := range resource.Items {
var client dynamic.ResourceInterface
if item.GetNamespace() != "" {
client = resourceClient.Namespace(item.GetNamespace())
} else {
client = resourceClient
}
res, err := client.Get(item.GetName(), metav1.GetOptions{})
if !assert.NoError(t, err) {
continue
}
itemJSON, err := json.Marshal(item)
if !assert.NoError(t, err) {
continue
}
t.Logf("%v", string(itemJSON))
u := make(map[string]interface{})
if !assert.NoError(t, json.Unmarshal(itemJSON, &u)) {
continue
}
want := &unstructured.Unstructured{Object: u}
// These fields get non-nil zero values in the unstructured objects if they're
// empty in the structured objects. Remove them to make comparison easier.
unstructured.RemoveNestedField(want.Object, "metadata", "creationTimestamp")
unstructured.RemoveNestedField(want.Object, "status")
assert.Equal(t, want, res)
}
}
})
}
}
// recordResourcesAction is a restore item action that can be configured
// to run for specific resources/namespaces and simply records the items
// that it is executed for.
type recordResourcesAction struct {
selector velero.ResourceSelector
ids []string
additionalItems []velero.ResourceIdentifier
}
func (a *recordResourcesAction) AppliesTo() (velero.ResourceSelector, error) {
return a.selector, nil
}
func (a *recordResourcesAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
metadata, err := meta.Accessor(input.Item)
if err != nil {
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: a.additionalItems,
}, err
}
a.ids = append(a.ids, kubeutil.NamespaceAndName(metadata))
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: a.additionalItems,
}, nil
}
func (a *recordResourcesAction) ForResource(resource string) *recordResourcesAction {
a.selector.IncludedResources = append(a.selector.IncludedResources, resource)
return a
}
func (a *recordResourcesAction) ForNamespace(namespace string) *recordResourcesAction {
a.selector.IncludedNamespaces = append(a.selector.IncludedNamespaces, namespace)
return a
}
func (a *recordResourcesAction) ForLabelSelector(selector string) *recordResourcesAction {
a.selector.LabelSelector = selector
return a
}
func (a *recordResourcesAction) WithAdditionalItems(items []velero.ResourceIdentifier) *recordResourcesAction {
a.additionalItems = items
return a
}
// TestRestoreActionsRunsForCorrectItems runs restores with restore item actions, and
// verifies that each restore 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 TestRestoreActionsRunForCorrectItems(t *testing.T) {
tests := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
apiResources []*test.APIResource
tarball io.Reader
actions map[*recordResourcesAction][]string
}{
{
name: "single action with no selector runs for all items",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
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",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
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",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
actions: map[*recordResourcesAction][]string{
new(recordResourcesAction).ForResource("persistentvolumes"): {"pv-1", "pv-2"},
},
},
{
name: "multiple actions, each with a different resource selector using short name, run for matching resources",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
addItems("persistentvolumeclaims", test.NewPVC("ns-1", "pvc-1"), test.NewPVC("ns-2", "pvc-2")).
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
done(),
apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()},
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",
restore: defaultRestore().Restore(),
backup: defaultBackup().Backup(),
tarball: newTarWriter(t).
addItems("pods", test.NewPod("ns-1", "pod-1")).
addItems("persistentvolumeclaims", test.NewPVC("ns-2", "pvc-2")).
done(),
apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()},
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) {
h := newHarness(t)
for _, r := range tc.apiResources {
h.addItems(t, r)
}
actions := []velero.RestoreItemAction{}
for action := range tc.actions {
actions = append(actions, action)
}
warnings, errs := h.restorer.Restore(
h.log,
tc.restore,
tc.backup,
nil, // volume snapshots
tc.tarball,
actions,
nil, // snapshot location lister
nil, // volume snapshotter getter
)
assertEmptyResults(t, warnings, errs)
for action, want := range tc.actions {
sort.Strings(want)
sort.Strings(action.ids)
assert.Equal(t, want, action.ids)
}
})
}
}
// assertResourceCreationOrder ensures that resources were created in the expected
// order. Any resources *not* in resourcePriorities are required to come *after* all
// resources in any order.
@@ -717,6 +1196,8 @@ func defaultRestore() *Builder {
// all of the items specified in 'want' (a map from an APIResource definition to a slice
// of resource identifiers, formatted as <namespace>/<name>).
func assertAPIContents(t *testing.T, h *harness, want map[*test.APIResource][]string) {
t.Helper()
for r, want := range want {
res, err := h.DynamicClient.Resource(r.GVR()).List(metav1.ListOptions{})
assert.NoError(t, err)
@@ -848,3 +1329,29 @@ func newHarness(t *testing.T) *harness {
log: log,
}
}
func (h *harness) addItems(t *testing.T, resource *test.APIResource) {
t.Helper()
h.DiscoveryClient.WithAPIResource(resource)
require.NoError(t, h.restorer.discoveryHelper.Refresh())
for _, item := range resource.Items {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
require.NoError(t, err)
unstructuredObj := &unstructured.Unstructured{Object: obj}
// These fields have non-nil zero values in the unstructured objects. We remove
// them to make comparison easier in our tests.
unstructured.RemoveNestedField(unstructuredObj.Object, "metadata", "creationTimestamp")
unstructured.RemoveNestedField(unstructuredObj.Object, "status")
if resource.Namespaced {
_, err = h.DynamicClient.Resource(resource.GVR()).Namespace(item.GetNamespace()).Create(unstructuredObj, metav1.CreateOptions{})
} else {
_, err = h.DynamicClient.Resource(resource.GVR()).Create(unstructuredObj, metav1.CreateOptions{})
}
require.NoError(t, err)
}
}

View File

@@ -21,8 +21,6 @@ import (
"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"
@@ -35,16 +33,17 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
discoveryfake "k8s.io/client-go/discovery/fake"
kubefake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
api "github.com/heptio/velero/pkg/apis/velero/v1"
pkgclient "github.com/heptio/velero/pkg/client"
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
"github.com/heptio/velero/pkg/discovery"
"github.com/heptio/velero/pkg/kuberesource"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/test"
"github.com/heptio/velero/pkg/util/collections"
"github.com/heptio/velero/pkg/util/logging"
velerotest "github.com/heptio/velero/pkg/util/test"
"github.com/heptio/velero/pkg/volume"
)
@@ -90,32 +89,40 @@ func TestPrioritizeResources(t *testing.T) {
logger := velerotest.NewLogger()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var helperResourceList []*metav1.APIResourceList
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
discoveryClient := &test.DiscoveryClient{
FakeDiscovery: kubefake.NewSimpleClientset().Discovery().(*discoveryfake.FakeDiscovery),
}
helper, err := discovery.NewHelper(discoveryClient, logger)
require.NoError(t, err)
// add all the test case's API resources to the discovery client
for gvString, resources := range tc.apiResources {
gv, err := schema.ParseGroupVersion(gvString)
require.NoError(t, err)
for gv, resources := range test.apiResources {
resourceList := &metav1.APIResourceList{GroupVersion: gv}
for _, resource := range resources {
resourceList.APIResources = append(resourceList.APIResources, metav1.APIResource{Name: resource})
discoveryClient.WithAPIResource(&test.APIResource{
Group: gv.Group,
Version: gv.Version,
Name: resource,
})
}
helperResourceList = append(helperResourceList, resourceList)
}
helper := velerotest.NewFakeDiscoveryHelper(true, nil)
helper.ResourceList = helperResourceList
require.NoError(t, helper.Refresh())
includesExcludes := collections.NewIncludesExcludes().Includes(test.includes...).Excludes(test.excludes...)
includesExcludes := collections.NewIncludesExcludes().Includes(tc.includes...).Excludes(tc.excludes...)
result, err := prioritizeResources(helper, test.priorities, includesExcludes, logger)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result, err := prioritizeResources(helper, tc.priorities, includesExcludes, logger)
require.NoError(t, err)
require.Equal(t, len(test.expected), len(result))
require.Equal(t, len(tc.expected), len(result))
for i := range result {
if e, a := test.expected[i], result[i].Resource; e != a {
if e, a := tc.expected[i], result[i].Resource; e != a {
t.Errorf("index %d, expected %s, got %s", i, e, a)
}
}
@@ -123,430 +130,6 @@ func TestPrioritizeResources(t *testing.T) {
}
}
func TestRestorePriority(t *testing.T) {
tests := []struct {
name string
fileSystem *velerotest.FakeFileSystem
restore *api.Restore
baseDir string
prioritizedResources []schema.GroupResource
expectedErrors Result
expectedReadDirs []string
}{
{
name: "error in a single resource doesn't terminate restore immediately, but is returned",
fileSystem: velerotest.NewFakeFileSystem().
WithFile("bak/resources/a/namespaces/ns-1/invalid-json.json", []byte("invalid json")).
WithDirectory("bak/resources/c/namespaces/ns-1"),
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
baseDir: "bak",
prioritizedResources: []schema.GroupResource{
{Resource: "a"},
{Resource: "b"},
{Resource: "c"},
},
expectedErrors: Result{
Namespaces: map[string][]string{
"ns-1": {"error decoding \"bak/resources/a/namespaces/ns-1/invalid-json.json\": invalid character 'i' looking for beginning of value"},
},
},
expectedReadDirs: []string{"bak/resources", "bak/resources/a/namespaces", "bak/resources/a/namespaces/ns-1", "bak/resources/c/namespaces", "bak/resources/c/namespaces/ns-1"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
log := velerotest.NewLogger()
nsClient := &velerotest.FakeNamespaceClient{}
ctx := &context{
restore: test.restore,
namespaceClient: nsClient,
fileSystem: test.fileSystem,
prioritizedResources: test.prioritizedResources,
log: log,
restoreDir: test.baseDir,
}
nsClient.On("Get", mock.Anything, metav1.GetOptions{}).Return(&v1.Namespace{}, nil)
warnings, errors := ctx.restoreFromDir()
assert.Empty(t, warnings.Velero)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Equal(t, test.expectedErrors, errors)
assert.Equal(t, test.expectedReadDirs, test.fileSystem.ReadDirCalls)
})
}
}
func TestRestoreResourceForNamespace(t *testing.T) {
tests := []struct {
name string
namespace string
resourcePath string
labelSelector labels.Selector
includeClusterResources *bool
fileSystem *velerotest.FakeFileSystem
actions []resolvedAction
expectedErrors Result
expectedObjs []unstructured.Unstructured
}{
{
name: "no such directory causes error",
namespace: "ns-1",
resourcePath: "configmaps",
fileSystem: velerotest.NewFakeFileSystem(),
expectedErrors: Result{
Namespaces: map[string][]string{
"ns-1": {"error reading \"configmaps\" resource directory: open configmaps: file does not exist"},
},
},
},
{
name: "empty directory is no-op",
namespace: "ns-1",
resourcePath: "configmaps",
fileSystem: velerotest.NewFakeFileSystem().WithDirectory("configmaps"),
},
{
name: "unmarshall failure does not cause immediate return",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().
WithFile("configmaps/cm-1-invalid.json", []byte("this is not valid json")).
WithFile("configmaps/cm-2.json", newNamedTestConfigMap("cm-2").ToJSON()),
expectedErrors: Result{
Namespaces: map[string][]string{
"ns-1": {"error decoding \"configmaps/cm-1-invalid.json\": invalid character 'h' in literal true (expecting 'r')"},
},
},
expectedObjs: toUnstructured(newNamedTestConfigMap("cm-2").ConfigMap),
},
{
name: "custom restorer is correctly used",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
actions: []resolvedAction{
{
RestoreItemAction: newFakeAction("configmaps"),
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("configmaps"),
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
selector: labels.Everything(),
},
},
expectedObjs: toUnstructured(newTestConfigMap().WithLabels(map[string]string{"fake-restorer": "foo"}).ConfigMap),
},
{
name: "custom restorer for different group/resource is not used",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
actions: []resolvedAction{
{
RestoreItemAction: newFakeAction("foo-resource"),
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo-resource"),
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
selector: labels.Everything(),
},
},
expectedObjs: toUnstructured(newTestConfigMap().ConfigMap),
},
}
var (
client = fake.NewSimpleClientset()
sharedInformers = informers.NewSharedInformerFactory(client, 0)
snapshotLocationLister = sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister()
)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resourceClient := &velerotest.FakeDynamicClient{}
for i := range test.expectedObjs {
addRestoreLabels(&test.expectedObjs[i], "my-restore", "my-backup")
resourceClient.On("Create", &test.expectedObjs[i]).Return(&test.expectedObjs[i], nil)
}
dynamicFactory := &velerotest.FakeDynamicFactory{}
gv := schema.GroupVersion{Group: "", Version: "v1"}
configMapResource := metav1.APIResource{Name: "configmaps", Namespaced: true}
dynamicFactory.On("ClientForGroupVersionResource", gv, configMapResource, test.namespace).Return(resourceClient, nil)
pvResource := metav1.APIResource{Name: "persistentvolumes", Namespaced: false}
dynamicFactory.On("ClientForGroupVersionResource", gv, pvResource, test.namespace).Return(resourceClient, nil)
resourceClient.On("Watch", metav1.ListOptions{}).Return(&fakeWatch{}, nil)
if test.resourcePath == "persistentvolumes" {
resourceClient.On("Get", mock.Anything, metav1.GetOptions{}).Return(&unstructured.Unstructured{}, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumes"}, ""))
}
// Assume the persistentvolume doesn't already exist in the cluster.
saResource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true}
dynamicFactory.On("ClientForGroupVersionResource", gv, saResource, test.namespace).Return(resourceClient, nil)
podResource := metav1.APIResource{Name: "pods", Namespaced: true}
dynamicFactory.On("ClientForGroupVersionResource", gv, podResource, test.namespace).Return(resourceClient, nil)
ctx := &context{
dynamicFactory: dynamicFactory,
actions: test.actions,
fileSystem: test.fileSystem,
selector: test.labelSelector,
restore: &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
Name: "my-restore",
},
Spec: api.RestoreSpec{
IncludeClusterResources: test.includeClusterResources,
BackupName: "my-backup",
},
},
backup: &api.Backup{},
log: velerotest.NewLogger(),
pvRestorer: &pvRestorer{
logger: logging.DefaultLogger(logrus.DebugLevel),
volumeSnapshotterGetter: &fakeVolumeSnapshotterGetter{
volumeMap: map[velerotest.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
volumeID: "volume-1",
},
snapshotLocationLister: snapshotLocationLister,
backup: &api.Backup{},
},
applicableActions: make(map[schema.GroupResource][]resolvedAction),
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
}
warnings, errors := ctx.restoreResource(test.resourcePath, test.namespace, test.resourcePath)
assert.Empty(t, warnings.Velero)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Equal(t, test.expectedErrors, errors)
})
}
}
func TestRestoreLabels(t *testing.T) {
tests := []struct {
name string
namespace string
resourcePath string
backupName string
restoreName string
labelSelector labels.Selector
includeClusterResources *bool
fileSystem *velerotest.FakeFileSystem
actions []resolvedAction
expectedErrors Result
expectedObjs []unstructured.Unstructured
}{
{
name: "backup name and restore name less than 63 characters",
namespace: "ns-1",
resourcePath: "configmaps",
backupName: "less-than-63-characters",
restoreName: "less-than-63-characters-12345",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
expectedObjs: toUnstructured(
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
api.BackupNameLabel: "less-than-63-characters",
api.RestoreNameLabel: "less-than-63-characters-12345",
}).ConfigMap,
),
},
{
name: "backup name equal to 63 characters",
namespace: "ns-1",
resourcePath: "configmaps",
backupName: "the-really-long-kube-service-name-that-is-exactly-63-characters",
restoreName: "the-really-long-kube-service-name-that-is-exactly-63-characters-12345",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
expectedObjs: toUnstructured(
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
api.BackupNameLabel: "the-really-long-kube-service-name-that-is-exactly-63-characters",
api.RestoreNameLabel: "the-really-long-kube-service-name-that-is-exactly-63-char0871f3",
}).ConfigMap,
),
},
{
name: "backup name greter than 63 characters",
namespace: "ns-1",
resourcePath: "configmaps",
backupName: "the-really-long-kube-service-name-that-is-much-greater-than-63-characters",
restoreName: "the-really-long-kube-service-name-that-is-much-greater-than-63-characters-12345",
labelSelector: labels.NewSelector(),
fileSystem: velerotest.NewFakeFileSystem().
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
expectedObjs: toUnstructured(
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
api.BackupNameLabel: "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
api.RestoreNameLabel: "the-really-long-kube-service-name-that-is-much-greater-th1bf26f",
}).ConfigMap,
),
},
}
var (
client = fake.NewSimpleClientset()
sharedInformers = informers.NewSharedInformerFactory(client, 0)
snapshotLocationLister = sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister()
)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resourceClient := &velerotest.FakeDynamicClient{}
for i := range test.expectedObjs {
resourceClient.On("Create", &test.expectedObjs[i]).Return(&test.expectedObjs[i], nil)
}
dynamicFactory := &velerotest.FakeDynamicFactory{}
gv := schema.GroupVersion{Group: "", Version: "v1"}
configMapResource := metav1.APIResource{Name: "configmaps", Namespaced: true}
dynamicFactory.On("ClientForGroupVersionResource", gv, configMapResource, test.namespace).Return(resourceClient, nil)
ctx := &context{
dynamicFactory: dynamicFactory,
actions: test.actions,
fileSystem: test.fileSystem,
selector: test.labelSelector,
restore: &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
Name: test.restoreName,
},
Spec: api.RestoreSpec{
IncludeClusterResources: test.includeClusterResources,
BackupName: test.backupName,
},
},
backup: &api.Backup{},
log: velerotest.NewLogger(),
pvRestorer: &pvRestorer{
logger: logging.DefaultLogger(logrus.DebugLevel),
volumeSnapshotterGetter: &fakeVolumeSnapshotterGetter{
volumeMap: map[velerotest.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
volumeID: "volume-1",
},
snapshotLocationLister: snapshotLocationLister,
backup: &api.Backup{},
},
applicableActions: make(map[schema.GroupResource][]resolvedAction),
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
}
warnings, errors := ctx.restoreResource(test.resourcePath, test.namespace, test.resourcePath)
assert.Empty(t, warnings.Velero)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Equal(t, test.expectedErrors, errors)
})
}
}
func TestRestoringExistingServiceAccount(t *testing.T) {
fromCluster := newTestServiceAccount()
fromClusterUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fromCluster.ServiceAccount)
require.NoError(t, err)
different := newTestServiceAccount().WithImagePullSecret("image-secret").WithSecret("secret")
differentUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(different.ServiceAccount)
require.NoError(t, err)
tests := []struct {
name string
expectedPatch []byte
fromBackup *unstructured.Unstructured
}{
{
name: "fromCluster and fromBackup are exactly the same",
fromBackup: &unstructured.Unstructured{Object: fromClusterUnstructured},
},
{
name: "fromCluster and fromBackup are different",
fromBackup: &unstructured.Unstructured{Object: differentUnstructured},
expectedPatch: []byte(`{"imagePullSecrets":[{"name":"image-secret"}],"secrets":[{"name":"secret"}]}`),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resourceClient := &velerotest.FakeDynamicClient{}
defer resourceClient.AssertExpectations(t)
name := fromCluster.GetName()
// restoreResource will add the restore label to object provided to create, so we need to make a copy to provide to our expected call
m := make(map[string]interface{})
for k, v := range test.fromBackup.Object {
m[k] = v
}
fromBackupWithLabel := &unstructured.Unstructured{Object: m}
addRestoreLabels(fromBackupWithLabel, "my-restore", "my-backup")
// resetMetadataAndStatus will strip the creationTimestamp before calling Create
fromBackupWithLabel.SetCreationTimestamp(metav1.Time{Time: time.Time{}})
resourceClient.On("Create", fromBackupWithLabel).Return(new(unstructured.Unstructured), k8serrors.NewAlreadyExists(kuberesource.ServiceAccounts, name))
resourceClient.On("Get", name, metav1.GetOptions{}).Return(&unstructured.Unstructured{Object: fromClusterUnstructured}, nil)
if len(test.expectedPatch) > 0 {
resourceClient.On("Patch", name, test.expectedPatch).Return(test.fromBackup, nil)
}
dynamicFactory := &velerotest.FakeDynamicFactory{}
gv := schema.GroupVersion{Group: "", Version: "v1"}
resource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true}
dynamicFactory.On("ClientForGroupVersionResource", gv, resource, "ns-1").Return(resourceClient, nil)
fromBackupJSON, err := json.Marshal(test.fromBackup)
require.NoError(t, err)
ctx := &context{
dynamicFactory: dynamicFactory,
actions: []resolvedAction{},
fileSystem: velerotest.NewFakeFileSystem().
WithFile("foo/resources/serviceaccounts/namespaces/ns-1/sa-1.json", fromBackupJSON),
selector: labels.NewSelector(),
restore: &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
Name: "my-restore",
},
Spec: api.RestoreSpec{
IncludeClusterResources: nil,
BackupName: "my-backup",
},
},
backup: &api.Backup{},
log: velerotest.NewLogger(),
applicableActions: make(map[schema.GroupResource][]resolvedAction),
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
}
warnings, errors := ctx.restoreResource("serviceaccounts", "ns-1", "foo/resources/serviceaccounts/namespaces/ns-1/")
assert.Empty(t, warnings.Velero)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Equal(t, Result{}, errors)
})
}
}
func TestRestoringPVsWithoutSnapshots(t *testing.T) {
pv := `apiVersion: v1
kind: PersistentVolume
@@ -1372,67 +955,6 @@ func toUnstructured(objs ...runtime.Object) []unstructured.Unstructured {
return res
}
type testServiceAccount struct {
*v1.ServiceAccount
}
func newTestServiceAccount() *testServiceAccount {
return &testServiceAccount{
ServiceAccount: &v1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ServiceAccount",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "test-sa",
CreationTimestamp: metav1.Time{Time: time.Now()},
},
},
}
}
func (sa *testServiceAccount) WithImagePullSecret(name string) *testServiceAccount {
secret := v1.LocalObjectReference{Name: name}
sa.ImagePullSecrets = append(sa.ImagePullSecrets, secret)
return sa
}
func (sa *testServiceAccount) WithSecret(name string) *testServiceAccount {
secret := v1.ObjectReference{Name: name}
sa.Secrets = append(sa.Secrets, secret)
return sa
}
func (sa *testServiceAccount) ToJSON() []byte {
bytes, _ := json.Marshal(sa.ServiceAccount)
return bytes
}
type testPersistentVolume struct {
*v1.PersistentVolume
}
func newTestPV() *testPersistentVolume {
return &testPersistentVolume{
PersistentVolume: &v1.PersistentVolume{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "PersistentVolume",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-pv",
},
Status: v1.PersistentVolumeStatus{},
},
}
}
func (pv *testPersistentVolume) ToJSON() []byte {
bytes, _ := json.Marshal(pv.PersistentVolume)
return bytes
}
type testNamespace struct {
*v1.Namespace
}
@@ -1452,60 +974,6 @@ func (ns *testNamespace) ToJSON() []byte {
return bytes
}
type testConfigMap struct {
*v1.ConfigMap
}
func newTestConfigMap() *testConfigMap {
return newNamedTestConfigMap("cm-1")
}
func newNamedTestConfigMap(name string) *testConfigMap {
return &testConfigMap{
ConfigMap: &v1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: name,
},
Data: map[string]string{
"foo": "bar",
},
},
}
}
func (cm *testConfigMap) WithNamespace(name string) *testConfigMap {
cm.Namespace = name
return cm
}
func (cm *testConfigMap) WithLabels(labels map[string]string) *testConfigMap {
cm.Labels = labels
return cm
}
func (cm *testConfigMap) WithControllerOwner() *testConfigMap {
t := true
ownerRef := metav1.OwnerReference{
Controller: &t,
}
cm.ConfigMap.OwnerReferences = append(cm.ConfigMap.OwnerReferences, ownerRef)
return cm
}
func (cm *testConfigMap) ToJSON() []byte {
bytes, _ := json.Marshal(cm.ConfigMap)
return bytes
}
type fakeAction struct {
resource string
}
type fakeVolumeSnapshotterGetter struct {
fakeVolumeSnapshotter *velerotest.FakeVolumeSnapshotter
volumeMap map[velerotest.VolumeBackupInfo]string
@@ -1521,42 +989,3 @@ func (r *fakeVolumeSnapshotterGetter) GetVolumeSnapshotter(provider string) (vel
}
return r.fakeVolumeSnapshotter, nil
}
func newFakeAction(resource string) *fakeAction {
return &fakeAction{resource}
}
func (r *fakeAction) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{r.resource},
}, nil
}
func (r *fakeAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
labels, found, err := unstructured.NestedMap(input.Item.UnstructuredContent(), "metadata", "labels")
if err != nil {
return nil, err
}
if !found {
labels = make(map[string]interface{})
}
labels["fake-restorer"] = "foo"
if err := unstructured.SetNestedField(input.Item.UnstructuredContent(), labels, "metadata", "labels"); err != nil {
return nil, err
}
unstructuredObj, ok := input.Item.(*unstructured.Unstructured)
if !ok {
return nil, errors.New("Unexpected type")
}
// want the baseline functionality too
res, err := resetMetadataAndStatus(unstructuredObj)
if err != nil {
return nil, err
}
return velero.NewRestoreItemActionExecuteOutput(res), nil
}

View File

@@ -294,3 +294,19 @@ func WithAnnotations(vals ...string) func(obj metav1.Object) {
obj.SetAnnotations(objAnnotations)
}
}
// WithClusterName is a functional option that applies the specified
// cluster name to an object.
func WithClusterName(val string) func(obj metav1.Object) {
return func(obj metav1.Object) {
obj.SetClusterName(val)
}
}
// WithFinalizers is a functional option that applies the specified
// finalizers to an object.
func WithFinalizers(vals ...string) func(obj metav1.Object) {
return func(obj metav1.Object) {
obj.SetFinalizers(vals)
}
}