From 81c2adc0598601ccb44a7879b5133aab126713bf Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Thu, 27 Jun 2019 12:57:47 -0600 Subject: [PATCH] Refactor pkg/restore tests (part 2) (#1606) * update TestPrioritizeResources to use real discovery helper Signed-off-by: Steve Kriss * migrate invalid tarball contents tests Signed-off-by: Steve Kriss * migrate item restore tests Signed-off-by: Steve Kriss * migrate restore item action tests Signed-off-by: Steve Kriss --- pkg/restore/restore.go | 3 +- pkg/restore/restore_new_test.go | 507 ++++++++++++++++++++++++++ pkg/restore/restore_test.go | 627 ++------------------------------ pkg/test/resources.go | 16 + 4 files changed, 553 insertions(+), 600 deletions(-) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 322a3fe75..43e24edd3 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -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 } diff --git a/pkg/restore/restore_new_test.go b/pkg/restore/restore_new_test.go index 6e172696e..776d08dfd 100644 --- a/pkg/restore/restore_new_test.go +++ b/pkg/restore/restore_new_test.go @@ -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 /). 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) + } +} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index d0dfb25eb..fb98030e2 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -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 -} diff --git a/pkg/test/resources.go b/pkg/test/resources.go index d3a221813..f0c1dce32 100644 --- a/pkg/test/resources.go +++ b/pkg/test/resources.go @@ -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) + } +}