Merge pull request #4785 from RafaeLeal/restore-status

Add ability to restore status on selected resources
This commit is contained in:
Scott Seago
2022-05-24 09:41:18 -04:00
committed by GitHub
11 changed files with 272 additions and 83 deletions
+19
View File
@@ -86,6 +86,12 @@ type RestoreSpec struct {
// +nullable
RestorePVs *bool `json:"restorePVs,omitempty"`
// RestoreStatus specifies which resources we should restore the status
// field. If nil, no objects are included. Optional.
// +optional
// +nullable
RestoreStatus *RestoreStatusSpec `json:"restoreStatus,omitempty"`
// PreserveNodePorts specifies whether to restore old nodePorts from backup.
// +optional
// +nullable
@@ -113,6 +119,19 @@ type RestoreHooks struct {
Resources []RestoreResourceHookSpec `json:"resources,omitempty"`
}
type RestoreStatusSpec struct {
// IncludedResources specifies the resources to which will restore the status.
// If empty, it applies to all resources.
// +optional
// +nullable
IncludedResources []string `json:"includedResources,omitempty"`
// ExcludedResources specifies the resources to which will not restore the status.
// +optional
// +nullable
ExcludedResources []string `json:"excludedResources,omitempty"`
}
// RestoreResourceHookSpec defines one or more RestoreResrouceHooks that should be executed based on
// the rules defined for namespaces, resources, and label selector.
type RestoreResourceHookSpec struct {
@@ -1278,6 +1278,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
*out = new(bool)
**out = **in
}
if in.RestoreStatus != nil {
in, out := &in.RestoreStatus, &out.RestoreStatus
*out = new(RestoreStatusSpec)
(*in).DeepCopyInto(*out)
}
if in.PreserveNodePorts != nil {
in, out := &in.PreserveNodePorts, &out.PreserveNodePorts
*out = new(bool)
@@ -1334,6 +1339,31 @@ func (in *RestoreStatus) DeepCopy() *RestoreStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreStatusSpec) DeepCopyInto(out *RestoreStatusSpec) {
*out = *in
if in.IncludedResources != nil {
in, out := &in.IncludedResources, &out.IncludedResources
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ExcludedResources != nil {
in, out := &in.ExcludedResources, &out.ExcludedResources
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreStatusSpec.
func (in *RestoreStatusSpec) DeepCopy() *RestoreStatusSpec {
if in == nil {
return nil
}
out := new(RestoreStatusSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Schedule) DeepCopyInto(out *Schedule) {
*out = *in
+10
View File
@@ -89,6 +89,11 @@ type Deletor interface {
Delete(name string, opts metav1.DeleteOptions) error
}
// StatusUpdater updates status field of a object
type StatusUpdater interface {
UpdateStatus(obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error)
}
// Dynamic contains client methods that Velero needs for backing up and restoring resources.
type Dynamic interface {
Creator
@@ -97,6 +102,7 @@ type Dynamic interface {
Getter
Patcher
Deletor
StatusUpdater
}
// dynamicResourceClient implements Dynamic.
@@ -129,3 +135,7 @@ func (d *dynamicResourceClient) Patch(name string, data []byte) (*unstructured.U
func (d *dynamicResourceClient) Delete(name string, opts metav1.DeleteOptions) error {
return d.resourceClient.Delete(context.TODO(), name, opts)
}
func (d *dynamicResourceClient) UpdateStatus(obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error) {
return d.resourceClient.UpdateStatus(context.TODO(), obj, opts)
}
+11
View File
@@ -85,6 +85,8 @@ type CreateOptions struct {
ExistingResourcePolicy string
IncludeResources flag.StringArray
ExcludeResources flag.StringArray
StatusIncludeResources flag.StringArray
StatusExcludeResources flag.StringArray
NamespaceMappings flag.Map
Selector flag.LabelSelector
IncludeClusterResources flag.OptionalBool
@@ -115,6 +117,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources).")
flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io.")
flags.StringVar(&o.ExistingResourcePolicy, "existing-resource-policy", "", "Restore Policy to be used during the restore workflow, can be - none or update")
flags.Var(&o.StatusIncludeResources, "status-include-resources", "Resources to include in the restore status, formatted as resource.group, such as storageclasses.storage.k8s.io.")
flags.Var(&o.StatusExcludeResources, "status-exclude-resources", "Resources to exclude from the restore status, formatted as resource.group, such as storageclasses.storage.k8s.io.")
flags.VarP(&o.Selector, "selector", "l", "Only restore resources matching this label selector.")
f := flags.VarPF(&o.RestoreVolumes, "restore-volumes", "", "Whether to restore volumes from snapshots.")
// this allows the user to just specify "--restore-volumes" as shorthand for "--restore-volumes=true"
@@ -279,6 +283,13 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
},
}
if len([]string(o.StatusIncludeResources)) > 0 {
restore.Spec.RestoreStatus = &api.RestoreStatusSpec{
IncludedResources: o.StatusIncludeResources,
ExcludedResources: o.StatusExcludeResources,
}
}
if printed, err := output.PrintWithFormat(c, restore); printed || err != nil {
return err
}
+126 -75
View File
@@ -206,6 +206,20 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
req.Restore.Spec.ExcludedResources,
)
// Get resource status includes-excludes. Defaults to excluding all resources
restoreStatusIncludesExcludes := collections.GetResourceIncludesExcludes(
kr.discoveryHelper,
[]string{},
[]string{"*"},
)
if req.Restore.Spec.RestoreStatus != nil {
restoreStatusIncludesExcludes = collections.GetResourceIncludesExcludes(
kr.discoveryHelper,
req.Restore.Spec.RestoreStatus.IncludedResources,
req.Restore.Spec.RestoreStatus.ExcludedResources,
)
}
// Get namespace includes-excludes.
namespaceIncludesExcludes := collections.NewIncludesExcludes().
Includes(req.Restore.Spec.IncludedNamespaces...).
@@ -268,83 +282,85 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
}
restoreCtx := &restoreContext{
backup: req.Backup,
backupReader: req.BackupReader,
restore: req.Restore,
resourceIncludesExcludes: resourceIncludesExcludes,
namespaceIncludesExcludes: namespaceIncludesExcludes,
chosenGrpVersToRestore: make(map[string]ChosenGroupVersion),
selector: selector,
OrSelectors: OrSelectors,
log: req.Log,
dynamicFactory: kr.dynamicFactory,
fileSystem: kr.fileSystem,
namespaceClient: kr.namespaceClient,
restoreItemActions: resolvedActions,
itemSnapshotterActions: resolvedItemSnapshotterActions,
volumeSnapshotterGetter: volumeSnapshotterGetter,
resticRestorer: resticRestorer,
resticErrs: make(chan error),
pvsToProvision: sets.NewString(),
pvRestorer: pvRestorer,
volumeSnapshots: req.VolumeSnapshots,
podVolumeBackups: req.PodVolumeBackups,
resourceTerminatingTimeout: kr.resourceTerminatingTimeout,
resourceClients: make(map[resourceClientKey]client.Dynamic),
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
renamedPVs: make(map[string]string),
pvRenamer: kr.pvRenamer,
discoveryHelper: kr.discoveryHelper,
resourcePriorities: kr.resourcePriorities,
resourceRestoreHooks: resourceRestoreHooks,
hooksErrs: make(chan error),
waitExecHookHandler: waitExecHookHandler,
hooksContext: hooksCtx,
hooksCancelFunc: hooksCancelFunc,
restoreClient: kr.restoreClient,
backup: req.Backup,
backupReader: req.BackupReader,
restore: req.Restore,
resourceIncludesExcludes: resourceIncludesExcludes,
resourceStatusIncludesExcludes: restoreStatusIncludesExcludes,
namespaceIncludesExcludes: namespaceIncludesExcludes,
chosenGrpVersToRestore: make(map[string]ChosenGroupVersion),
selector: selector,
OrSelectors: OrSelectors,
log: req.Log,
dynamicFactory: kr.dynamicFactory,
fileSystem: kr.fileSystem,
namespaceClient: kr.namespaceClient,
restoreItemActions: resolvedActions,
itemSnapshotterActions: resolvedItemSnapshotterActions,
volumeSnapshotterGetter: volumeSnapshotterGetter,
resticRestorer: resticRestorer,
resticErrs: make(chan error),
pvsToProvision: sets.NewString(),
pvRestorer: pvRestorer,
volumeSnapshots: req.VolumeSnapshots,
podVolumeBackups: req.PodVolumeBackups,
resourceTerminatingTimeout: kr.resourceTerminatingTimeout,
resourceClients: make(map[resourceClientKey]client.Dynamic),
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
renamedPVs: make(map[string]string),
pvRenamer: kr.pvRenamer,
discoveryHelper: kr.discoveryHelper,
resourcePriorities: kr.resourcePriorities,
resourceRestoreHooks: resourceRestoreHooks,
hooksErrs: make(chan error),
waitExecHookHandler: waitExecHookHandler,
hooksContext: hooksCtx,
hooksCancelFunc: hooksCancelFunc,
restoreClient: kr.restoreClient,
}
return restoreCtx.execute()
}
type restoreContext struct {
backup *velerov1api.Backup
backupReader io.Reader
restore *velerov1api.Restore
restoreDir string
restoreClient velerov1client.RestoresGetter
resourceIncludesExcludes *collections.IncludesExcludes
namespaceIncludesExcludes *collections.IncludesExcludes
chosenGrpVersToRestore map[string]ChosenGroupVersion
selector labels.Selector
OrSelectors []labels.Selector
log logrus.FieldLogger
dynamicFactory client.DynamicFactory
fileSystem filesystem.Interface
namespaceClient corev1.NamespaceInterface
restoreItemActions []framework.RestoreItemResolvedAction
itemSnapshotterActions []framework.ItemSnapshotterResolvedAction
volumeSnapshotterGetter VolumeSnapshotterGetter
resticRestorer restic.Restorer
resticWaitGroup sync.WaitGroup
resticErrs chan error
pvsToProvision sets.String
pvRestorer PVRestorer
volumeSnapshots []*volume.Snapshot
podVolumeBackups []*velerov1api.PodVolumeBackup
resourceTerminatingTimeout time.Duration
resourceClients map[resourceClientKey]client.Dynamic
restoredItems map[velero.ResourceIdentifier]struct{}
renamedPVs map[string]string
pvRenamer func(string) (string, error)
discoveryHelper discovery.Helper
resourcePriorities []string
hooksWaitGroup sync.WaitGroup
hooksErrs chan error
resourceRestoreHooks []hook.ResourceRestoreHook
waitExecHookHandler hook.WaitExecHookHandler
hooksContext go_context.Context
hooksCancelFunc go_context.CancelFunc
backup *velerov1api.Backup
backupReader io.Reader
restore *velerov1api.Restore
restoreDir string
restoreClient velerov1client.RestoresGetter
resourceIncludesExcludes *collections.IncludesExcludes
resourceStatusIncludesExcludes *collections.IncludesExcludes
namespaceIncludesExcludes *collections.IncludesExcludes
chosenGrpVersToRestore map[string]ChosenGroupVersion
selector labels.Selector
OrSelectors []labels.Selector
log logrus.FieldLogger
dynamicFactory client.DynamicFactory
fileSystem filesystem.Interface
namespaceClient corev1.NamespaceInterface
restoreItemActions []framework.RestoreItemResolvedAction
itemSnapshotterActions []framework.ItemSnapshotterResolvedAction
volumeSnapshotterGetter VolumeSnapshotterGetter
resticRestorer restic.Restorer
resticWaitGroup sync.WaitGroup
resticErrs chan error
pvsToProvision sets.String
pvRestorer PVRestorer
volumeSnapshots []*volume.Snapshot
podVolumeBackups []*velerov1api.PodVolumeBackup
resourceTerminatingTimeout time.Duration
resourceClients map[resourceClientKey]client.Dynamic
restoredItems map[velero.ResourceIdentifier]struct{}
renamedPVs map[string]string
pvRenamer func(string) (string, error)
discoveryHelper discovery.Helper
resourcePriorities []string
hooksWaitGroup sync.WaitGroup
hooksErrs chan error
resourceRestoreHooks []hook.ResourceRestoreHook
waitExecHookHandler hook.WaitExecHookHandler
hooksContext go_context.Context
hooksCancelFunc go_context.CancelFunc
}
type resourceClientKey struct {
@@ -1111,19 +1127,21 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
}
objStatus, statusFieldExists, statusFieldErr := unstructured.NestedFieldCopy(obj.Object, "status")
// Clear out non-core metadata fields and status.
if obj, err = resetMetadataAndStatus(obj); err != nil {
errs.Add(namespace, err)
return warnings, errs
}
ctx.log.Infof("restore status includes excludes: %+v", ctx.resourceStatusIncludesExcludes)
for _, action := range ctx.getApplicableActions(groupResource, namespace) {
if !action.Selector.Matches(labels.Set(obj.GetLabels())) {
continue
}
ctx.log.Infof("Executing item action for %v", &groupResource)
executeOutput, err := action.RestoreItemAction.Execute(&velero.RestoreItemActionExecuteInput{
Item: obj,
ItemFromBackup: itemFromBackup,
@@ -1344,6 +1362,29 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
return warnings, errs
}
shouldRestoreStatus := ctx.resourceStatusIncludesExcludes.ShouldInclude(groupResource.String())
if shouldRestoreStatus && statusFieldErr != nil {
err := fmt.Errorf("could not get status to be restored %s: %v", kube.NamespaceAndName(obj), statusFieldErr)
ctx.log.Errorf(err.Error())
errs.Add(namespace, err)
return warnings, errs
}
// if it should restore status, run a UpdateStatus
if statusFieldExists && shouldRestoreStatus {
if err := unstructured.SetNestedField(obj.Object, objStatus, "status"); err != nil {
ctx.log.Errorf("could not set status field %s: %v", kube.NamespaceAndName(obj), err)
errs.Add(namespace, err)
return warnings, errs
}
obj.SetResourceVersion(createdObj.GetResourceVersion())
updated, err := resourceClient.UpdateStatus(obj, metav1.UpdateOptions{})
if err != nil {
warnings.Add(namespace, err)
} else {
createdObj = updated
}
}
if groupResource == kuberesource.Pods {
pod := new(v1.Pod)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pod); err != nil {
@@ -1631,7 +1672,7 @@ func resetVolumeBindingInfo(obj *unstructured.Unstructured) *unstructured.Unstru
return obj
}
func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
func resetMetadata(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
res, ok := obj.Object["metadata"]
if !ok {
return nil, errors.New("metadata not found")
@@ -1649,9 +1690,19 @@ func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstr
}
}
// Never restore status
delete(obj.UnstructuredContent(), "status")
return obj, nil
}
func resetStatus(obj *unstructured.Unstructured) {
unstructured.RemoveNestedField(obj.UnstructuredContent(), "status")
}
func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
_, err := resetMetadata(obj)
if err != nil {
return nil, err
}
resetStatus(obj)
return obj, nil
}
+34 -7
View File
@@ -1867,6 +1867,7 @@ func assertRestoredItems(t *testing.T, h *harness, want []*test.APIResource) {
// empty in the structured objects. Remove them to make comparison easier.
unstructured.RemoveNestedField(want.Object, "metadata", "creationTimestamp")
unstructured.RemoveNestedField(want.Object, "status")
unstructured.RemoveNestedField(res.Object, "status")
assert.Equal(t, want, res)
}
@@ -2805,7 +2806,7 @@ func TestRestoreWithRestic(t *testing.T) {
}
}
func TestResetMetadataAndStatus(t *testing.T) {
func TestResetMetadata(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
@@ -2824,20 +2825,46 @@ func TestResetMetadataAndStatus(t *testing.T) {
expectedRes: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
},
{
name: "don't keep status",
name: "keep status",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := resetMetadata(test.obj)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
func TestResetStatus(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
expectedRes *unstructured.Unstructured
}{
{
name: "no status don't cause error",
obj: &unstructured.Unstructured{},
expectedRes: &unstructured.Unstructured{},
},
{
name: "remove status",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
expectedRes: NewTestUnstructured().WithMetadata().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := resetMetadataAndStatus(test.obj)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
resetStatus(test.obj)
assert.Equal(t, test.expectedRes, test.obj)
})
}
}
+5
View File
@@ -72,3 +72,8 @@ func (c *FakeDynamicClient) Delete(name string, opts metav1.DeleteOptions) error
args := c.Called(name, opts)
return args.Error(1)
}
func (c *FakeDynamicClient) UpdateStatus(obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error) {
args := c.Called(obj, opts)
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}