mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-25 17:34:17 +00:00
Initial commit
Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user