Restructure backups for resource prioritization.

Previously the directory structure separated resources depending on
whether or not they were cluster or namespace scoped. All cluster
resources were restored first, then all namespace resources. Priority
did not apply across both and you could not order any namespace
resources before any cluster resources.

This restructure sorts firstly on resource type.

resources/serviceaccounts/namespaces/ns1.json
resources/nodes/cluster/node1.json

This will break old backups as the format is no longer consistent as
announced on the Google group.

Signed-off-by: Devan Goodwin <dgoodwin@redhat.com>
pull/132/head
Devan Goodwin 2017-10-10 15:43:53 -03:00
parent 4fe50ed782
commit ed0194c09b
6 changed files with 228 additions and 221 deletions

View File

@ -62,24 +62,36 @@ 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: When unzipped, a typical backup directory (e.g. `backup1234.tar.gz`) looks like the following:
``` ```
cluster/ resources/
persistentvolumes/ persistentvolumes/
cluster/
pv01.json pv01.json
... ...
namespaces/
namespace1/
configmaps/ configmaps/
namespaces/
namespace1/
myconfigmap.json myconfigmap.json
... ...
pods namespace2/
...
pods/
namespaces/
namespace1/
mypod.json mypod.json
... ...
jobs namespace2/
...
jobs/
namespaces/
namespace1/
awesome-job.json awesome-job.json
... ...
deployments namespace2/
cool-deployment.json
... ...
deployments/
namespaces/
namespace1/
cool-deployment.json
... ...
namespace2/ namespace2/
... ...

View File

@ -21,6 +21,10 @@ const (
// the Ark server and API objects. // the Ark server and API objects.
DefaultNamespace = "heptio-ark" 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 // RestoreLabelKey is the label key that's applied to all resources that
// are created during a restore. This is applied for ease of identification // are created during a restore. This is applied for ease of identification
// of restored resources. The value will be the restore's name. // of restored resources. The value will be the restore's name.

View File

@ -22,6 +22,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"path/filepath"
"strings" "strings"
"time" "time"
@ -473,9 +474,9 @@ func (ib *realItemBackupper) backupItem(ctx *backupContext, item map[string]inte
var filePath string var filePath string
if namespace != "" { 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 { } 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) itemBytes, err := json.Marshal(item)

View File

@ -439,14 +439,14 @@ func TestBackupMethod(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
expectedFiles := sets.NewString( expectedFiles := sets.NewString(
"namespaces/a/configmaps/configMap1.json", "resources/configmaps/namespaces/a/configMap1.json",
"namespaces/b/configmaps/configMap2.json", "resources/configmaps/namespaces/b/configMap2.json",
"namespaces/a/roles.rbac.authorization.k8s.io/role1.json", "resources/roles.rbac.authorization.k8s.io/namespaces/a/role1.json",
// CSRs are not expected because they're unrelated cluster-scoped resources // CSRs are not expected because they're unrelated cluster-scoped resources
) )
expectedData := map[string]string{ expectedData := map[string]string{
"namespaces/a/configmaps/configMap1.json": ` "resources/configmaps/namespaces/a/configMap1.json": `
{ {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "ConfigMap", "kind": "ConfigMap",
@ -458,7 +458,7 @@ func TestBackupMethod(t *testing.T) {
"a": "b" "a": "b"
} }
}`, }`,
"namespaces/b/configmaps/configMap2.json": ` "resources/configmaps/namespaces/b/configMap2.json": `
{ {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "ConfigMap", "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", "apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "Role", "kind": "Role",
@ -1114,7 +1114,7 @@ func TestBackupItem(t *testing.T) {
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo"), namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo"),
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "namespaces/foo/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json",
}, },
{ {
name: "* namespace include", name: "* namespace include",
@ -1122,21 +1122,21 @@ func TestBackupItem(t *testing.T) {
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"), namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "namespaces/foo/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/namespaces/foo/bar.json",
}, },
{ {
name: "cluster-scoped", name: "cluster-scoped",
item: `{"metadata":{"name":"bar"}}`, item: `{"metadata":{"name":"bar"}}`,
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/cluster/bar.json",
}, },
{ {
name: "make sure status is deleted", name: "make sure status is deleted",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`, item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/cluster/bar.json",
}, },
{ {
name: "tar header write error", name: "tar header write error",
@ -1156,7 +1156,7 @@ func TestBackupItem(t *testing.T) {
item: `{"metadata":{"name":"bar"}}`, item: `{"metadata":{"name":"bar"}}`,
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/cluster/bar.json",
customAction: true, customAction: true,
expectedActionID: "bar", expectedActionID: "bar",
}, },
@ -1166,7 +1166,7 @@ func TestBackupItem(t *testing.T) {
item: `{"metadata":{"namespace": "myns", "name":"bar"}}`, item: `{"metadata":{"namespace": "myns", "name":"bar"}}`,
expectError: false, expectError: false,
expectExcluded: false, expectExcluded: false,
expectedTarHeaderName: "namespaces/myns/resource.group/bar.json", expectedTarHeaderName: "resources/resource.group/namespaces/myns/bar.json",
customAction: true, customAction: true,
expectedActionID: "myns/bar", expectedActionID: "myns/bar",
}, },

View File

@ -20,10 +20,10 @@ import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "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 // restoreFromDir executes a restore based on backup data contained within a local
// directory. // directory.
func (ctx *context) restoreFromDir(dir string) (api.RestoreResult, api.RestoreResult) { 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)
if err != nil {
errors.Cluster = []string{err.Error()}
}
if exists {
w, e := ctx.restoreNamespace("", clusterPath)
merge(&warnings, &w)
merge(&errors, &e)
}
// namespace-scoped
namespacesPath := path.Join(dir, api.NamespaceScopedDir)
exists, err = ctx.fileSystem.DirExists(namespacesPath)
if err != nil {
addArkError(&errors, err)
return warnings, errors
}
if !exists {
return warnings, errors
}
nses, err := ctx.fileSystem.ReadDir(namespacesPath)
if err != nil {
addArkError(&errors, err)
return warnings, errors
}
namespaceFilter := collections.NewIncludesExcludes().Includes(ctx.restore.Spec.IncludedNamespaces...).Excludes(ctx.restore.Spec.ExcludedNamespaces...) namespaceFilter := collections.NewIncludesExcludes().Includes(ctx.restore.Spec.IncludedNamespaces...).Excludes(ctx.restore.Spec.ExcludedNamespaces...)
for _, ns := range nses {
if !ns.IsDir() {
continue
}
nsPath := path.Join(namespacesPath, ns.Name())
if !namespaceFilter.ShouldInclude(ns.Name()) { // Make sure the top level "resources" dir exists:
ctx.infof("Skipping namespace %s", ns.Name()) resourcesDir := filepath.Join(dir, api.ResourcesDir)
rde, err := ctx.fileSystem.DirExists(resourcesDir)
if err != nil {
addArkError(&errs, err)
return warnings, errs
}
if !rde {
addArkError(&errs, errors.New("backup does not contain top level resources directory"))
}
resourceDirs, err := ctx.fileSystem.ReadDir(resourcesDir)
if err != nil {
addArkError(&errs, err)
return warnings, errs
}
resourceDirsMap := make(map[string]os.FileInfo)
for _, rscDir := range resourceDirs {
rscName := rscDir.Name()
resourceDirsMap[rscName] = rscDir
}
for _, resource := range ctx.prioritizedResources {
rscDir := resourceDirsMap[resource.String()]
if rscDir == nil {
continue continue
} }
w, e := ctx.restoreNamespace(ns.Name(), nsPath) 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(&warnings, &w)
merge(&errors, &e) merge(&errs, &e)
continue
} }
return warnings, errors nsSubDir := filepath.Join(resourcePath, api.NamespaceScopedDir)
nsSubDirExists, err := ctx.fileSystem.DirExists(nsSubDir)
if err != nil {
addArkError(&errs, err)
return warnings, errs
}
if !nsSubDirExists {
continue
}
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, errs
} }
// merge combines two RestoreResult objects into one // 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, // restoreResource restores the specified cluster or namespace scoped resource. If namespace is
// or from the cluster-scoped directory if no namespace is specified. // empty we are restoring a cluster level resource, otherwise into the specified namespace.
func (ctx *context) restoreNamespace(nsName, nsPath string) (api.RestoreResult, api.RestoreResult) { func (ctx *context) restoreResource(resource, namespace, resourcePath string) (api.RestoreResult, api.RestoreResult) {
warnings, errors := api.RestoreResult{}, api.RestoreResult{} warnings, errs := api.RestoreResult{}, api.RestoreResult{}
if nsName == "" { if namespace != "" {
ctx.infof("Restoring cluster-scoped resources") ctx.infof("Restoring resource '%s' into namespace '%s' from: %s", resource, namespace, resourcePath)
} else { } 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) files, err := ctx.fileSystem.ReadDir(resourcePath)
if err != nil { if err != nil {
addToResult(&errors, namespace, fmt.Errorf("error reading %q resource directory: %v", resource, err)) addToResult(&errs, namespace, fmt.Errorf("error reading %q resource directory: %v", resource, err))
return warnings, errors return warnings, errs
} }
if len(files) == 0 { if len(files) == 0 {
return warnings, errors return warnings, errs
} }
var ( var (
resourceClient client.Dynamic resourceClient client.Dynamic
restorer restorers.ResourceRestorer restorer restorers.ResourceRestorer
waiter *resourceWaiter waiter *resourceWaiter
groupResource = schema.ParseGroupResource(path.Base(resourcePath)) groupResource = schema.ParseGroupResource(resource)
) )
for _, file := range files { for _, file := range files {
fullPath := filepath.Join(resourcePath, file.Name()) fullPath := filepath.Join(resourcePath, file.Name())
obj, err := ctx.unmarshal(fullPath) obj, err := ctx.unmarshal(fullPath)
if err != nil { 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 continue
} }
@ -448,8 +444,8 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s
var err error var err error
resourceClient, err = ctx.dynamicFactory.ClientForGroupVersionKind(obj.GroupVersionKind(), resource, namespace) resourceClient, err = ctx.dynamicFactory.ClientForGroupVersionKind(obj.GroupVersionKind(), resource, namespace)
if err != nil { if err != nil {
addArkError(&errors, fmt.Errorf("error getting resource client for namespace %q, resource %q: %v", namespace, &groupResource, err)) addArkError(&errs, fmt.Errorf("error getting resource client for namespace %q, resource %q: %v", namespace, &groupResource, err))
return warnings, errors return warnings, errs
} }
restorer = ctx.restorers[groupResource] restorer = ctx.restorers[groupResource]
@ -463,8 +459,8 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s
if restorer.Wait() { if restorer.Wait() {
itmWatch, err := resourceClient.Watch(metav1.ListOptions{}) itmWatch, err := resourceClient.Watch(metav1.ListOptions{})
if err != nil { if err != nil {
addArkError(&errors, fmt.Errorf("error watching for namespace %q, resource %q: %v", namespace, &groupResource, err)) addArkError(&errs, fmt.Errorf("error watching for namespace %q, resource %q: %v", namespace, &groupResource, err))
return warnings, errors return warnings, errs
} }
watchChan := itmWatch.ResultChan() watchChan := itmWatch.ResultChan()
defer itmWatch.Stop() 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)) addToResult(&warnings, namespace, fmt.Errorf("warning preparing %s: %v", fullPath, warning))
} }
if err != nil { 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 continue
} }
unstructuredObj, ok := preparedObj.(*unstructured.Unstructured) unstructuredObj, ok := preparedObj.(*unstructured.Unstructured)
if !ok { 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 continue
} }
@ -511,7 +507,7 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s
} }
if err != nil { if err != nil {
ctx.infof("error restoring %s: %v", unstructuredObj.GetName(), err) 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 continue
} }
@ -522,11 +518,11 @@ func (ctx *context) restoreResourceForNamespace(namespace string, resourcePath s
if waiter != nil { if waiter != nil {
if err := waiter.Wait(); err != 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. // 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 return "", err
} }
target := path.Join(dir, header.Name) target := filepath.Join(dir, header.Name)
switch header.Typeflag { switch header.Typeflag {
case tar.TypeDir: case tar.TypeDir:
@ -616,7 +612,7 @@ func (ctx *context) readBackup(tarRdr *tar.Reader) (string, error) {
case tar.TypeReg: case tar.TypeReg:
// make sure we have the directory created // 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 { if err != nil {
ctx.infof("mkdirall error: %v", err) ctx.infof("mkdirall error: %v", err)
return "", err return "", err

View File

@ -116,62 +116,58 @@ func TestPrioritizeResources(t *testing.T) {
} }
} }
func TestRestoreMethod(t *testing.T) { func TestRestoreNamespaceFiltering(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fileSystem *fakeFileSystem fileSystem *fakeFileSystem
baseDir string baseDir string
restore *api.Restore restore *api.Restore
expectedReadDirs []string 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", 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", baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}}, 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", 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", baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"b", "c"}}}, 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 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"},
}, },
{ {
name: "namespacesToRestore properly filters with exclusion filter", 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", baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}, ExcludedNamespaces: []string{"a"}}}, 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", 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", baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"a", "b", "c"}, ExcludedNamespaces: []string{"b"}}}, 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"},
},
}, },
} }
@ -184,6 +180,7 @@ func TestRestoreMethod(t *testing.T) {
namespaceClient: &fakeNamespaceClient{}, namespaceClient: &fakeNamespaceClient{},
fileSystem: test.fileSystem, fileSystem: test.fileSystem,
logger: log, logger: log,
prioritizedResources: test.prioritizedResources,
} }
warnings, errors := ctx.restoreFromDir(test.baseDir) 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 { tests := []struct {
name string name string
fileSystem *fakeFileSystem fileSystem *fakeFileSystem
restore *api.Restore restore *api.Restore
namespace string baseDir string
path string
prioritizedResources []schema.GroupResource prioritizedResources []schema.GroupResource
expectedErrors api.RestoreResult expectedErrors api.RestoreResult
expectedReadDirs []string expectedReadDirs []string
}{ }{
{ {
name: "cluster test", name: "cluster test",
fileSystem: newFakeFileSystem().WithDirectory("bak/cluster/a").WithDirectory("bak/cluster/c"), fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/cluster").WithDirectory("bak/resources/c/cluster"),
namespace: "", baseDir: "bak",
path: "bak/cluster", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
prioritizedResources: []schema.GroupResource{ prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "a"},
schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "b"},
schema.GroupResource{Resource: "c"}, 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", name: "resource priorities are applied",
fileSystem: newFakeFileSystem().WithDirectory("bak/cluster/a").WithDirectory("bak/cluster/c"), fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/cluster").WithDirectory("bak/resources/c/cluster"),
namespace: "", restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
path: "bak/cluster", baseDir: "bak",
prioritizedResources: []schema.GroupResource{ prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "c"}, schema.GroupResource{Resource: "c"},
schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "b"},
schema.GroupResource{Resource: "a"}, 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", name: "basic namespace",
fileSystem: newFakeFileSystem().WithDirectory("bak/namespaces/ns-1/a").WithDirectory("bak/namespaces/ns-1/c"), fileSystem: newFakeFileSystem().WithDirectory("bak/resources/a/namespaces/ns-1").WithDirectory("bak/resources/c/namespaces/ns-1"),
restore: &api.Restore{Spec: api.RestoreSpec{NamespaceMapping: make(map[string]string)}}, restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
namespace: "ns-1", baseDir: "bak",
path: "bak/namespaces/ns-1",
prioritizedResources: []schema.GroupResource{ prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "a"},
schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "b"},
schema.GroupResource{Resource: "c"}, 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", name: "error in a single resource doesn't terminate restore immediately, but is returned",
fileSystem: newFakeFileSystem(). fileSystem: newFakeFileSystem().
WithFile("bak/namespaces/ns-1/a/invalid-json.json", []byte("invalid json")). WithFile("bak/resources/a/namespaces/ns-1/invalid-json.json", []byte("invalid json")).
WithDirectory("bak/namespaces/ns-1/c"), WithDirectory("bak/resources/c/namespaces/ns-1"),
restore: &api.Restore{Spec: api.RestoreSpec{NamespaceMapping: make(map[string]string)}}, restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
namespace: "ns-1", baseDir: "bak",
path: "bak/namespaces/ns-1",
prioritizedResources: []schema.GroupResource{ prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "a"}, schema.GroupResource{Resource: "a"},
schema.GroupResource{Resource: "b"}, schema.GroupResource{Resource: "b"},
@ -262,10 +256,10 @@ func TestRestoreNamespace(t *testing.T) {
}, },
expectedErrors: api.RestoreResult{ expectedErrors: api.RestoreResult{
Namespaces: map[string][]string{ 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, 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.Ark)
assert.Empty(t, warnings.Cluster) assert.Empty(t, warnings.Cluster)
@ -431,7 +425,7 @@ func TestRestoreResourceForNamespace(t *testing.T) {
logger: log, 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.Ark)
assert.Empty(t, warnings.Cluster) assert.Empty(t, warnings.Cluster)