Initial commit

Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
Andy Goldstein
2017-08-02 13:27:17 -04:00
commit 2fe501f527
2024 changed files with 948288 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
import (
"io"
"io/ioutil"
"os"
)
// FileSystem defines methods for interacting with an
// underlying file system.
type FileSystem interface {
TempDir(dir, prefix string) (string, error)
MkdirAll(path string, perm os.FileMode) error
Create(name string) (io.WriteCloser, error)
RemoveAll(path string) error
ReadDir(dirname string) ([]os.FileInfo, error)
ReadFile(filename string) ([]byte, error)
DirExists(path string) (bool, error)
}
var _ FileSystem = &osFileSystem{}
type osFileSystem struct {
}
func (fs *osFileSystem) TempDir(dir, prefix string) (string, error) {
return ioutil.TempDir(dir, prefix)
}
func (fs *osFileSystem) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
func (fs *osFileSystem) Create(name string) (io.WriteCloser, error) {
return os.Create(name)
}
func (fs *osFileSystem) RemoveAll(path string) error {
return os.RemoveAll(path)
}
func (fs *osFileSystem) ReadDir(dirname string) ([]os.FileInfo, error) {
return ioutil.ReadDir(dirname)
}
func (fs *osFileSystem) ReadFile(filename string) ([]byte, error) {
return ioutil.ReadFile(filename)
}
func (fs *osFileSystem) DirExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
import (
"errors"
"fmt"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
)
// how long should we wait for certain objects (e.g. PVs, PVCs) to reach
// their specified conditions before continuing on.
const objectCreateWaitTimeout = 30 * time.Second
// resourceWaiter knows how to wait for a set of registered items to become "ready" (according
// to a provided readyFunc) based on listening to a channel of Events. The correct usage
// of this struct is to construct it, register all of the desired items to wait for via
// RegisterItem, and then to Wait() for them to become ready or the timeout to be exceeded.
type resourceWaiter struct {
watchChan <-chan watch.Event
items sets.String
readyFunc func(runtime.Unstructured) bool
}
func newResourceWaiter(watchChan <-chan watch.Event, readyFunc func(runtime.Unstructured) bool) *resourceWaiter {
return &resourceWaiter{
watchChan: watchChan,
items: sets.NewString(),
readyFunc: readyFunc,
}
}
// RegisterItem adds the specified key to a list of items to listen for events for.
func (rw *resourceWaiter) RegisterItem(key string) {
rw.items.Insert(key)
}
// Wait listens for events on the watchChan related to items that have been registered,
// and returns when either all of them have become ready according to readyFunc, or when
// the timeout has been exceeded.
func (rw *resourceWaiter) Wait() error {
for {
if rw.items.Len() <= 0 {
return nil
}
timeout := time.NewTimer(objectCreateWaitTimeout)
select {
case event := <-rw.watchChan:
obj, ok := event.Object.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Unexpected type %T", event.Object)
}
if event.Type == watch.Added || event.Type == watch.Modified {
if rw.items.Has(obj.GetName()) && rw.readyFunc(obj) {
rw.items.Delete(obj.GetName())
}
}
case <-timeout.C:
return errors.New("failed to observe all items becoming ready within the timeout")
}
}
}
+604
View File
@@ -0,0 +1,604 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"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/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/client"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/discovery"
arkv1client "github.com/heptio/ark/pkg/generated/clientset/typed/ark/v1"
"github.com/heptio/ark/pkg/restore/restorers"
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/kube"
)
// Restorer knows how to restore a backup.
type Restorer interface {
// Restore restores the backup data from backupReader, returning warnings and errors.
Restore(restore *api.Restore, backup *api.Backup, backupReader io.Reader) (api.RestoreResult, api.RestoreResult)
}
var _ Restorer = &kubernetesRestorer{}
type gvString string
type kindString string
// kubernetesRestorer implements Restorer for restoring into a Kubernetes cluster.
type kubernetesRestorer struct {
discoveryHelper discovery.Helper
dynamicFactory client.DynamicFactory
restorers map[schema.GroupResource]restorers.ResourceRestorer
backupService cloudprovider.BackupService
backupClient arkv1client.BackupsGetter
namespaceClient corev1.NamespaceInterface
resourcePriorities []string
fileSystem FileSystem
}
// prioritizeResources takes a list of pre-prioritized resources and a full list of resources to restore,
// and returns an ordered list of GroupResource-resolved resources in the order that they should be
// restored.
func prioritizeResources(mapper meta.RESTMapper, priorities []string, resources []*metav1.APIResourceList) ([]schema.GroupResource, error) {
var ret []schema.GroupResource
// set keeps track of resolved GroupResource names
set := sets.NewString()
// start by resolving priorities into GroupResources and adding them to ret
for _, r := range priorities {
gr := schema.ParseGroupResource(r)
gvr, err := mapper.ResourceFor(gr.WithVersion(""))
if err != nil {
return nil, err
}
gr = gvr.GroupResource()
ret = append(ret, gr)
set.Insert(gr.String())
}
// go through everything we got from discovery and add anything not in "set" to byName
var byName []schema.GroupResource
for _, resourceGroup := range resources {
// will be something like storage.k8s.io/v1
groupVersion, err := schema.ParseGroupVersion(resourceGroup.GroupVersion)
if err != nil {
return nil, err
}
for _, resource := range resourceGroup.APIResources {
gr := groupVersion.WithResource(resource.Name).GroupResource()
if !set.Has(gr.String()) {
byName = append(byName, gr)
}
}
}
// sort byName by name
sort.Slice(byName, func(i, j int) bool {
return byName[i].String() < byName[j].String()
})
// combine prioritized with by-name
ret = append(ret, byName...)
return ret, nil
}
// NewKubernetesRestorer creates a new kubernetesRestorer.
func NewKubernetesRestorer(
discoveryHelper discovery.Helper,
dynamicFactory client.DynamicFactory,
customRestorers map[string]restorers.ResourceRestorer,
backupService cloudprovider.BackupService,
resourcePriorities []string,
backupClient arkv1client.BackupsGetter,
namespaceClient corev1.NamespaceInterface,
) (Restorer, error) {
mapper := discoveryHelper.Mapper()
r := make(map[schema.GroupResource]restorers.ResourceRestorer)
for gr, restorer := range customRestorers {
gvr, err := mapper.ResourceFor(schema.ParseGroupResource(gr).WithVersion(""))
if err != nil {
return nil, err
}
r[gvr.GroupResource()] = restorer
}
return &kubernetesRestorer{
discoveryHelper: discoveryHelper,
dynamicFactory: dynamicFactory,
restorers: r,
backupService: backupService,
backupClient: backupClient,
namespaceClient: namespaceClient,
resourcePriorities: resourcePriorities,
fileSystem: &osFileSystem{},
}, nil
}
// Restore executes a restore into the target Kubernetes cluster according to the restore spec
// and using data from the provided backup/backup reader. Returns a warnings and errors RestoreResult,
// respectively, summarizing info about the restore.
func (kr *kubernetesRestorer) Restore(restore *api.Restore, backup *api.Backup, backupReader io.Reader) (api.RestoreResult, api.RestoreResult) {
// metav1.LabelSelectorAsSelector converts a nil LabelSelector to a
// Nothing Selector, i.e. a selector that matches nothing. We want
// a selector that matches everything. This can be accomplished by
// passing a non-nil empty LabelSelector.
ls := restore.Spec.LabelSelector
if ls == nil {
ls = &metav1.LabelSelector{}
}
selector, err := metav1.LabelSelectorAsSelector(ls)
if err != nil {
return api.RestoreResult{}, api.RestoreResult{Ark: []string{err.Error()}}
}
prioritizedResources, err := prioritizeResources(kr.discoveryHelper.Mapper(), kr.resourcePriorities, kr.discoveryHelper.Resources())
if err != nil {
return api.RestoreResult{}, api.RestoreResult{Ark: []string{err.Error()}}
}
dir, err := kr.unzipAndExtractBackup(backupReader)
if err != nil {
glog.Errorf("error unzipping and extracting: %v", err)
return api.RestoreResult{}, api.RestoreResult{Ark: []string{err.Error()}}
}
defer kr.fileSystem.RemoveAll(dir)
return kr.restoreFromDir(dir, restore, backup, prioritizedResources, selector)
}
// restoreFromDir executes a restore based on backup data contained within a local
// directory.
func (kr *kubernetesRestorer) restoreFromDir(
dir string,
restore *api.Restore,
backup *api.Backup,
prioritizedResources []schema.GroupResource,
selector labels.Selector,
) (api.RestoreResult, api.RestoreResult) {
warnings, errors := api.RestoreResult{}, api.RestoreResult{}
// cluster-scoped
clusterPath := path.Join(dir, api.ClusterScopedDir)
exists, err := kr.fileSystem.DirExists(clusterPath)
if err != nil {
errors.Cluster = []string{err.Error()}
}
if exists {
w, e := kr.restoreNamespace(restore, "", clusterPath, prioritizedResources, selector, backup)
merge(&warnings, &w)
merge(&errors, &e)
}
// namespace-scoped
namespacesPath := path.Join(dir, api.NamespaceScopedDir)
nses, err := kr.fileSystem.ReadDir(namespacesPath)
if err != nil {
addArkError(&errors, err)
return warnings, errors
}
namespacesToRestore := sets.NewString(restore.Spec.Namespaces...)
for _, ns := range nses {
if !ns.IsDir() {
continue
}
nsPath := path.Join(namespacesPath, ns.Name())
if !namespacesToRestore.Has("*") && !namespacesToRestore.Has(ns.Name()) {
glog.Infof("Skipping namespace %s", ns.Name())
continue
}
w, e := kr.restoreNamespace(restore, ns.Name(), nsPath, prioritizedResources, selector, backup)
merge(&warnings, &w)
merge(&errors, &e)
}
return warnings, errors
}
// merge combines two RestoreResult objects into one
// by appending the corresponding lists to one another.
func merge(a, b *api.RestoreResult) {
a.Cluster = append(a.Cluster, b.Cluster...)
a.Ark = append(a.Ark, b.Ark...)
for k, v := range b.Namespaces {
if a.Namespaces == nil {
a.Namespaces = make(map[string][]string)
}
a.Namespaces[k] = append(a.Namespaces[k], v...)
}
}
// addArkError appends an error to the provided RestoreResult's Ark list.
func addArkError(r *api.RestoreResult, err error) {
r.Ark = append(r.Ark, err.Error())
}
// addToResult appends an error to the provided RestoreResult, either within
// the cluster-scoped list (if ns == "") or within the provided namespace's
// entry.
func addToResult(r *api.RestoreResult, ns string, e error) {
if ns == "" {
r.Cluster = append(r.Cluster, e.Error())
} else {
if r.Namespaces == nil {
r.Namespaces = make(map[string][]string)
}
r.Namespaces[ns] = append(r.Namespaces[ns], 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 (kr *kubernetesRestorer) restoreNamespace(
restore *api.Restore,
nsName string,
nsPath string,
prioritizedResources []schema.GroupResource,
labelSelector labels.Selector,
backup *api.Backup,
) (api.RestoreResult, api.RestoreResult) {
warnings, errors := api.RestoreResult{}, api.RestoreResult{}
if nsName == "" {
glog.Info("Restoring cluster-scoped resources")
} else {
glog.Infof("Restoring namespace %s", nsName)
}
resourceDirs, err := kr.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 := restore.Spec.NamespaceMapping[nsName]; ok {
nsName = target
}
// ensure namespace exists
ns := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
},
}
if _, err := kube.EnsureNamespaceExists(ns, kr.namespaceClient); err != nil {
addArkError(&errors, err)
return warnings, errors
}
}
for _, resource := range prioritizedResources {
rscDir := resourceDirsMap[resource.String()]
if rscDir == nil {
continue
}
resourcePath := path.Join(nsPath, rscDir.Name())
w, e := kr.restoreResourceForNamespace(nsName, resourcePath, labelSelector, restore, backup)
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 (kr *kubernetesRestorer) restoreResourceForNamespace(
namespace string,
resourcePath string,
labelSelector labels.Selector,
restore *api.Restore,
backup *api.Backup,
) (api.RestoreResult, api.RestoreResult) {
warnings, errors := api.RestoreResult{}, api.RestoreResult{}
resource := path.Base(resourcePath)
glog.Infof("Restoring resource %v into namespace %v\n", resource, namespace)
files, err := kr.fileSystem.ReadDir(resourcePath)
if err != nil {
addToResult(&errors, namespace, fmt.Errorf("error reading %q resource directory: %v", resource, err))
return warnings, errors
}
if len(files) == 0 {
return warnings, errors
}
var (
resourceClient client.Dynamic
restorer restorers.ResourceRestorer
waiter *resourceWaiter
groupResource = schema.ParseGroupResource(path.Base(resourcePath))
)
for _, file := range files {
fullPath := filepath.Join(resourcePath, file.Name())
obj, err := kr.unmarshal(fullPath)
if err != nil {
addToResult(&errors, namespace, fmt.Errorf("error decoding %q: %v", fullPath, err))
continue
}
if !labelSelector.Matches(labels.Set(obj.GetLabels())) {
continue
}
if restorer == nil {
// initialize client & restorer for this Resource. we need
// metadata from an object to do this.
glog.Infof("Getting client for %s", obj.GroupVersionKind().String())
resource := metav1.APIResource{
Namespaced: len(namespace) > 0,
Name: groupResource.Resource,
}
var err error
resourceClient, err = kr.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.String(), err))
return warnings, errors
}
restorer = kr.restorers[groupResource]
if restorer == nil {
glog.Infof("Using default restorer for %s", groupResource.String())
restorer = restorers.NewBasicRestorer(true)
} else {
glog.Infof("Using custom restorer for %s", groupResource.String())
}
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.String(), err))
return warnings, errors
}
watchChan := itmWatch.ResultChan()
defer itmWatch.Stop()
waiter = newResourceWaiter(watchChan, restorer.Ready)
}
}
if !restorer.Handles(obj, restore) {
continue
}
hasControllerOwner, err := hasControllerOwner(obj.UnstructuredContent())
if err != nil {
addToResult(&errors, namespace, fmt.Errorf("error check for controller owner for %q: %v", fullPath, err))
continue
}
if hasControllerOwner {
continue
}
preparedObj, err := restorer.Prepare(obj, restore, backup)
if err != nil {
addToResult(&errors, 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))
continue
}
// necessary because we may have remapped the namespace
unstructuredObj.SetNamespace(namespace)
// add an ark-restore label to each resource for easy ID
addLabel(unstructuredObj, api.RestoreLabelKey, restore.Name)
glog.Infof("Restoring item %v", unstructuredObj.GetName())
_, err = resourceClient.Create(unstructuredObj)
if apierrors.IsAlreadyExists(err) {
addToResult(&warnings, namespace, err)
continue
}
if err != nil {
glog.Errorf("error restoring %s: %v", unstructuredObj.GetName(), err)
addToResult(&errors, namespace, fmt.Errorf("error restoring %s: %v", fullPath, err))
continue
}
if waiter != nil {
waiter.RegisterItem(unstructuredObj.GetName())
}
}
if waiter != nil {
if err := waiter.Wait(); err != nil {
addArkError(&errors, fmt.Errorf("error waiting for all %s resources to be created in namespace %s: %v", groupResource.String(), namespace, err))
}
}
return warnings, errors
}
// addLabel applies the specified key/value to an object as a label.
func addLabel(obj *unstructured.Unstructured, key string, val string) {
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels[key] = val
obj.SetLabels(labels)
}
// hasControllerOwner returns whether or not an object has a controller
// owner ref. Used to identify whether or not an object should be explicitly
// recreated during a restore.
func hasControllerOwner(objData map[string]interface{}) (bool, error) {
meta, err := collections.GetMap(objData, "metadata")
if err != nil {
return false, err
}
ownerRefsObj, found := meta["ownerReferences"]
if !found {
return false, nil
}
ownerRefs, ok := ownerRefsObj.([]interface{})
if !ok {
return false, fmt.Errorf("Unexpected type %T", ownerRefsObj)
}
for _, refObj := range ownerRefs {
ownerRef, ok := refObj.(map[string]interface{})
if !ok {
return false, fmt.Errorf("Unexpected type %T", refObj)
}
if _, exists := ownerRef["controller"]; exists {
return true, nil
}
}
return false, nil
}
// unmarshal reads the specified file, unmarshals the JSON contained within it
// and returns an Unstructured object.
func (kr *kubernetesRestorer) unmarshal(filePath string) (*unstructured.Unstructured, error) {
var obj unstructured.Unstructured
bytes, err := kr.fileSystem.ReadFile(filePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &obj)
if err != nil {
return nil, err
}
return &obj, nil
}
// unzipAndExtractBackup extracts a reader on a gzipped tarball to a local temp directory
func (kr *kubernetesRestorer) unzipAndExtractBackup(src io.Reader) (string, error) {
gzr, err := gzip.NewReader(src)
if err != nil {
glog.Errorf("error creating gzip reader: %v", err)
return "", err
}
defer gzr.Close()
return kr.readBackup(tar.NewReader(gzr))
}
// readBackup extracts a tar reader to a local directory/file tree within a
// temp directory.
func (kr *kubernetesRestorer) readBackup(tarRdr *tar.Reader) (string, error) {
dir, err := kr.fileSystem.TempDir("", "")
if err != nil {
glog.Errorf("error creating temp dir: %v", err)
return "", err
}
for {
header, err := tarRdr.Next()
if err == io.EOF {
glog.Infof("end of tar")
break
}
if err != nil {
glog.Errorf("error reading tar: %v", err)
return "", err
}
target := path.Join(dir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
err := kr.fileSystem.MkdirAll(target, header.FileInfo().Mode())
if err != nil {
glog.Errorf("mkdirall error: %v", err)
return "", err
}
case tar.TypeReg:
// make sure we have the directory created
err := kr.fileSystem.MkdirAll(path.Dir(target), header.FileInfo().Mode())
if err != nil {
glog.Errorf("mkdirall error: %v", err)
return "", err
}
// create the file
file, err := kr.fileSystem.Create(target)
if err != nil {
return "", err
}
defer file.Close()
if _, err := io.Copy(file, tarRdr); err != nil {
glog.Errorf("error copying: %v", err)
return "", err
}
}
}
return dir, nil
}
+562
View File
@@ -0,0 +1,562 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restore
import (
"encoding/json"
"io"
"os"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api/v1"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/restore/restorers"
"github.com/heptio/ark/pkg/util/collections"
. "github.com/heptio/ark/pkg/util/test"
)
func TestPrioritizeResources(t *testing.T) {
mapper := &FakeMapper{AutoReturnResource: true}
priorities := []string{"namespaces", "configmaps", "pods"}
resources := []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{Name: "aaa"},
{Name: "bbb"},
{Name: "configmaps"},
{Name: "ddd"},
{Name: "namespaces"},
{Name: "ooo"},
{Name: "pods"},
{Name: "sss"},
},
},
}
result, err := prioritizeResources(mapper, priorities, resources)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := []string{"namespaces", "configmaps", "pods", "aaa", "bbb", "ddd", "ooo", "sss"}
for i := range result {
if len(expected) < i+1 {
t.Errorf("result is too small: %v", result)
break
}
if e, a := expected[i], result[i].Resource; e != a {
t.Errorf("index %d, expected %s, got %s", i, e, a)
}
}
}
func TestRestoreMethod(t *testing.T) {
tests := []struct {
name string
fileSystem *fakeFileSystem
baseDir string
restore *api.Restore
expectedReadDirs []string
}{
{
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: "namespacesToRestore having * restores all namespaces",
fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"),
baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{Namespaces: []string{"*"}}},
expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"},
},
{
name: "namespacesToRestore properly filters",
fileSystem: newFakeFileSystem().WithDirectories("bak/cluster", "bak/namespaces/a", "bak/namespaces/b", "bak/namespaces/c"),
baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{Namespaces: []string{"b", "c"}}},
expectedReadDirs: []string{"bak/cluster", "bak/namespaces", "bak/namespaces/b", "bak/namespaces/c"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := &kubernetesRestorer{
discoveryHelper: nil,
dynamicFactory: nil,
restorers: nil,
backupService: nil,
backupClient: nil,
namespaceClient: &fakeNamespaceClient{},
resourcePriorities: nil,
fileSystem: test.fileSystem,
}
warnings, errors := restorer.restoreFromDir(test.baseDir, test.restore, nil, nil, nil)
assert.Empty(t, warnings.Ark)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Empty(t, errors.Ark)
assert.Empty(t, errors.Cluster)
assert.Empty(t, errors.Namespaces)
assert.Equal(t, test.expectedReadDirs, test.fileSystem.readDirCalls)
})
}
}
func TestRestoreNamespace(t *testing.T) {
tests := []struct {
name string
fileSystem *fakeFileSystem
restore *api.Restore
namespace string
path 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",
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"},
},
{
name: "resource priorities are applied",
fileSystem: newFakeFileSystem().WithDirectory("bak/cluster/a").WithDirectory("bak/cluster/c"),
namespace: "",
path: "bak/cluster",
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"},
},
{
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",
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"},
},
{
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",
prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "a"},
schema.GroupResource{Resource: "b"},
schema.GroupResource{Resource: "c"},
},
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"},
},
},
expectedReadDirs: []string{"bak/namespaces/ns-1", "bak/namespaces/ns-1/a", "bak/namespaces/ns-1/c"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := &kubernetesRestorer{
discoveryHelper: nil,
dynamicFactory: nil,
restorers: nil,
backupService: nil,
backupClient: nil,
namespaceClient: &fakeNamespaceClient{},
resourcePriorities: nil,
fileSystem: test.fileSystem,
}
warnings, errors := restorer.restoreNamespace(test.restore, test.namespace, test.path, test.prioritizedResources, nil, nil)
assert.Empty(t, warnings.Ark)
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
fileSystem *fakeFileSystem
restorers map[schema.GroupResource]restorers.ResourceRestorer
expectedErrors api.RestoreResult
expectedObjs []unstructured.Unstructured
}{
{
name: "basic normal case",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()).
WithFile("configmaps/cm-2.json", newNamedTestConfigMap("cm-2").ToJSON()),
expectedObjs: toUnstructured(
newNamedTestConfigMap("cm-1").WithArkLabel("my-restore").ConfigMap,
newNamedTestConfigMap("cm-2").WithArkLabel("my-restore").ConfigMap,
),
},
{
name: "no such directory causes error",
namespace: "ns-1",
resourcePath: "configmaps",
fileSystem: newFakeFileSystem(),
expectedErrors: api.RestoreResult{
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: newFakeFileSystem().WithDirectory("configmaps"),
},
{
name: "unmarshall failure does not cause immediate return",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().
WithFile("configmaps/cm-1-invalid.json", []byte("this is not valid json")).
WithFile("configmaps/cm-2.json", newNamedTestConfigMap("cm-2").ToJSON()),
expectedErrors: api.RestoreResult{
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").WithArkLabel("my-restore").ConfigMap),
},
{
name: "matching label selector correctly includes",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.SelectorFromSet(labels.Set(map[string]string{"foo": "bar"})),
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().WithLabels(map[string]string{"foo": "bar"}).ToJSON()),
expectedObjs: toUnstructured(newTestConfigMap().WithLabels(map[string]string{"foo": "bar"}).WithArkLabel("my-restore").ConfigMap),
},
{
name: "non-matching label selector correctly excludes",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.SelectorFromSet(labels.Set(map[string]string{"foo": "not-bar"})),
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().WithLabels(map[string]string{"foo": "bar"}).ToJSON()),
},
{
name: "items with controller owner are skipped",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().
WithFile("configmaps/cm-1.json", newTestConfigMap().WithControllerOwner().ToJSON()).
WithFile("configmaps/cm-2.json", newNamedTestConfigMap("cm-2").ToJSON()),
expectedObjs: toUnstructured(newNamedTestConfigMap("cm-2").WithArkLabel("my-restore").ConfigMap),
},
{
name: "namespace is remapped",
namespace: "ns-2",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().WithNamespace("ns-1").ToJSON()),
expectedObjs: toUnstructured(newTestConfigMap().WithNamespace("ns-2").WithArkLabel("my-restore").ConfigMap),
},
{
name: "custom restorer is correctly used",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
restorers: map[schema.GroupResource]restorers.ResourceRestorer{schema.GroupResource{Resource: "configmaps"}: newFakeCustomRestorer()},
expectedObjs: toUnstructured(newTestConfigMap().WithLabels(map[string]string{"fake-restorer": "foo"}).WithArkLabel("my-restore").ConfigMap),
},
{
name: "custom restorer for different group/resource is not used",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
restorers: map[schema.GroupResource]restorers.ResourceRestorer{schema.GroupResource{Resource: "foo-resource"}: newFakeCustomRestorer()},
expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resourceClient := &FakeDynamicClient{}
for i := range test.expectedObjs {
resourceClient.On("Create", &test.expectedObjs[i]).Return(&test.expectedObjs[i], nil)
}
dynamicFactory := &FakeDynamicFactory{}
resource := metav1.APIResource{Name: "configmaps", Namespaced: true}
gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}
dynamicFactory.On("ClientForGroupVersionKind", gvk, resource, test.namespace).Return(resourceClient, nil)
restorer := &kubernetesRestorer{
discoveryHelper: nil,
dynamicFactory: dynamicFactory,
restorers: test.restorers,
backupService: nil,
backupClient: nil,
namespaceClient: nil,
resourcePriorities: nil,
fileSystem: test.fileSystem,
}
var (
restore = &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
Name: "my-restore",
},
}
backup = &api.Backup{}
)
warnings, errors := restorer.restoreResourceForNamespace(test.namespace, test.resourcePath, test.labelSelector, restore, backup)
assert.Empty(t, warnings.Ark)
assert.Empty(t, warnings.Cluster)
assert.Empty(t, warnings.Namespaces)
assert.Equal(t, test.expectedErrors, errors)
})
}
}
func toUnstructured(objs ...runtime.Object) []unstructured.Unstructured {
res := make([]unstructured.Unstructured, 0, len(objs))
for _, obj := range objs {
jsonObj, err := json.Marshal(obj)
if err != nil {
panic(err)
}
var unstructuredObj unstructured.Unstructured
if err := json.Unmarshal(jsonObj, &unstructuredObj); err != nil {
panic(err)
}
metadata := unstructuredObj.Object["metadata"].(map[string]interface{})
delete(metadata, "creationTimestamp")
res = append(res, unstructuredObj)
}
return res
}
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) WithArkLabel(restoreName string) *testConfigMap {
if cm.Labels == nil {
cm.Labels = make(map[string]string)
}
cm.Labels[api.RestoreLabelKey] = restoreName
return cm
}
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 fakeFileSystem struct {
fs afero.Fs
readDirCalls []string
}
func newFakeFileSystem() *fakeFileSystem {
return &fakeFileSystem{
fs: afero.NewMemMapFs(),
}
}
func (fs *fakeFileSystem) WithFile(path string, data []byte) *fakeFileSystem {
file, _ := fs.fs.Create(path)
file.Write(data)
file.Close()
return fs
}
func (fs *fakeFileSystem) WithDirectory(path string) *fakeFileSystem {
fs.fs.MkdirAll(path, 0755)
return fs
}
func (fs *fakeFileSystem) WithDirectories(path ...string) *fakeFileSystem {
for _, dir := range path {
fs = fs.WithDirectory(dir)
}
return fs
}
func (fs *fakeFileSystem) TempDir(dir, prefix string) (string, error) {
return afero.TempDir(fs.fs, dir, prefix)
}
func (fs *fakeFileSystem) MkdirAll(path string, perm os.FileMode) error {
return fs.fs.MkdirAll(path, perm)
}
func (fs *fakeFileSystem) Create(name string) (io.WriteCloser, error) {
return fs.fs.Create(name)
}
func (fs *fakeFileSystem) RemoveAll(path string) error {
return fs.fs.RemoveAll(path)
}
func (fs *fakeFileSystem) ReadDir(dirname string) ([]os.FileInfo, error) {
fs.readDirCalls = append(fs.readDirCalls, dirname)
return afero.ReadDir(fs.fs, dirname)
}
func (fs *fakeFileSystem) ReadFile(filename string) ([]byte, error) {
return afero.ReadFile(fs.fs, filename)
}
func (fs *fakeFileSystem) DirExists(path string) (bool, error) {
return afero.DirExists(fs.fs, path)
}
type fakeCustomRestorer struct {
restorers.ResourceRestorer
}
func newFakeCustomRestorer() *fakeCustomRestorer {
return &fakeCustomRestorer{
ResourceRestorer: restorers.NewBasicRestorer(true),
}
}
func (r *fakeCustomRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
if _, found := metadata["labels"]; !found {
metadata["labels"] = make(map[string]interface{})
}
metadata["labels"].(map[string]interface{})["fake-restorer"] = "foo"
// want the baseline functionality too
return r.ResourceRestorer.Prepare(obj, restore, backup)
}
type fakeNamespaceClient struct {
corev1.NamespaceInterface
}
func (nsc *fakeNamespaceClient) Create(ns *v1.Namespace) (*v1.Namespace, error) {
return ns, nil
}
+71
View File
@@ -0,0 +1,71 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type jobRestorer struct{}
var _ ResourceRestorer = &jobRestorer{}
func NewJobRestorer() ResourceRestorer {
return &jobRestorer{}
}
func (r *jobRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (r *jobRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
glog.V(4).Infof("resetting metadata and status")
_, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
glog.V(4).Infof("getting spec.selector.matchLabels")
matchLabels, err := collections.GetMap(obj.UnstructuredContent(), "spec.selector.matchLabels")
if err != nil {
glog.V(4).Infof("unable to get spec.selector.matchLabels: %v", err)
} else {
delete(matchLabels, "controller-uid")
}
templateLabels, err := collections.GetMap(obj.UnstructuredContent(), "spec.template.metadata.labels")
if err != nil {
glog.V(4).Infof("unable to get spec.template.metadata.labels: %v", err)
} else {
delete(templateLabels, "controller-uid")
}
return obj, nil
}
func (r *jobRestorer) Wait() bool {
return false
}
func (r *jobRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
+138
View File
@@ -0,0 +1,138 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestJobRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no metadata should error",
obj: NewTestUnstructured().Unstructured,
expectedErr: true,
},
{
name: "missing spec.selector and/or spec.template should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpec().
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpec().
Unstructured,
},
{
name: "missing spec.selector.matchLabels should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{}).
Unstructured,
},
{
name: "spec.selector.matchLabels[controller-uid] is removed",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{
"matchLabels": map[string]interface{}{
"controller-uid": "foo",
"hello": "world",
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{
"matchLabels": map[string]interface{}{
"hello": "world",
},
}).
Unstructured,
},
{
name: "missing spec.template.metadata should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{}).
Unstructured,
},
{
name: "missing spec.template.metadata.labels should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{},
}).
Unstructured,
},
{
name: "spec.template.metadata.labels[controller-uid] is removed",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"controller-uid": "foo",
"hello": "world",
},
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"hello": "world",
},
},
}).
Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewJobRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
@@ -0,0 +1,78 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type namespaceRestorer struct{}
var _ ResourceRestorer = &namespaceRestorer{}
func NewNamespaceRestorer() ResourceRestorer {
return &namespaceRestorer{}
}
func (nsr *namespaceRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
nsName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
if err != nil {
return false
}
for _, restorableNS := range restore.Spec.Namespaces {
if restorableNS == nsName {
return true
}
}
return false
}
func (nsr *namespaceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
updated, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
currentName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
if err != nil {
return nil, err
}
if newName, mapped := restore.Spec.NamespaceMapping[currentName]; mapped {
metadata["name"] = newName
}
return updated, nil
}
func (nsr *namespaceRestorer) Wait() bool {
return false
}
func (nsr *namespaceRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
@@ -0,0 +1,145 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/stretchr/testify/assert"
)
func TestHandles(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
expect bool
}{
{
name: "restorable NS",
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
expect: true,
},
{
name: "non-restorable NS",
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-2").Restore,
expect: false,
},
{
name: "namespace obj doesn't have name",
obj: NewTestUnstructured().WithMetadata().Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
expect: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewNamespaceRestorer()
assert.Equal(t, test.expect, restorer.Handles(test.obj, test.restore))
})
}
}
func TestPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "standard non-mapped namespace",
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
restore: newTestRestore().Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-1").Unstructured,
},
{
name: "standard mapped namespace",
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
restore: newTestRestore().WithMappedNamespace("ns-1", "ns-2").Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-2").Unstructured,
},
{
name: "object without name results in error",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "annotations are kept",
obj: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
restore: newTestRestore().Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewNamespaceRestorer()
res, err := restorer.Prepare(test.obj, test.restore, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
type testRestore struct {
*api.Restore
}
func newTestRestore() *testRestore {
return &testRestore{
Restore: &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
},
Spec: api.RestoreSpec{},
},
}
}
func (r *testRestore) WithRestorableNamespace(namespace string) *testRestore {
r.Spec.Namespaces = append(r.Spec.Namespaces, namespace)
return r
}
func (r *testRestore) WithMappedNamespace(from string, to string) *testRestore {
if r.Spec.NamespaceMapping == nil {
r.Spec.NamespaceMapping = make(map[string]string)
}
r.Spec.NamespaceMapping[from] = to
return r
}
func (r *testRestore) WithRestorePVs(restorePVs bool) *testRestore {
r.Spec.RestorePVs = restorePVs
return r
}
+129
View File
@@ -0,0 +1,129 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"regexp"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type podRestorer struct{}
var _ ResourceRestorer = &podRestorer{}
func NewPodRestorer() ResourceRestorer {
return &podRestorer{}
}
func (nsr *podRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
var (
defaultTokenRegex = regexp.MustCompile("default-token-.*")
)
func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
glog.V(4).Infof("resetting metadata and status")
_, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
glog.V(4).Infof("getting spec")
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
glog.V(4).Infof("deleting spec.NodeName")
delete(spec, "nodeName")
newVolumes := make([]interface{}, 0)
glog.V(4).Infof("iterating over volumes")
err = collections.ForEach(spec, "volumes", func(volume map[string]interface{}) error {
name, err := collections.GetString(volume, "name")
if err != nil {
return err
}
glog.V(4).Infof("checking volume with name %q", name)
if !defaultTokenRegex.MatchString(name) {
glog.V(4).Infof("preserving volume")
newVolumes = append(newVolumes, volume)
} else {
glog.V(4).Infof("excluding volume")
}
return nil
})
if err != nil {
return nil, err
}
glog.V(4).Infof("setting spec.volumes")
spec["volumes"] = newVolumes
glog.V(4).Infof("iterating over containers")
err = collections.ForEach(spec, "containers", func(container map[string]interface{}) error {
var newVolumeMounts []interface{}
err := collections.ForEach(container, "volumeMounts", func(volumeMount map[string]interface{}) error {
name, err := collections.GetString(volumeMount, "name")
if err != nil {
return err
}
glog.V(4).Infof("checking volumeMount with name %q", name)
if !defaultTokenRegex.MatchString(name) {
glog.V(4).Infof("preserving volumeMount")
newVolumeMounts = append(newVolumeMounts, volumeMount)
} else {
glog.V(4).Infof("excluding volumeMount")
}
return nil
})
if err != nil {
return err
}
container["volumeMounts"] = newVolumeMounts
return nil
})
if err != nil {
return nil, err
}
return obj, nil
}
func (nsr *podRestorer) Wait() bool {
return false
}
func (nsr *podRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
+108
View File
@@ -0,0 +1,108 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestPodRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("pod-1").Unstructured,
expectedErr: true,
},
{
name: "nodeName (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("pod-1").WithSpec("nodeName", "foo").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{}).
Unstructured,
},
{
name: "volumes matching default-token regex should be deleted",
obj: NewTestUnstructured().WithName("pod-1").
WithSpecField("volumes", []interface{}{
map[string]interface{}{"name": "foo"},
map[string]interface{}{"name": "default-token-foo"},
}).WithSpecField("containers", []interface{}{}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pod-1").
WithSpecField("volumes", []interface{}{
map[string]interface{}{"name": "foo"},
}).WithSpecField("containers", []interface{}{}).Unstructured,
},
{
name: "container volumeMounts matching default-token regex should be deleted",
obj: NewTestUnstructured().WithName("svc-1").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{
map[string]interface{}{
"volumeMounts": []interface{}{
map[string]interface{}{
"name": "foo",
},
map[string]interface{}{
"name": "default-token-foo",
},
},
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{
map[string]interface{}{
"volumeMounts": []interface{}{
map[string]interface{}{
"name": "foo",
},
},
},
}).
Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPodRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
+117
View File
@@ -0,0 +1,117 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
)
type persistentVolumeRestorer struct {
snapshotService cloudprovider.SnapshotService
}
var _ ResourceRestorer = &persistentVolumeRestorer{}
func NewPersistentVolumeRestorer(snapshotService cloudprovider.SnapshotService) ResourceRestorer {
return &persistentVolumeRestorer{
snapshotService: snapshotService,
}
}
func (sr *persistentVolumeRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *persistentVolumeRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
if _, err := resetMetadataAndStatus(obj, false); err != nil {
return nil, err
}
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
delete(spec, "claimRef")
delete(spec, "storageClassName")
if restore.Spec.RestorePVs {
volumeID, err := sr.restoreVolume(obj.UnstructuredContent(), restore, backup)
if err != nil {
return nil, err
}
if err := setVolumeID(spec, volumeID); err != nil {
return nil, err
}
}
return obj, nil
}
func (sr *persistentVolumeRestorer) Wait() bool {
return true
}
func (sr *persistentVolumeRestorer) Ready(obj runtime.Unstructured) bool {
phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase")
return err == nil && phase == "Available"
}
func setVolumeID(spec map[string]interface{}, volumeID string) error {
if pvSource, found := spec["awsElasticBlockStore"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["volumeID"] = volumeID
return nil
} else if pvSource, found := spec["gcePersistentDisk"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["pdName"] = volumeID
return nil
} else if pvSource, found := spec["azureDisk"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["diskName"] = volumeID
return nil
}
return errors.New("persistent volume source is not compatible")
}
func (sr *persistentVolumeRestorer) restoreVolume(item map[string]interface{}, restore *api.Restore, backup *api.Backup) (string, error) {
pvName, err := collections.GetString(item, "metadata.name")
if err != nil {
return "", err
}
if backup.Status.VolumeBackups == nil {
return "", fmt.Errorf("VolumeBackups map not found for persistent volume %s", pvName)
}
backupInfo, found := backup.Status.VolumeBackups[pvName]
if !found {
return "", fmt.Errorf("BackupInfo not found for PersistentVolume %s", pvName)
}
return sr.snapshotService.CreateVolumeFromSnapshot(backupInfo.SnapshotID, backupInfo.Type, backupInfo.Iops)
}
+186
View File
@@ -0,0 +1,186 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
. "github.com/heptio/ark/pkg/util/test"
)
func TestPVRestorerPrepare(t *testing.T) {
iops := 1000
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
backup *api.Backup
volumeMap map[api.VolumeBackupInfo]string
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no name should error",
obj: NewTestUnstructured().WithMetadata().Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("pv-1").Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "when RestorePVs=false, should not error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: newTestRestore().WithRestorePVs(false).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
},
{
name: "when RestorePVs=true, error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: true,
expectedRes: nil,
},
{
name: "claimRef and storageClassName (only) should be cleared from spec",
obj: NewTestUnstructured().
WithName("pv-1").
WithSpecField("claimRef", "foo").
WithSpecField("storageClassName", "foo").
WithSpecField("foo", "bar").
Unstructured,
restore: newTestRestore().WithRestorePVs(false).Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().
WithName("pv-1").
WithSpecField("foo", "bar").
Unstructured,
},
{
name: "when RestorePVs=true, AWS volume ID should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, GCE pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", map[string]interface{}{"pdName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, Azure pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", map[string]interface{}{"diskName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, unsupported PV source should cause error",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("unsupportedPVSource", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: true,
},
{
name: "volume type and IOPS are correctly passed to CreateVolume",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1", Type: "gp", Iops: &iops}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1", Type: "gp", Iops: &iops}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
snapService := &FakeSnapshotService{RestorableVolumes: test.volumeMap}
restorer := NewPersistentVolumeRestorer(snapService)
res, err := restorer.Prepare(test.obj, test.restore, test.backup)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
func TestPVRestorerReady(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
expected bool
}{
{
name: "no status returns not ready",
obj: NewTestUnstructured().Unstructured,
expected: false,
},
{
name: "no status.phase returns not ready",
obj: NewTestUnstructured().WithStatus().Unstructured,
expected: false,
},
{
name: "empty status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "").Unstructured,
expected: false,
},
{
name: "non-Available status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "foo").Unstructured,
expected: false,
},
{
name: "Available status.phase returns ready",
obj: NewTestUnstructured().WithStatusField("phase", "Available").Unstructured,
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPersistentVolumeRestorer(nil)
assert.Equal(t, test.expected, restorer.Ready(test.obj))
})
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type persistentVolumeClaimRestorer struct{}
var _ ResourceRestorer = &persistentVolumeClaimRestorer{}
func NewPersistentVolumeClaimRestorer() ResourceRestorer {
return &persistentVolumeClaimRestorer{}
}
func (sr *persistentVolumeClaimRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *persistentVolumeClaimRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
return resetMetadataAndStatus(obj, true)
}
func (sr *persistentVolumeClaimRestorer) Wait() bool {
return true
}
func (sr *persistentVolumeClaimRestorer) Ready(obj runtime.Unstructured) bool {
phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase")
return err == nil && phase == "Bound"
}
@@ -0,0 +1,67 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestPVCRestorerReady(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
expected bool
}{
{
name: "no status returns not ready",
obj: NewTestUnstructured().Unstructured,
expected: false,
},
{
name: "no status.phase returns not ready",
obj: NewTestUnstructured().WithStatus().Unstructured,
expected: false,
},
{
name: "empty status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "").Unstructured,
expected: false,
},
{
name: "non-Available status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "foo").Unstructured,
expected: false,
},
{
name: "Bound status.phase returns ready",
obj: NewTestUnstructured().WithStatusField("phase", "Bound").Unstructured,
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPersistentVolumeClaimRestorer()
assert.Equal(t, test.expected, restorer.Ready(test.obj))
})
}
}
@@ -0,0 +1,83 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
// ResourceRestorer exposes the operations necessary to prepare Kubernetes resources
// for restore and confirm their readiness following restoration via Ark.
type ResourceRestorer interface {
// Handles returns true if the Restorer should restore this object.
Handles(obj runtime.Unstructured, restore *api.Restore) bool
// Prepare gets an item ready to be restored
Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error)
// Wait returns true if restoration should wait for all of this restorer's resources to be ready before moving on to the next restorer.
Wait() bool
// Ready returns true if the given item is considered ready by the system. Only used if Wait() returns true.
Ready(obj runtime.Unstructured) bool
}
func resetMetadataAndStatus(obj runtime.Unstructured, keepAnnotations bool) (runtime.Unstructured, error) {
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
for k := range metadata {
if k != "name" && k != "namespace" && k != "labels" && (!keepAnnotations || k != "annotations") {
delete(metadata, k)
}
}
delete(obj.UnstructuredContent(), "status")
return obj, nil
}
var _ ResourceRestorer = &basicRestorer{}
type basicRestorer struct {
saveAnnotations bool
}
func (br *basicRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (br *basicRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
return resetMetadataAndStatus(obj, br.saveAnnotations)
}
func (br *basicRestorer) Wait() bool {
return false
}
func (br *basicRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
func NewBasicRestorer(saveAnnotations bool) ResourceRestorer {
return &basicRestorer{saveAnnotations: saveAnnotations}
}
@@ -0,0 +1,160 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/assert"
)
func TestResetMetadataAndStatus(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
keepAnnotations bool
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no metadata causes error",
obj: NewTestUnstructured(),
keepAnnotations: false,
expectedErr: true,
},
{
name: "don't keep annotations",
obj: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata("name", "namespace", "labels").Unstructured,
},
{
name: "keep annotations",
obj: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
keepAnnotations: true,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
},
{
name: "don't keep extraneous metadata",
obj: NewTestUnstructured().WithMetadata("foo").Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata().Unstructured,
},
{
name: "don't keep status",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := resetMetadataAndStatus(test.obj, test.keepAnnotations)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
type testUnstructured struct {
*unstructured.Unstructured
}
func NewTestUnstructured() *testUnstructured {
obj := &testUnstructured{
Unstructured: &unstructured.Unstructured{
Object: make(map[string]interface{}),
},
}
return obj
}
func (obj *testUnstructured) WithMetadata(fields ...string) *testUnstructured {
return obj.withMap("metadata", fields...)
}
func (obj *testUnstructured) WithSpec(fields ...string) *testUnstructured {
return obj.withMap("spec", fields...)
}
func (obj *testUnstructured) WithStatus(fields ...string) *testUnstructured {
return obj.withMap("status", fields...)
}
func (obj *testUnstructured) WithMetadataField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("metadata", field, value)
}
func (obj *testUnstructured) WithSpecField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("spec", field, value)
}
func (obj *testUnstructured) WithStatusField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("status", field, value)
}
func (obj *testUnstructured) WithAnnotations(fields ...string) *testUnstructured {
annotations := make(map[string]interface{})
for _, field := range fields {
annotations[field] = "foo"
}
obj = obj.WithMetadataField("annotations", annotations)
return obj
}
func (obj *testUnstructured) WithName(name string) *testUnstructured {
return obj.WithMetadataField("name", name)
}
func (obj *testUnstructured) withMap(name string, fields ...string) *testUnstructured {
m := make(map[string]interface{})
obj.Object[name] = m
for _, field := range fields {
m[field] = "foo"
}
return obj
}
func (obj *testUnstructured) withMapEntry(mapName, field string, value interface{}) *testUnstructured {
var m map[string]interface{}
if res, ok := obj.Unstructured.Object[mapName]; !ok {
m = make(map[string]interface{})
obj.Unstructured.Object[mapName] = m
} else {
m = res.(map[string]interface{})
}
m[field] = value
return obj
}
+69
View File
@@ -0,0 +1,69 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type serviceRestorer struct{}
var _ ResourceRestorer = &serviceRestorer{}
func NewServiceRestorer() ResourceRestorer {
return &serviceRestorer{}
}
func (sr *serviceRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *serviceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
if _, err := resetMetadataAndStatus(obj, true); err != nil {
return nil, err
}
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
delete(spec, "clusterIP")
ports, err := collections.GetSlice(obj.UnstructuredContent(), "spec.ports")
if err != nil {
return nil, err
}
for _, port := range ports {
p := port.(map[string]interface{})
delete(p, "nodePort")
}
return obj, nil
}
func (sr *serviceRestorer) Wait() bool {
return false
}
func (sr *serviceRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
@@ -0,0 +1,72 @@
/*
Copyright 2017 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestServiceRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("svc-1").Unstructured,
expectedErr: true,
},
{
name: "clusterIP (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("svc-1").WithSpec("clusterIP", "foo").WithSpecField("ports", []interface{}{}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").WithSpec("foo").WithSpecField("ports", []interface{}{}).Unstructured,
},
{
name: "nodePort (only) should be deleted from all spec.ports",
obj: NewTestUnstructured().WithName("svc-1").
WithSpecField("ports", []interface{}{
map[string]interface{}{"nodePort": ""},
map[string]interface{}{"nodePort": "", "foo": "bar"},
}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").
WithSpecField("ports", []interface{}{
map[string]interface{}{},
map[string]interface{}{"foo": "bar"},
}).Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewServiceRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}