diff --git a/docs/cli-reference/ark_create_restore.md b/docs/cli-reference/ark_create_restore.md index b0422c175..071757d20 100644 --- a/docs/cli-reference/ark_create_restore.md +++ b/docs/cli-reference/ark_create_restore.md @@ -14,18 +14,19 @@ ark create restore BACKUP [flags] ### Options ``` - --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 - -h, --help help for restore - --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) - --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) - --label-columns stringArray a comma-separated list of labels to be displayed as columns - --labels mapStringString labels to apply to the restore - --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... - -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'. - --restore-volumes optionalBool[=true] whether to restore volumes from snapshots - -l, --selector labelSelector only restore resources matching this label selector (default ) - --show-labels show labels in the last column + --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 + -h, --help help for restore + --include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore + --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) + --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) + --label-columns stringArray a comma-separated list of labels to be displayed as columns + --labels mapStringString labels to apply to the restore + --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... + -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'. + --restore-volumes optionalBool[=true] whether to restore volumes from snapshots + -l, --selector labelSelector only restore resources matching this label selector (default ) + --show-labels show labels in the last column ``` ### Options inherited from parent commands diff --git a/docs/cli-reference/ark_restore_create.md b/docs/cli-reference/ark_restore_create.md index 395dcbe3f..ec984dfab 100644 --- a/docs/cli-reference/ark_restore_create.md +++ b/docs/cli-reference/ark_restore_create.md @@ -14,18 +14,19 @@ ark restore create BACKUP [flags] ### Options ``` - --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 - -h, --help help for create - --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) - --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) - --label-columns stringArray a comma-separated list of labels to be displayed as columns - --labels mapStringString labels to apply to the restore - --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... - -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'. - --restore-volumes optionalBool[=true] whether to restore volumes from snapshots - -l, --selector labelSelector only restore resources matching this label selector (default ) - --show-labels show labels in the last column + --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 + -h, --help help for create + --include-cluster-resources optionalBool[=true] include cluster-scoped resources in the restore + --include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces) (default *) + --include-resources stringArray resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources) + --label-columns stringArray a comma-separated list of labels to be displayed as columns + --labels mapStringString labels to apply to the restore + --namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,... + -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'. + --restore-volumes optionalBool[=true] whether to restore volumes from snapshots + -l, --selector labelSelector only restore resources matching this label selector (default ) + --show-labels show labels in the last column ``` ### Options inherited from parent commands diff --git a/pkg/apis/ark/v1/restore.go b/pkg/apis/ark/v1/restore.go index 17835966f..196532cc1 100644 --- a/pkg/apis/ark/v1/restore.go +++ b/pkg/apis/ark/v1/restore.go @@ -54,6 +54,11 @@ type RestoreSpec struct { // RestorePVs specifies whether to restore all included // PVs from snapshot (via the cloudprovider). 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 diff --git a/pkg/cmd/cli/restore/create.go b/pkg/cmd/cli/restore/create.go index d0caf2054..edddabb8d 100644 --- a/pkg/cmd/cli/restore/create.go +++ b/pkg/cmd/cli/restore/create.go @@ -54,23 +54,25 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command { } type CreateOptions struct { - BackupName string - RestoreVolumes flag.OptionalBool - Labels flag.Map - IncludeNamespaces flag.StringArray - ExcludeNamespaces flag.StringArray - IncludeResources flag.StringArray - ExcludeResources flag.StringArray - NamespaceMappings flag.Map - Selector flag.LabelSelector + BackupName string + RestoreVolumes flag.OptionalBool + Labels flag.Map + IncludeNamespaces flag.StringArray + ExcludeNamespaces flag.StringArray + IncludeResources flag.StringArray + ExcludeResources flag.StringArray + NamespaceMappings flag.Map + Selector flag.LabelSelector + IncludeClusterResources flag.OptionalBool } func NewCreateOptions() *CreateOptions { return &CreateOptions{ - Labels: flag.NewMap(), - IncludeNamespaces: flag.NewStringArray("*"), - NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"), - RestoreVolumes: flag.NewOptionalBool(nil), + Labels: flag.NewMap(), + IncludeNamespaces: flag.NewStringArray("*"), + NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"), + 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" // like a normal bool flag 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 { @@ -118,14 +123,15 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { Labels: o.Labels.Data(), }, Spec: api.RestoreSpec{ - BackupName: o.BackupName, - IncludedNamespaces: o.IncludeNamespaces, - ExcludedNamespaces: o.ExcludeNamespaces, - IncludedResources: o.IncludeResources, - ExcludedResources: o.ExcludeResources, - NamespaceMapping: o.NamespaceMappings.Data(), - LabelSelector: o.Selector.LabelSelector, - RestorePVs: o.RestoreVolumes.Value, + BackupName: o.BackupName, + IncludedNamespaces: o.IncludeNamespaces, + ExcludedNamespaces: o.ExcludeNamespaces, + IncludedResources: o.IncludeResources, + ExcludedResources: o.ExcludeResources, + NamespaceMapping: o.NamespaceMappings.Data(), + LabelSelector: o.Selector.LabelSelector, + RestorePVs: o.RestoreVolumes.Value, + IncludeClusterResources: o.IncludeClusterResources.Value, }, } diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index bac2d563e..cad0c2d89 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -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) { 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 != "" { ctx.infof("Restoring resource '%s' into namespace '%s' from: %s", resource, namespace, resourcePath) } else { diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 184e741f9..6596a99e2 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -159,10 +159,15 @@ func TestRestoreNamespaceFiltering(t *testing.T) { }, }, { - 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"), - baseDir: "bak", - restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"a", "b", "c"}, ExcludedNamespaces: []string{"b"}}}, + 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"), + baseDir: "bak", + 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"}, prioritizedResources: []schema.GroupResource{ schema.GroupResource{Resource: "nodes"}, @@ -288,15 +293,23 @@ func TestRestorePriority(t *testing.T) { } func TestRestoreResourceForNamespace(t *testing.T) { + var ( + trueVal = true + falseVal = false + truePtr = &trueVal + falsePtr = &falseVal + ) + 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 string + namespace string + resourcePath string + labelSelector labels.Selector + includeClusterResources *bool + fileSystem *fakeFileSystem + restorers map[schema.GroupResource]restorers.ResourceRestorer + expectedErrors api.RestoreResult + expectedObjs []unstructured.Unstructured }{ { 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()}, 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 { @@ -408,6 +474,9 @@ func TestRestoreResourceForNamespace(t *testing.T) { gv := schema.GroupVersion{Group: "", Version: "v1"} 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() ctx := &context{ @@ -420,12 +489,15 @@ func TestRestoreResourceForNamespace(t *testing.T) { Namespace: api.DefaultNamespace, Name: "my-restore", }, + Spec: api.RestoreSpec{ + IncludeClusterResources: test.includeClusterResources, + }, }, backup: &api.Backup{}, 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.Cluster) @@ -517,12 +589,50 @@ func toUnstructured(objs ...runtime.Object) []unstructured.Unstructured { delete(metadata, "creationTimestamp") + if _, exists := metadata["namespace"]; !exists { + metadata["namespace"] = "" + } + + delete(unstructuredObj.Object, "status") + res = append(res, unstructuredObj) } 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 { *v1.ConfigMap }