add --include-cluster-resources flag to restores (optional, default true)

Signed-off-by: Steve Kriss <steve@heptio.com>
pull/147/head
Steve Kriss 2017-10-20 12:51:54 -07:00
parent e460199536
commit a7cc58730e
6 changed files with 186 additions and 58 deletions

View File

@ -14,18 +14,19 @@ ark create restore BACKUP [flags]
### Options ### Options
``` ```
--exclude-namespaces stringArray namespaces to exclude from the restore --exclude-namespaces stringArray namespaces to exclude from the restore
--exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io --exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io
-h, --help help for restore -h, --help help for restore
--include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) --include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore
--include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *)
--label-columns stringArray a comma-separated list of labels to be displayed as columns --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources)
--labels mapStringString labels to apply to the restore --label-columns stringArray a comma-separated list of labels to be displayed as columns
--namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... --labels mapStringString labels to apply to the restore
-o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'. --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,...
--restore-volumes optionalBool[=true] whether to restore volumes from snapshots -o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'.
-l, --selector labelSelector only restore resources matching this label selector (default <none>) --restore-volumes optionalBool[=true] whether to restore volumes from snapshots
--show-labels show labels in the last column -l, --selector labelSelector only restore resources matching this label selector (default <none>)
--show-labels show labels in the last column
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

View File

@ -14,18 +14,19 @@ ark restore create BACKUP [flags]
### Options ### Options
``` ```
--exclude-namespaces stringArray namespaces to exclude from the restore --exclude-namespaces stringArray namespaces to exclude from the restore
--exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io --exclude-resources stringArray resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io
-h, --help help for create -h, --help help for create
--include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) --include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore
--include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *)
--label-columns stringArray a comma-separated list of labels to be displayed as columns --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources)
--labels mapStringString labels to apply to the restore --label-columns stringArray a comma-separated list of labels to be displayed as columns
--namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... --labels mapStringString labels to apply to the restore
-o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'. --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,...
--restore-volumes optionalBool[=true] whether to restore volumes from snapshots -o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'.
-l, --selector labelSelector only restore resources matching this label selector (default <none>) --restore-volumes optionalBool[=true] whether to restore volumes from snapshots
--show-labels show labels in the last column -l, --selector labelSelector only restore resources matching this label selector (default <none>)
--show-labels show labels in the last column
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

View File

@ -54,6 +54,11 @@ type RestoreSpec struct {
// RestorePVs specifies whether to restore all included // RestorePVs specifies whether to restore all included
// PVs from snapshot (via the cloudprovider). // PVs from snapshot (via the cloudprovider).
RestorePVs *bool `json:"restorePVs"` RestorePVs *bool `json:"restorePVs"`
// IncludeClusterResources specifies whether cluster-scoped resources
// should be included for consideration in the restore. If null, defaults
// to true.
IncludeClusterResources *bool `json:"includeClusterResources"`
} }
// RestorePhase is a string representation of the lifecycle phase // RestorePhase is a string representation of the lifecycle phase

View File

@ -54,23 +54,25 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
} }
type CreateOptions struct { type CreateOptions struct {
BackupName string BackupName string
RestoreVolumes flag.OptionalBool RestoreVolumes flag.OptionalBool
Labels flag.Map Labels flag.Map
IncludeNamespaces flag.StringArray IncludeNamespaces flag.StringArray
ExcludeNamespaces flag.StringArray ExcludeNamespaces flag.StringArray
IncludeResources flag.StringArray IncludeResources flag.StringArray
ExcludeResources flag.StringArray ExcludeResources flag.StringArray
NamespaceMappings flag.Map NamespaceMappings flag.Map
Selector flag.LabelSelector Selector flag.LabelSelector
IncludeClusterResources flag.OptionalBool
} }
func NewCreateOptions() *CreateOptions { func NewCreateOptions() *CreateOptions {
return &CreateOptions{ return &CreateOptions{
Labels: flag.NewMap(), Labels: flag.NewMap(),
IncludeNamespaces: flag.NewStringArray("*"), IncludeNamespaces: flag.NewStringArray("*"),
NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"), NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"),
RestoreVolumes: flag.NewOptionalBool(nil), RestoreVolumes: flag.NewOptionalBool(nil),
IncludeClusterResources: flag.NewOptionalBool(nil),
} }
} }
@ -86,6 +88,9 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
// this allows the user to just specify "--restore-volumes" as shorthand for "--restore-volumes=true" // this allows the user to just specify "--restore-volumes" as shorthand for "--restore-volumes=true"
// like a normal bool flag // like a normal bool flag
f.NoOptDefVal = "true" f.NoOptDefVal = "true"
f = flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "include cluster-scoped resources in the restore")
f.NoOptDefVal = "true"
} }
func (o *CreateOptions) Validate(c *cobra.Command, args []string) error { func (o *CreateOptions) Validate(c *cobra.Command, args []string) error {
@ -118,14 +123,15 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
Labels: o.Labels.Data(), Labels: o.Labels.Data(),
}, },
Spec: api.RestoreSpec{ Spec: api.RestoreSpec{
BackupName: o.BackupName, BackupName: o.BackupName,
IncludedNamespaces: o.IncludeNamespaces, IncludedNamespaces: o.IncludeNamespaces,
ExcludedNamespaces: o.ExcludeNamespaces, ExcludedNamespaces: o.ExcludeNamespaces,
IncludedResources: o.IncludeResources, IncludedResources: o.IncludeResources,
ExcludedResources: o.ExcludeResources, ExcludedResources: o.ExcludeResources,
NamespaceMapping: o.NamespaceMappings.Data(), NamespaceMapping: o.NamespaceMappings.Data(),
LabelSelector: o.Selector.LabelSelector, LabelSelector: o.Selector.LabelSelector,
RestorePVs: o.RestoreVolumes.Value, RestorePVs: o.RestoreVolumes.Value,
IncludeClusterResources: o.IncludeClusterResources.Value,
}, },
} }

View File

@ -397,6 +397,11 @@ func addToResult(r *api.RestoreResult, ns string, e error) {
func (ctx *context) restoreResource(resource, namespace, resourcePath string) (api.RestoreResult, api.RestoreResult) { func (ctx *context) restoreResource(resource, namespace, resourcePath string) (api.RestoreResult, api.RestoreResult) {
warnings, errs := api.RestoreResult{}, api.RestoreResult{} warnings, errs := api.RestoreResult{}, api.RestoreResult{}
if ctx.restore.Spec.IncludeClusterResources != nil && !*ctx.restore.Spec.IncludeClusterResources && namespace == "" {
ctx.infof("Skipping resource %s because it's cluster-scoped", resource)
return warnings, errs
}
if namespace != "" { if namespace != "" {
ctx.infof("Restoring resource '%s' into namespace '%s' from: %s", resource, namespace, resourcePath) ctx.infof("Restoring resource '%s' into namespace '%s' from: %s", resource, namespace, resourcePath)
} else { } else {

View File

@ -159,10 +159,15 @@ func TestRestoreNamespaceFiltering(t *testing.T) {
}, },
}, },
{ {
name: "namespacesToRestore properly filters with inclusion & exclusion filters", name: "namespacesToRestore properly filters with inclusion & exclusion filters",
fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"), fileSystem: newFakeFileSystem().WithDirectories("bak/resources/nodes/cluster", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/b", "bak/resources/secrets/namespaces/c"),
baseDir: "bak", baseDir: "bak",
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"a", "b", "c"}, ExcludedNamespaces: []string{"b"}}}, restore: &api.Restore{
Spec: api.RestoreSpec{
IncludedNamespaces: []string{"a", "b", "c"},
ExcludedNamespaces: []string{"b"},
},
},
expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/c"}, expectedReadDirs: []string{"bak/resources", "bak/resources/nodes/cluster", "bak/resources/secrets/namespaces", "bak/resources/secrets/namespaces/a", "bak/resources/secrets/namespaces/c"},
prioritizedResources: []schema.GroupResource{ prioritizedResources: []schema.GroupResource{
schema.GroupResource{Resource: "nodes"}, schema.GroupResource{Resource: "nodes"},
@ -288,15 +293,23 @@ func TestRestorePriority(t *testing.T) {
} }
func TestRestoreResourceForNamespace(t *testing.T) { func TestRestoreResourceForNamespace(t *testing.T) {
var (
trueVal = true
falseVal = false
truePtr = &trueVal
falsePtr = &falseVal
)
tests := []struct { tests := []struct {
name string name string
namespace string namespace string
resourcePath string resourcePath string
labelSelector labels.Selector labelSelector labels.Selector
fileSystem *fakeFileSystem includeClusterResources *bool
restorers map[schema.GroupResource]restorers.ResourceRestorer fileSystem *fakeFileSystem
expectedErrors api.RestoreResult restorers map[schema.GroupResource]restorers.ResourceRestorer
expectedObjs []unstructured.Unstructured expectedErrors api.RestoreResult
expectedObjs []unstructured.Unstructured
}{ }{
{ {
name: "basic normal case", name: "basic normal case",
@ -394,6 +407,59 @@ func TestRestoreResourceForNamespace(t *testing.T) {
restorers: map[schema.GroupResource]restorers.ResourceRestorer{schema.GroupResource{Resource: "foo-resource"}: newFakeCustomRestorer()}, restorers: map[schema.GroupResource]restorers.ResourceRestorer{schema.GroupResource{Resource: "foo-resource"}: newFakeCustomRestorer()},
expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap), expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap),
}, },
{
name: "cluster-scoped resources are skipped when IncludeClusterResources=false",
namespace: "",
resourcePath: "persistentvolumes",
labelSelector: labels.NewSelector(),
includeClusterResources: falsePtr,
fileSystem: newFakeFileSystem().WithFile("persistentvolumes/pv-1.json", newTestPV().ToJSON()),
},
{
name: "namespaced resources are not skipped when IncludeClusterResources=false",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
includeClusterResources: falsePtr,
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap),
},
{
name: "cluster-scoped resources are not skipped when IncludeClusterResources=true",
namespace: "",
resourcePath: "persistentvolumes",
labelSelector: labels.NewSelector(),
includeClusterResources: truePtr,
fileSystem: newFakeFileSystem().WithFile("persistentvolumes/pv-1.json", newTestPV().ToJSON()),
expectedObjs: toUnstructured(newTestPV().WithArkLabel("my-restore").PersistentVolume),
},
{
name: "namespaced resources are not skipped when IncludeClusterResources=true",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
includeClusterResources: truePtr,
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap),
},
{
name: "cluster-scoped resources are not skipped when IncludeClusterResources=nil",
namespace: "",
resourcePath: "persistentvolumes",
labelSelector: labels.NewSelector(),
includeClusterResources: nil,
fileSystem: newFakeFileSystem().WithFile("persistentvolumes/pv-1.json", newTestPV().ToJSON()),
expectedObjs: toUnstructured(newTestPV().WithArkLabel("my-restore").PersistentVolume),
},
{
name: "namespaced resources are not skipped when IncludeClusterResources=nil",
namespace: "ns-1",
resourcePath: "configmaps",
labelSelector: labels.NewSelector(),
includeClusterResources: nil,
fileSystem: newFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
expectedObjs: toUnstructured(newTestConfigMap().WithArkLabel("my-restore").ConfigMap),
},
} }
for _, test := range tests { for _, test := range tests {
@ -408,6 +474,9 @@ func TestRestoreResourceForNamespace(t *testing.T) {
gv := schema.GroupVersion{Group: "", Version: "v1"} gv := schema.GroupVersion{Group: "", Version: "v1"}
dynamicFactory.On("ClientForGroupVersionResource", gv, resource, test.namespace).Return(resourceClient, nil) dynamicFactory.On("ClientForGroupVersionResource", gv, resource, test.namespace).Return(resourceClient, nil)
pvResource := metav1.APIResource{Name: "persistentvolumes", Namespaced: false}
dynamicFactory.On("ClientForGroupVersionResource", gv, pvResource, test.namespace).Return(resourceClient, nil)
log, _ := testlogger.NewNullLogger() log, _ := testlogger.NewNullLogger()
ctx := &context{ ctx := &context{
@ -420,12 +489,15 @@ func TestRestoreResourceForNamespace(t *testing.T) {
Namespace: api.DefaultNamespace, Namespace: api.DefaultNamespace,
Name: "my-restore", Name: "my-restore",
}, },
Spec: api.RestoreSpec{
IncludeClusterResources: test.includeClusterResources,
},
}, },
backup: &api.Backup{}, backup: &api.Backup{},
logger: log, logger: log,
} }
warnings, errors := ctx.restoreResource("configmaps", test.namespace, test.resourcePath) warnings, errors := ctx.restoreResource(test.resourcePath, test.namespace, test.resourcePath)
assert.Empty(t, warnings.Ark) assert.Empty(t, warnings.Ark)
assert.Empty(t, warnings.Cluster) assert.Empty(t, warnings.Cluster)
@ -517,12 +589,50 @@ func toUnstructured(objs ...runtime.Object) []unstructured.Unstructured {
delete(metadata, "creationTimestamp") delete(metadata, "creationTimestamp")
if _, exists := metadata["namespace"]; !exists {
metadata["namespace"] = ""
}
delete(unstructuredObj.Object, "status")
res = append(res, unstructuredObj) res = append(res, unstructuredObj)
} }
return res return res
} }
type testPersistentVolume struct {
*v1.PersistentVolume
}
func newTestPV() *testPersistentVolume {
return &testPersistentVolume{
PersistentVolume: &v1.PersistentVolume{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "PersistentVolume",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-pv",
},
Status: v1.PersistentVolumeStatus{},
},
}
}
func (pv *testPersistentVolume) WithArkLabel(restoreName string) *testPersistentVolume {
if pv.Labels == nil {
pv.Labels = make(map[string]string)
}
pv.Labels[api.RestoreLabelKey] = restoreName
return pv
}
func (pv *testPersistentVolume) ToJSON() []byte {
bytes, _ := json.Marshal(pv.PersistentVolume)
return bytes
}
type testConfigMap struct { type testConfigMap struct {
*v1.ConfigMap *v1.ConfigMap
} }