diff --git a/docs/output-file-format.md b/docs/output-file-format.md index 7f7b92a073..11ab1310bb 100644 --- a/docs/output-file-format.md +++ b/docs/output-file-format.md @@ -62,26 +62,38 @@ Note that this file includes detailed info about your volume snapshots in the `s When unzipped, a typical backup directory (e.g. `backup1234.tar.gz`) looks like the following: ``` -cluster/ +resources/ persistentvolumes/ - pv01.json - ... -namespaces/ - namespace1/ - configmaps/ - myconfigmap.json + cluster/ + pv01.json ... - pods - mypod.json - ... - jobs - awesome-job.json - ... - deployments - cool-deployment.json - ... - ... - namespace2/ - ... + configmaps/ + namespaces/ + namespace1/ + myconfigmap.json + ... + namespace2/ + ... + pods/ + namespaces/ + namespace1/ + mypod.json + ... + namespace2/ + ... + jobs/ + namespaces/ + namespace1/ + awesome-job.json + ... + namespace2/ + ... + deployments/ + namespaces/ + namespace1/ + cool-deployment.json + ... + namespace2/ + ... ... ``` diff --git a/pkg/apis/ark/v1/constants.go b/pkg/apis/ark/v1/constants.go index fb9b1ade09..236db03303 100644 --- a/pkg/apis/ark/v1/constants.go +++ b/pkg/apis/ark/v1/constants.go @@ -21,6 +21,10 @@ const ( // the Ark server and API objects. DefaultNamespace = "heptio-ark" + // ResourcesDir is a top-level directory expected in backups which contains sub-directories + // for each resource type in the backup. + ResourcesDir = "resources" + // RestoreLabelKey is the label key that's applied to all resources that // are created during a restore. This is applied for ease of identification // of restored resources. The value will be the restore's name. diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index dff5e0cc8d..2bbb7ce164 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -22,6 +22,7 @@ import ( "encoding/json" "fmt" "io" + "path/filepath" "strings" "time" @@ -473,9 +474,9 @@ func (ib *realItemBackupper) backupItem(ctx *backupContext, item map[string]inte var filePath string if namespace != "" { - filePath = strings.Join([]string{api.NamespaceScopedDir, namespace, groupResource.String(), name + ".json"}, "/") + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.NamespaceScopedDir, namespace, name+".json") } else { - filePath = strings.Join([]string{api.ClusterScopedDir, groupResource.String(), name + ".json"}, "/") + filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.ClusterScopedDir, name+".json") } itemBytes, err := json.Marshal(item) diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index aa99eb8dca..0ea9b803be 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -439,14 +439,14 @@ func TestBackupMethod(t *testing.T) { require.NoError(t, err) expectedFiles := sets.NewString( - "namespaces/a/configmaps/configMap1.json", - "namespaces/b/configmaps/configMap2.json", - "namespaces/a/roles.rbac.authorization.k8s.io/role1.json", + "resources/configmaps/namespaces/a/configMap1.json", + "resources/configmaps/namespaces/b/configMap2.json", + "resources/roles.rbac.authorization.k8s.io/namespaces/a/role1.json", // CSRs are not expected because they're unrelated cluster-scoped resources ) expectedData := map[string]string{ - "namespaces/a/configmaps/configMap1.json": ` + "resources/configmaps/namespaces/a/configMap1.json": ` { "apiVersion": "v1", "kind": "ConfigMap", @@ -458,7 +458,7 @@ func TestBackupMethod(t *testing.T) { "a": "b" } }`, - "namespaces/b/configmaps/configMap2.json": ` + "resources/configmaps/namespaces/b/configMap2.json": ` { "apiVersion": "v1", "kind": "ConfigMap", @@ -471,7 +471,7 @@ func TestBackupMethod(t *testing.T) { } } `, - "namespaces/a/roles.rbac.authorization.k8s.io/role1.json": ` + "resources/roles.rbac.authorization.k8s.io/namespaces/a/role1.json": ` { "apiVersion": "rbac.authorization.k8s.io/v1beta1", "kind": "Role", @@ -1114,7 +1114,7 @@ func TestBackupItem(t *testing.T) { namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo"), expectError: false, expectExcluded: false, - expectedTarHeaderName: "namespaces/foo/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json", }, { name: "* namespace include", @@ -1122,21 +1122,21 @@ func TestBackupItem(t *testing.T) { namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"), expectError: false, expectExcluded: false, - expectedTarHeaderName: "namespaces/foo/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json", }, { name: "cluster-scoped", item: `{"metadata":{"name":"bar"}}`, expectError: false, expectExcluded: false, - expectedTarHeaderName: "cluster/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/cluster/bar.json", }, { name: "make sure status is deleted", item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`, expectError: false, expectExcluded: false, - expectedTarHeaderName: "cluster/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/cluster/bar.json", }, { name: "tar header write error", @@ -1156,7 +1156,7 @@ func TestBackupItem(t *testing.T) { item: `{"metadata":{"name":"bar"}}`, expectError: false, expectExcluded: false, - expectedTarHeaderName: "cluster/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/cluster/bar.json", customAction: true, expectedActionID: "bar", }, @@ -1166,7 +1166,7 @@ func TestBackupItem(t *testing.T) { item: `{"metadata":{"namespace": "myns", "name":"bar"}}`, expectError: false, expectExcluded: false, - expectedTarHeaderName: "namespaces/myns/resource.group/bar.json", + expectedTarHeaderName: "resources/resource.group/namespaces/myns/bar.json", customAction: true, expectedActionID: "myns/bar", }, diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 72aeb21c05..f32077db59 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -20,10 +20,10 @@ import ( "archive/tar" "compress/gzip" "encoding/json" + "errors" "fmt" "io" "os" - "path" "path/filepath" "sort" @@ -257,55 +257,107 @@ func (ctx *context) execute() (api.RestoreResult, api.RestoreResult) { // restoreFromDir executes a restore based on backup data contained within a local // directory. func (ctx *context) restoreFromDir(dir string) (api.RestoreResult, api.RestoreResult) { - warnings, errors := api.RestoreResult{}, api.RestoreResult{} + warnings, errs := api.RestoreResult{}, api.RestoreResult{} - // cluster-scoped - clusterPath := path.Join(dir, api.ClusterScopedDir) - exists, err := ctx.fileSystem.DirExists(clusterPath) + namespaceFilter := collections.NewIncludesExcludes().Includes(ctx.restore.Spec.IncludedNamespaces...).Excludes(ctx.restore.Spec.ExcludedNamespaces...) + + // Make sure the top level "resources" dir exists: + resourcesDir := filepath.Join(dir, api.ResourcesDir) + rde, err := ctx.fileSystem.DirExists(resourcesDir) if err != nil { - errors.Cluster = []string{err.Error()} + addArkError(&errs, err) + return warnings, errs } - if exists { - w, e := ctx.restoreNamespace("", clusterPath) - merge(&warnings, &w) - merge(&errors, &e) + if !rde { + addArkError(&errs, errors.New("backup does not contain top level resources directory")) } - // namespace-scoped - namespacesPath := path.Join(dir, api.NamespaceScopedDir) - exists, err = ctx.fileSystem.DirExists(namespacesPath) + resourceDirs, err := ctx.fileSystem.ReadDir(resourcesDir) if err != nil { - addArkError(&errors, err) - return warnings, errors - } - if !exists { - return warnings, errors + addArkError(&errs, err) + return warnings, errs } - nses, err := ctx.fileSystem.ReadDir(namespacesPath) - if err != nil { - addArkError(&errors, err) - return warnings, errors + resourceDirsMap := make(map[string]os.FileInfo) + + for _, rscDir := range resourceDirs { + rscName := rscDir.Name() + resourceDirsMap[rscName] = rscDir } - namespaceFilter := collections.NewIncludesExcludes().Includes(ctx.restore.Spec.IncludedNamespaces...).Excludes(ctx.restore.Spec.ExcludedNamespaces...) - for _, ns := range nses { - if !ns.IsDir() { + for _, resource := range ctx.prioritizedResources { + rscDir := resourceDirsMap[resource.String()] + if rscDir == nil { + continue + } + + resourcePath := filepath.Join(resourcesDir, rscDir.Name()) + + clusterSubDir := filepath.Join(resourcePath, api.ClusterScopedDir) + clusterSubDirExists, err := ctx.fileSystem.DirExists(clusterSubDir) + if err != nil { + addArkError(&errs, err) + return warnings, errs + } + if clusterSubDirExists { + w, e := ctx.restoreResource(resource.String(), "", clusterSubDir) + merge(&warnings, &w) + merge(&errs, &e) continue } - nsPath := path.Join(namespacesPath, ns.Name()) - if !namespaceFilter.ShouldInclude(ns.Name()) { - ctx.infof("Skipping namespace %s", ns.Name()) + nsSubDir := filepath.Join(resourcePath, api.NamespaceScopedDir) + nsSubDirExists, err := ctx.fileSystem.DirExists(nsSubDir) + if err != nil { + addArkError(&errs, err) + return warnings, errs + } + if !nsSubDirExists { continue } - w, e := ctx.restoreNamespace(ns.Name(), nsPath) - merge(&warnings, &w) - merge(&errors, &e) + nsDirs, err := ctx.fileSystem.ReadDir(nsSubDir) + if err != nil { + addArkError(&errs, err) + return warnings, errs + } + + for _, nsDir := range nsDirs { + if !nsDir.IsDir() { + continue + } + nsName := nsDir.Name() + nsPath := filepath.Join(nsSubDir, nsName) + + if !namespaceFilter.ShouldInclude(nsName) { + ctx.infof("Skipping namespace %s", nsName) + continue + } + + // fetch mapped NS name + mappedNsName := nsName + if target, ok := ctx.restore.Spec.NamespaceMapping[nsName]; ok { + mappedNsName = target + } + + // ensure namespace exists + ns := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: mappedNsName, + }, + } + if _, err := kube.EnsureNamespaceExists(ns, ctx.namespaceClient); err != nil { + addArkError(&errs, err) + continue + } + + w, e := ctx.restoreResource(resource.String(), nsName, nsPath) + merge(&warnings, &w) + merge(&errs, &e) + } } - return warnings, errors + return warnings, errs } // merge combines two RestoreResult objects into one @@ -340,94 +392,38 @@ func addToResult(r *api.RestoreResult, ns string, e error) { } } -// restoreNamespace restores the resources from a specified namespace directory in the backup, -// or from the cluster-scoped directory if no namespace is specified. -func (ctx *context) restoreNamespace(nsName, nsPath string) (api.RestoreResult, api.RestoreResult) { - warnings, errors := api.RestoreResult{}, api.RestoreResult{} +// restoreResource restores the specified cluster or namespace scoped resource. If namespace is +// empty we are restoring a cluster level resource, otherwise into the specified namespace. +func (ctx *context) restoreResource(resource, namespace, resourcePath string) (api.RestoreResult, api.RestoreResult) { + warnings, errs := api.RestoreResult{}, api.RestoreResult{} - if nsName == "" { - ctx.infof("Restoring cluster-scoped resources") + if namespace != "" { + ctx.infof("Restoring resource '%s' into namespace '%s' from: %s", resource, namespace, resourcePath) } else { - ctx.infof("Restoring namespace %s", nsName) + ctx.infof("Restoring cluster level resource '%s' from: %s", resource, resourcePath) } - resourceDirs, err := ctx.fileSystem.ReadDir(nsPath) - if err != nil { - addToResult(&errors, nsName, err) - return warnings, errors - } - - resourceDirsMap := make(map[string]os.FileInfo) - - for _, rscDir := range resourceDirs { - rscName := rscDir.Name() - resourceDirsMap[rscName] = rscDir - } - - if nsName != "" { - // fetch mapped NS name - if target, ok := ctx.restore.Spec.NamespaceMapping[nsName]; ok { - nsName = target - } - - // ensure namespace exists - ns := &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: nsName, - }, - } - - if _, err := kube.EnsureNamespaceExists(ns, ctx.namespaceClient); err != nil { - addArkError(&errors, err) - return warnings, errors - } - } - - for _, resource := range ctx.prioritizedResources { - rscDir := resourceDirsMap[resource.String()] - if rscDir == nil { - continue - } - - resourcePath := path.Join(nsPath, rscDir.Name()) - - w, e := ctx.restoreResourceForNamespace(nsName, resourcePath) - merge(&warnings, &w) - merge(&errors, &e) - } - - return warnings, errors -} - -// restoreResourceForNamespace restores the specified resource type for the specified -// namespace (or blank for cluster-scoped resources). -func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath string) (api.RestoreResult, api.RestoreResult) { - warnings, errors := api.RestoreResult{}, api.RestoreResult{} - resource := path.Base(resourcePath) - - ctx.infof("Restoring resource %v into namespace %v", resource, namespace) - files, err := ctx.fileSystem.ReadDir(resourcePath) if err != nil { - addToResult(&errors, namespace, fmt.Errorf("error reading %q resource directory: %v", resource, err)) - return warnings, errors + addToResult(&errs, namespace, fmt.Errorf("error reading %q resource directory: %v", resource, err)) + return warnings, errs } if len(files) == 0 { - return warnings, errors + return warnings, errs } var ( resourceClient client.Dynamic restorer restorers.ResourceRestorer waiter *resourceWaiter - groupResource = schema.ParseGroupResource(path.Base(resourcePath)) + groupResource = schema.ParseGroupResource(resource) ) for _, file := range files { fullPath := filepath.Join(resourcePath, file.Name()) obj, err := ctx.unmarshal(fullPath) if err != nil { - addToResult(&errors, namespace, fmt.Errorf("error decoding %q: %v", fullPath, err)) + addToResult(&errs, namespace, fmt.Errorf("error decoding %q: %v", fullPath, err)) continue } @@ -448,8 +444,8 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s var err error resourceClient, err = ctx.dynamicFactory.ClientForGroupVersionKind(obj.GroupVersionKind(), resource, namespace) if err != nil { - addArkError(&errors, fmt.Errorf("error getting resource client for namespace %q, resource %q: %v", namespace, &groupResource, err)) - return warnings, errors + addArkError(&errs, fmt.Errorf("error getting resource client for namespace %q, resource %q: %v", namespace, &groupResource, err)) + return warnings, errs } restorer = ctx.restorers[groupResource] @@ -463,8 +459,8 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s if restorer.Wait() { itmWatch, err := resourceClient.Watch(metav1.ListOptions{}) if err != nil { - addArkError(&errors, fmt.Errorf("error watching for namespace %q, resource %q: %v", namespace, &groupResource, err)) - return warnings, errors + addArkError(&errs, fmt.Errorf("error watching for namespace %q, resource %q: %v", namespace, &groupResource, err)) + return warnings, errs } watchChan := itmWatch.ResultChan() defer itmWatch.Stop() @@ -487,13 +483,13 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s addToResult(&warnings, namespace, fmt.Errorf("warning preparing %s: %v", fullPath, warning)) } if err != nil { - addToResult(&errors, namespace, fmt.Errorf("error preparing %s: %v", fullPath, err)) + addToResult(&errs, namespace, fmt.Errorf("error preparing %s: %v", fullPath, err)) continue } unstructuredObj, ok := preparedObj.(*unstructured.Unstructured) if !ok { - addToResult(&errors, namespace, fmt.Errorf("%s: unexpected type %T", fullPath, preparedObj)) + addToResult(&errs, namespace, fmt.Errorf("%s: unexpected type %T", fullPath, preparedObj)) continue } @@ -511,7 +507,7 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s } if err != nil { ctx.infof("error restoring %s: %v", unstructuredObj.GetName(), err) - addToResult(&errors, namespace, fmt.Errorf("error restoring %s: %v", fullPath, err)) + addToResult(&errs, namespace, fmt.Errorf("error restoring %s: %v", fullPath, err)) continue } @@ -522,11 +518,11 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s if waiter != nil { if err := waiter.Wait(); err != nil { - addArkError(&errors, fmt.Errorf("error waiting for all %v resources to be created in namespace %s: %v", &groupResource, namespace, err)) + addArkError(&errs, fmt.Errorf("error waiting for all %v resources to be created in namespace %s: %v", &groupResource, namespace, err)) } } - return warnings, errors + return warnings, errs } // addLabel applies the specified key/value to an object as a label. @@ -604,7 +600,7 @@ func (ctx *context) readBackup(tarRdr *tar.Reader) (string, error) { return "", err } - target := path.Join(dir, header.Name) + target := filepath.Join(dir, header.Name) switch header.Typeflag { case tar.TypeDir: @@ -616,7 +612,7 @@ func (ctx *context) readBackup(tarRdr *tar.Reader) (string, error) { case tar.TypeReg: // make sure we have the directory created - err := ctx.fileSystem.MkdirAll(path.Dir(target), header.FileInfo().Mode()) + err := ctx.fileSystem.MkdirAll(filepath.Dir(target), header.FileInfo().Mode()) if err != nil { ctx.infof("mkdirall error: %v", err) return "", err diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 39e9cadad0..d207e0a998 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -116,62 +116,58 @@ func TestPrioritizeResources(t *testing.T) { } } -func TestRestoreMethod(t *testing.T) { +func TestRestoreNamespaceFiltering(t *testing.T) { tests := []struct { - name string - fileSystem *fakeFileSystem - baseDir string - restore *api.Restore - expectedReadDirs []string + name string + fileSystem *fakeFileSystem + baseDir string + restore *api.Restore + expectedReadDirs []string + prioritizedResources []schema.GroupResource }{ - { - name: "cluster comes before namespaced", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces"), - baseDir: "bak", - restore: &api.Restore{Spec: api.RestoreSpec{}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces"}, - }, - { - name: "namespaces dir is not read & does not error if it does not exist", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster"), - baseDir: "bak", - restore: &api.Restore{Spec: api.RestoreSpec{}}, - expectedReadDirs: []string{"bak/cluster"}, - }, { name: "namespacesToRestore having * restores all namespaces", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"), + fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"), baseDir: "bak", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"}, + prioritizedResources: []schema.GroupResource{ + schema.GroupResource{Resource: "nodes"}, + schema.GroupResource{Resource: "secrets"}, + }, }, { name: "namespacesToRestore properly filters", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"), + fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"), baseDir: "bak", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"b", "c"}}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/b", "bak/namespaces/c"}, - }, - { - name: "namespacesToRestore properly filters with inclusion filter", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"), - baseDir: "bak", - restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"b", "c"}}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/b", "bak/namespaces/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"}, + prioritizedResources: []schema.GroupResource{ + schema.GroupResource{Resource: "nodes"}, + schema.GroupResource{Resource: "secrets"}, + }, }, { name: "namespacesToRestore properly filters with exclusion filter", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"), + fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"), baseDir: "bak", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}, ExcludedNamespaces: []string{"a"}}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/b", "bak/namespaces/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"}, + prioritizedResources: []schema.GroupResource{ + schema.GroupResource{Resource: "nodes"}, + schema.GroupResource{Resource: "secrets"}, + }, }, { name: "namespacesToRestore properly filters with inclusion & exclusion filters", - fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"), + fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"), baseDir: "bak", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"a", "b", "c"}, ExcludedNamespaces: []string{"b"}}}, - expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/a", "bak/namespaces/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/c"}, + prioritizedResources: []schema.GroupResource{ + schema.GroupResource{Resource: "nodes"}, + schema.GroupResource{Resource: "secrets"}, + }, }, } @@ -180,10 +176,11 @@ func TestRestoreMethod(t *testing.T) { log, _ := testlogger.NewNullLogger() ctx := &context{ - restore: test.restore, - namespaceClient: &fakeNamespaceClient{}, - fileSystem: test.fileSystem, - logger: log, + restore: test.restore, + namespaceClient: &fakeNamespaceClient{}, + fileSystem: test.fileSystem, + logger: log, + prioritizedResources: test.prioritizedResources, } warnings, errors := ctx.restoreFromDir(test.baseDir) @@ -199,62 +196,59 @@ func TestRestoreMethod(t *testing.T) { } } -func TestRestoreNamespace(t *testing.T) { +func TestRestorePriority(t *testing.T) { tests := []struct { name string fileSystem *fakeFileSystem restore *api.Restore - namespace string - path string + baseDir string prioritizedResources []schema.GroupResource expectedErrors api.RestoreResult expectedReadDirs []string }{ { name: "cluster test", - fileSystem: newFakeFileSystem().WithDirectory("bak/cluster/a").WithDirectory("bak/cluster/c"), - namespace: "", - path: "bak/cluster", + fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/cluster").WithDirectory("bak/resources/c/cluster"), + baseDir: "bak", + restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}}, prioritizedResources: []schema.GroupResource{ schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "c"}, }, - expectedReadDirs: []string{"bak/cluster", "bak/cluster/a", "bak/cluster/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/a/cluster", "bak/resources/c/cluster"}, }, { name: "resource priorities are applied", - fileSystem: newFakeFileSystem().WithDirectory("bak/cluster/a").WithDirectory("bak/cluster/c"), - namespace: "", - path: "bak/cluster", + fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/cluster").WithDirectory("bak/resources/c/cluster"), + restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}}, + baseDir: "bak", prioritizedResources: []schema.GroupResource{ schema.GroupResource{Resource: "c"}, schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "a"}, }, - expectedReadDirs: []string{"bak/cluster", "bak/cluster/c", "bak/cluster/a"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/c/cluster", "bak/resources/a/cluster"}, }, { name: "basic namespace", - fileSystem: newFakeFileSystem().WithDirectory("bak/namespaces/ns-1/a").WithDirectory("bak/namespaces/ns-1/c"), - restore: &api.Restore{Spec: api.RestoreSpec{NamespaceMapping: make(map[string]string)}}, - namespace: "ns-1", - path: "bak/namespaces/ns-1", + fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/namespaces/ns-1").WithDirectory("bak/resources/c/namespaces/ns-1"), + restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}}, + baseDir: "bak", prioritizedResources: []schema.GroupResource{ schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "c"}, }, - expectedReadDirs: []string{"bak/namespaces/ns-1", "bak/namespaces/ns-1/a", "bak/namespaces/ns-1/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/a/namespaces", "bak/resources/a/namespaces/ns-1", "bak/resources/c/namespaces", "bak/resources/c/namespaces/ns-1"}, }, { name: "error in a single resource doesn't terminate restore immediately, but is returned", fileSystem: newFakeFileSystem(). - WithFile("bak/namespaces/ns-1/a/invalid-json.json", []byte("invalid json")). - WithDirectory("bak/namespaces/ns-1/c"), - restore: &api.Restore{Spec: api.RestoreSpec{NamespaceMapping: make(map[string]string)}}, - namespace: "ns-1", - path: "bak/namespaces/ns-1", + 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{ schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "b"}, @@ -262,10 +256,10 @@ func TestRestoreNamespace(t *testing.T) { }, expectedErrors: api.RestoreResult{ Namespaces: map[string][]string{ - "ns-1": {"error decoding \"bak/namespaces/ns-1/a/invalid-json.json\": invalid character 'i' looking for beginning of value"}, + "ns-1": {"error decoding \"bak/resources/a/namespaces/ns-1/invalid-json.json\": invalid character 'i' looking for beginning of value"}, }, }, - expectedReadDirs: []string{"bak/namespaces/ns-1", "bak/namespaces/ns-1/a", "bak/namespaces/ns-1/c"}, + expectedReadDirs: []string{"bak/resources", "bak/resources/a/namespaces", "bak/resources/a/namespaces/ns-1", "bak/resources/c/namespaces", "bak/resources/c/namespaces/ns-1"}, }, } @@ -281,7 +275,7 @@ func TestRestoreNamespace(t *testing.T) { logger: log, } - warnings, errors := ctx.restoreNamespace(test.namespace, test.path) + warnings, errors := ctx.restoreFromDir(test.baseDir) assert.Empty(t, warnings.Ark) assert.Empty(t, warnings.Cluster) @@ -431,7 +425,7 @@ func TestRestoreResourceForNamespace(t *testing.T) { logger: log, } - warnings, errors := ctx.restoreResourceForNamespace(test.namespace, test.resourcePath) + warnings, errors := ctx.restoreResource("configmaps", test.namespace, test.resourcePath) assert.Empty(t, warnings.Ark) assert.Empty(t, warnings.Cluster)