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

View File

@@ -0,0 +1 @@
Add ability to restore status on selected resources

View File

@@ -1773,6 +1773,26 @@ spec:
PVs from snapshot (via the cloudprovider).
nullable: true
type: boolean
restoreStatus:
description: RestoreStatus specifies which resources we should restore
the status field. If nil, no objects are included. Optional.
nullable: true
properties:
excludedResources:
description: ExcludedResources specifies the resources to which
will not restore the status.
items:
type: string
nullable: true
type: array
includedResources:
description: IncludedResources specifies the resources to which
will restore the status. If empty, it applies to all resources.
items:
type: string
nullable: true
type: array
type: object
scheduleName:
description: ScheduleName is the unique name of the Velero schedule
to restore from. If specified, and BackupName is empty, Velero will

File diff suppressed because one or more lines are too long

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 {

View File

@@ -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

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)
}

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
}

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
}

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)
})
}
}

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)
}

View File

@@ -46,6 +46,21 @@ spec:
# or fully-qualified. Optional.
excludedResources:
- storageclasses.storage.k8s.io
# restoreStatus selects resources to restore not only the specification, but
# the status of the manifest. This is specially useful for CRDs that maintain
# external references. By default, it excludes all resources.
restoreStatus:
# Array of resources to include in the restore status. Just like above,
# resources may be shortcuts (for example 'po' for 'pods') or fully-qualified.
# If unspecified, no resources are included. Optional.
includedResources:
- workflows
# Array of resources to exclude from the restore status. Resources may be
# shortcuts (for example 'po' for 'pods') or fully-qualified.
# If unspecified, all resources are excluded. Optional.
excludedResources: []
# Whether or not to include cluster-scoped resources. Valid values are true, false, and
# null/unset. If true, all cluster-scoped resources are included (subject to included/excluded
# resources and the label selector). If false, no cluster-scoped resources are included. If unset,