RIAv2 async operations controller work

Signed-off-by: Scott Seago <sseago@redhat.com>
This commit is contained in:
Scott Seago
2023-03-17 14:30:39 -04:00
parent 117d5e846f
commit 2155b2b215
33 changed files with 1478 additions and 79 deletions
+1
View File
@@ -0,0 +1 @@
RIAv2 async operations controller work
@@ -238,6 +238,10 @@ spec:
type: string
nullable: true
type: array
itemOperationTimeout:
description: ItemOperationTimeout specifies the time used to wait
for RestoreItemAction operations The default value is 1 hour.
type: string
labelSelector:
description: LabelSelector is a metav1.LabelSelector to filter with
when restoring individual objects from the backup. If empty or nil,
@@ -434,6 +438,20 @@ spec:
due to plugins that return additional related items to restore
type: integer
type: object
restoreItemOperationsAttempted:
description: RestoreItemOperationsAttempted is the total number of
attempted async RestoreItemAction operations for this restore.
type: integer
restoreItemOperationsCompleted:
description: RestoreItemOperationsCompleted is the total number of
successfully completed async RestoreItemAction operations for this
restore.
type: integer
restoreItemOperationsFailed:
description: RestoreItemOperationsFailed is the total number of async
RestoreItemAction operations for this restore which ended with an
error.
type: integer
startTimestamp:
description: StartTimestamp records the time the restore operation
was started. The server's time is used for StartTimestamps
File diff suppressed because one or more lines are too long
+6
View File
@@ -184,6 +184,12 @@ rules:
- get
- patch
- update
- apiGroups:
- velero.io
resources:
- restorestoragelocations
verbs:
- get
- apiGroups:
- velero.io
resources:
+21
View File
@@ -98,6 +98,27 @@ message OperationProgress {
}
```
In addition to the three new rpc methods added to the RestoreItemAction interface, there is also a new `Name()` method. This one is only actually used internally by Velero to get the name that the plugin was registered with, but it still must be defined in a plugin which implements RestoreItemActionV2 in order to implement the interface. It doesn't really matter what it returns, though, as this particular method is not delegated to the plugin via RPC calls. The new (and modified) interface methods for `RestoreItemAction` are as follows:
```
type BackupItemAction interface {
...
Name() string
...
Progress(operationID string, restore *api.Restore) (velero.OperationProgress, error)
Cancel(operationID string, backup *api.Restore) error
AreAdditionalItemsReady(AdditionalItems []velero.ResourceIdentifier, restore *api.Restore) (bool, error)
...
}
type RestoreItemActionExecuteOutput struct {
UpdatedItem runtime.Unstructured
AdditionalItems []ResourceIdentifier
SkipRestore bool
OperationID string
WaitForAdditionalItems bool
}
```
A new PluginKind, `RestoreItemActionV2`, will be created, and the restore process will be modified to use this plugin kind.
See [Plugin Versioning](plugin-versioning.md) for more details on implementation plans, including v1 adapters, etc.
+20
View File
@@ -112,6 +112,11 @@ type RestoreSpec struct {
// +optional
// +nullable
ExistingResourcePolicy PolicyType `json:"existingResourcePolicy,omitempty"`
// ItemOperationTimeout specifies the time used to wait for RestoreItemAction operations
// The default value is 1 hour.
// +optional
ItemOperationTimeout metav1.Duration `json:"itemOperationTimeout,omitempty"`
}
// RestoreHooks contains custom behaviors that should be executed during or post restore.
@@ -314,6 +319,21 @@ type RestoreStatus struct {
// +optional
// +nullable
Progress *RestoreProgress `json:"progress,omitempty"`
// RestoreItemOperationsAttempted is the total number of attempted
// async RestoreItemAction operations for this restore.
// +optional
RestoreItemOperationsAttempted int `json:"restoreItemOperationsAttempted,omitempty"`
// RestoreItemOperationsCompleted is the total number of successfully completed
// async RestoreItemAction operations for this restore.
// +optional
RestoreItemOperationsCompleted int `json:"restoreItemOperationsCompleted,omitempty"`
// RestoreItemOperationsFailed is the total number of async
// RestoreItemAction operations for this restore which ended with an error.
// +optional
RestoreItemOperationsFailed int `json:"restoreItemOperationsFailed,omitempty"`
}
// RestoreProgress stores information about the restore's execution progress
@@ -1321,6 +1321,7 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
**out = **in
}
in.Hooks.DeepCopyInto(&out.Hooks)
out.ItemOperationTimeout = in.ItemOperationTimeout
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSpec.
+6
View File
@@ -165,3 +165,9 @@ func (b *RestoreBuilder) CompletionTimestamp(val time.Time) *RestoreBuilder {
b.object.Status.CompletionTimestamp = &metav1.Time{Time: val}
return b
}
// ItemOperationTimeout sets the Restore's ItemOperationTimeout
func (b *RestoreBuilder) ItemOperationTimeout(timeout time.Duration) *RestoreBuilder {
b.object.Spec.ItemOperationTimeout.Duration = timeout
return b
}
+5
View File
@@ -92,6 +92,7 @@ type CreateOptions struct {
IncludeClusterResources flag.OptionalBool
Wait bool
AllowPartiallyFailed flag.OptionalBool
ItemOperationTimeout time.Duration
client veleroclient.Interface
}
@@ -120,6 +121,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
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.")
flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.")
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"
// like a normal bool flag
@@ -280,6 +282,9 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
RestorePVs: o.RestoreVolumes.Value,
PreserveNodePorts: o.PreserveNodePorts.Value,
IncludeClusterResources: o.IncludeClusterResources.Value,
ItemOperationTimeout: metav1.Duration{
Duration: o.ItemOperationTimeout,
},
},
}
+2 -2
View File
@@ -63,8 +63,8 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
}
switch restore.Status.Phase {
case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed:
// terminal phases, don't exit.
case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed:
// terminal and waiting for plugin operations phases, don't exit.
default:
cmd.Exit("Logs for restore %q are not available until it's finished processing. Please wait "+
"until the restore has a phase of Completed or Failed and try again.", restoreName)
+20
View File
@@ -652,6 +652,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
controller.DownloadRequest: {},
controller.GarbageCollection: {},
controller.Restore: {},
controller.RestoreOperations: {},
controller.Schedule: {},
controller.ServerStatusRequest: {},
}
@@ -831,6 +832,23 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
}
}
restoreOpsMap := itemoperationmap.NewRestoreItemOperationsMap()
if _, ok := enabledRuntimeControllers[controller.RestoreOperations]; ok {
r := controller.NewRestoreOperationsReconciler(
s.logger,
s.namespace,
s.mgr.GetClient(),
s.config.itemOperationSyncFrequency,
newPluginManager,
backupStoreGetter,
s.metrics,
restoreOpsMap,
)
if err := r.SetupWithManager(s.mgr); err != nil {
s.logger.Fatal(err, "unable to create controller", "controller", controller.BackupOperations)
}
}
if _, ok := enabledRuntimeControllers[controller.DownloadRequest]; ok {
r := controller.NewDownloadRequestReconciler(
s.mgr.GetClient(),
@@ -839,6 +857,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
backupStoreGetter,
s.logger,
backupOpsMap,
restoreOpsMap,
)
if err := r.SetupWithManager(s.mgr); err != nil {
s.logger.Fatal(err, "unable to create controller", "controller", controller.DownloadRequest)
@@ -890,6 +909,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
backupStoreGetter,
s.metrics,
s.config.formatFlag.Parse(),
s.config.defaultItemOperationTimeout,
)
if err = r.SetupWithManager(s.mgr); err != nil {
+59
View File
@@ -32,6 +32,7 @@ import (
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/util/results"
)
@@ -154,11 +155,14 @@ func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *vel
s = string(restore.Spec.ExistingResourcePolicy)
}
d.Printf("Existing Resource Policy: \t%s\n", s)
d.Printf("ItemOperationTimeout:\t%s\n", restore.Spec.ItemOperationTimeout.Duration)
d.Println()
d.Printf("Preserve Service NodePorts:\t%s\n", BoolPointerString(restore.Spec.PreserveNodePorts, "false", "true", "auto"))
d.Println()
describeRestoreItemOperations(ctx, kbClient, d, restore, details, insecureSkipTLSVerify, caCertFile)
if details {
describeRestoreResourceList(ctx, kbClient, d, restore, insecureSkipTLSVerify, caCertFile)
d.Println()
@@ -166,6 +170,33 @@ func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *vel
})
}
func describeRestoreItemOperations(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, details bool, insecureSkipTLSVerify bool, caCertPath string) {
status := restore.Status
if status.RestoreItemOperationsAttempted > 0 {
if !details {
d.Printf("Restore Item Operations:\t%d of %d completed successfully, %d failed (specify --details for more information)\n", status.RestoreItemOperationsCompleted, status.RestoreItemOperationsAttempted, status.RestoreItemOperationsFailed)
return
}
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(ctx, kbClient, restore.Namespace, restore.Name, velerov1api.DownloadTargetKindRestoreItemOperations, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
d.Printf("Restore Item Operations:\t<error getting operation info: %v>\n", err)
return
}
var operations []*itemoperation.RestoreOperation
if err := json.NewDecoder(buf).Decode(&operations); err != nil {
d.Printf("Restore Item Operations:\t<error reading operation info: %v>\n", err)
return
}
d.Printf("Restore Item Operations:\n")
for _, operation := range operations {
describeRestoreItemOperation(d, operation)
}
}
}
func describeRestoreResults(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, insecureSkipTLSVerify bool, caCertPath string) {
if restore.Status.Warnings == 0 && restore.Status.Errors == 0 {
return
@@ -208,6 +239,34 @@ func describeResult(d *Describer, name string, result results.Result) {
}
}
func describeRestoreItemOperation(d *Describer, operation *itemoperation.RestoreOperation) {
d.Printf("\tOperation for %s %s/%s:\n", operation.Spec.ResourceIdentifier, operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name)
d.Printf("\t\tRestore Item Action Plugin:\t%s\n", operation.Spec.RestoreItemAction)
d.Printf("\t\tOperation ID:\t%s\n", operation.Spec.OperationID)
d.Printf("\t\tPhase:\t%s\n", operation.Status.Phase)
if operation.Status.Error != "" {
d.Printf("\t\tOperation Error:\t%s\n", operation.Status.Error)
}
if operation.Status.NTotal > 0 || operation.Status.NCompleted > 0 {
d.Printf("\t\tProgress:\t%v of %v complete (%s)\n",
operation.Status.NCompleted,
operation.Status.NTotal,
operation.Status.OperationUnits)
}
if operation.Status.Description != "" {
d.Printf("\t\tProgress description:\t%s\n", operation.Status.Description)
}
if operation.Status.Created != nil {
d.Printf("\t\tCreated:\t%s\n", operation.Status.Created.String())
}
if operation.Status.Started != nil {
d.Printf("\t\tStarted:\t%s\n", operation.Status.Started.String())
}
if operation.Status.Updated != nil {
d.Printf("\t\tUpdated:\t%s\n", operation.Status.Updated.String())
}
}
// describePodVolumeRestores describes pod volume restores in human-readable format.
func describePodVolumeRestores(d *Describer, restores []velerov1api.PodVolumeRestore, details bool) {
// Get the type of pod volume uploader. Since the uploader only comes from a single source, we can
+4 -2
View File
@@ -17,8 +17,8 @@ limitations under the License.
package controller
const (
BackupOperations = "backup-operations"
Backup = "backup"
BackupOperations = "backup-operations"
BackupDeletion = "backup-deletion"
BackupFinalizer = "backup-finalizer"
BackupRepo = "backup-repo"
@@ -29,14 +29,15 @@ const (
PodVolumeBackup = "pod-volume-backup"
PodVolumeRestore = "pod-volume-restore"
Restore = "restore"
RestoreOperations = "restore-operations"
Schedule = "schedule"
ServerStatusRequest = "server-status-request"
)
// DisableableControllers is a list of controllers that can be disabled
var DisableableControllers = []string{
BackupOperations,
Backup,
BackupOperations,
BackupDeletion,
BackupFinalizer,
BackupSync,
@@ -44,6 +45,7 @@ var DisableableControllers = []string{
GarbageCollection,
BackupRepo,
Restore,
RestoreOperations,
Schedule,
ServerStatusRequest,
}
+19 -7
View File
@@ -44,6 +44,8 @@ type downloadRequestReconciler struct {
// used to force update of async backup item operations before processing download request
backupItemOperationsMap *itemoperationmap.BackupItemOperationsMap
// used to force update of async restore item operations before processing download request
restoreItemOperationsMap *itemoperationmap.RestoreItemOperationsMap
log logrus.FieldLogger
}
@@ -56,14 +58,16 @@ func NewDownloadRequestReconciler(
backupStoreGetter persistence.ObjectBackupStoreGetter,
log logrus.FieldLogger,
backupItemOperationsMap *itemoperationmap.BackupItemOperationsMap,
restoreItemOperationsMap *itemoperationmap.RestoreItemOperationsMap,
) *downloadRequestReconciler {
return &downloadRequestReconciler{
client: client,
clock: clock,
newPluginManager: newPluginManager,
backupStoreGetter: backupStoreGetter,
backupItemOperationsMap: backupItemOperationsMap,
log: log,
client: client,
clock: clock,
newPluginManager: newPluginManager,
backupStoreGetter: backupStoreGetter,
backupItemOperationsMap: backupItemOperationsMap,
restoreItemOperationsMap: restoreItemOperationsMap,
log: log,
}
}
@@ -129,7 +133,8 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreLog ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResourceList {
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResourceList ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreItemOperations {
restore := &velerov1api.Restore{}
if err := r.client.Get(ctx, kbclient.ObjectKey{
Namespace: downloadRequest.Namespace,
@@ -172,6 +177,13 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// ignore errors here. If we can't upload anything here, process the download as usual
_ = r.backupItemOperationsMap.UpdateForBackup(backupStore, backupName)
}
// If this is a request for restore item operations, force upload of in-memory operations that
// are not yet uploaded (if there are any)
if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreItemOperations &&
r.restoreItemOperationsMap != nil {
// ignore errors here. If we can't upload anything here, process the download as usual
_ = r.restoreItemOperationsMap.UpdateForRestore(backupStore, downloadRequest.Spec.Target.Name)
}
if downloadRequest.Status.DownloadURL, err = backupStore.GetDownloadURL(downloadRequest.Spec.Target); err != nil {
return ctrl.Result{Requeue: true}, errors.WithStack(err)
}
@@ -113,6 +113,7 @@ var _ = Describe("Download Request Reconciler", func() {
NewFakeObjectBackupStoreGetter(backupStores),
velerotest.NewLogger(),
nil,
nil,
)
if test.backupLocation != nil && test.expectGetsURL {
+98 -31
View File
@@ -40,6 +40,7 @@ import (
"github.com/vmware-tanzu/velero/internal/hook"
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/metrics"
"github.com/vmware-tanzu/velero/pkg/persistence"
@@ -84,15 +85,16 @@ var nonRestorableResources = []string{
}
type restoreReconciler struct {
ctx context.Context
namespace string
restorer pkgrestore.Restorer
kbClient client.Client
restoreLogLevel logrus.Level
logger logrus.FieldLogger
metrics *metrics.ServerMetrics
logFormat logging.Format
clock clock.WithTickerAndDelayedExecution
ctx context.Context
namespace string
restorer pkgrestore.Restorer
kbClient client.Client
restoreLogLevel logrus.Level
logger logrus.FieldLogger
metrics *metrics.ServerMetrics
logFormat logging.Format
clock clock.WithTickerAndDelayedExecution
defaultItemOperationTimeout time.Duration
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
backupStoreGetter persistence.ObjectBackupStoreGetter
@@ -114,17 +116,19 @@ func NewRestoreReconciler(
backupStoreGetter persistence.ObjectBackupStoreGetter,
metrics *metrics.ServerMetrics,
logFormat logging.Format,
defaultItemOperationTimeout time.Duration,
) *restoreReconciler {
r := &restoreReconciler{
ctx: ctx,
namespace: namespace,
restorer: restorer,
kbClient: kbClient,
logger: logger,
restoreLogLevel: restoreLogLevel,
metrics: metrics,
logFormat: logFormat,
clock: &clock.RealClock{},
ctx: ctx,
namespace: namespace,
restorer: restorer,
kbClient: kbClient,
logger: logger,
restoreLogLevel: restoreLogLevel,
metrics: metrics,
logFormat: logFormat,
clock: &clock.RealClock{},
defaultItemOperationTimeout: defaultItemOperationTimeout,
// use variables to refer to these functions so they can be
// replaced with fakes for testing.
@@ -172,6 +176,10 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
restore.Status.StartTimestamp = &metav1.Time{Time: r.clock.Now()}
restore.Status.Phase = api.RestorePhaseInProgress
}
if restore.Spec.ItemOperationTimeout.Duration == 0 {
// set default item operation timeout
restore.Spec.ItemOperationTimeout.Duration = r.defaultItemOperationTimeout
}
// patch to update status and persist to API
err = kubeutil.PatchResource(original, restore, r.kbClient)
@@ -194,17 +202,14 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
restore.Status.Phase = api.RestorePhaseFailed
restore.Status.FailureReason = err.Error()
r.metrics.RegisterRestoreFailed(backupScheduleName)
} else if restore.Status.Errors > 0 {
log.Debug("Restore partially failed")
restore.Status.Phase = api.RestorePhasePartiallyFailed
r.metrics.RegisterRestorePartialFailure(backupScheduleName)
} else {
log.Debug("Restore completed")
restore.Status.Phase = api.RestorePhaseCompleted
r.metrics.RegisterRestoreSuccess(backupScheduleName)
}
restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()}
// mark completion if in terminal phase
if restore.Status.Phase == api.RestorePhaseFailed ||
restore.Status.Phase == api.RestorePhasePartiallyFailed ||
restore.Status.Phase == api.RestorePhaseCompleted {
restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()}
}
log.Debug("Updating restore's final status")
if err = kubeutil.PatchResource(original, restore, r.kbClient); err != nil {
log.WithError(errors.WithStack(err)).Info("Error updating restore's final status")
@@ -375,15 +380,19 @@ func mostRecentCompletedBackup(backups []api.Backup) api.Backup {
// fetchBackupInfo checks the backup lister for a backup that matches the given name. If it doesn't
// find it, it returns an error.
func (r *restoreReconciler) fetchBackupInfo(backupName string) (backupInfo, error) {
return fetchBackupInfoInternal(r.kbClient, r.namespace, backupName)
}
func fetchBackupInfoInternal(kbClient client.Client, namespace, backupName string) (backupInfo, error) {
backup := &api.Backup{}
err := r.kbClient.Get(context.Background(), types.NamespacedName{Namespace: r.namespace, Name: backupName}, backup)
err := kbClient.Get(context.Background(), types.NamespacedName{Namespace: namespace, Name: backupName}, backup)
if err != nil {
return backupInfo{}, err
return backupInfo{}, errors.Wrap(err, fmt.Sprintf("can't find backup %s/%s", namespace, backupName))
}
location := &api.BackupStorageLocation{}
if err := r.kbClient.Get(context.Background(), client.ObjectKey{
Namespace: r.namespace,
if err := kbClient.Get(context.Background(), client.ObjectKey{
Namespace: namespace,
Name: backup.Spec.StorageLocation,
}, location); err != nil {
return backupInfo{}, errors.WithStack(err)
@@ -469,6 +478,21 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
restoreWarnings, restoreErrors := r.restorer.RestoreWithResolvers(restoreReq, actionsResolver, snapshotItemResolver,
pluginManager)
// Iterate over restore item operations and update progress.
// Any errors on operations at this point should be added to restore errors.
// If any operations are still not complete, then restore will not be set to
// Completed yet.
inProgressOperations, _, opsCompleted, opsFailed, errs := getRestoreItemOperationProgress(restoreReq.Restore, pluginManager, *restoreReq.GetItemOperationsList())
if len(errs) > 0 {
for err := range errs {
restoreLog.Error(err)
}
}
restore.Status.RestoreItemOperationsAttempted = len(*restoreReq.GetItemOperationsList())
restore.Status.RestoreItemOperationsCompleted = opsCompleted
restore.Status.RestoreItemOperationsFailed = opsFailed
// log errors and warnings to the restore log
for _, msg := range restoreErrors.Velero {
restoreLog.Errorf("Velero restore error: %v", msg)
@@ -537,6 +561,29 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
r.logger.WithError(err).Error("Error uploading restored resource list to backup storage")
}
if err := putOperationsForRestore(restore, *restoreReq.GetItemOperationsList(), backupStore); err != nil {
r.logger.WithError(err).Error("Error uploading restore item action operation resource list to backup storage")
}
if restore.Status.Errors > 0 {
if inProgressOperations {
r.logger.Debug("Restore WaitingForPluginOperationsPartiallyFailed")
restore.Status.Phase = api.RestorePhaseWaitingForPluginOperationsPartiallyFailed
} else {
r.logger.Debug("Restore partially failed")
restore.Status.Phase = api.RestorePhasePartiallyFailed
r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName)
}
} else {
if inProgressOperations {
r.logger.Debug("Restore WaitingForPluginOperations")
restore.Status.Phase = api.RestorePhaseWaitingForPluginOperations
} else {
r.logger.Debug("Restore completed")
restore.Status.Phase = api.RestorePhaseCompleted
r.metrics.RegisterRestoreSuccess(restore.Spec.ScheduleName)
}
}
return nil
}
@@ -603,6 +650,26 @@ func putRestoredResourceList(restore *api.Restore, list map[string][]string, bac
return nil
}
func putOperationsForRestore(restore *api.Restore, operations []*itemoperation.RestoreOperation, backupStore persistence.BackupStore) error {
buf := new(bytes.Buffer)
gzw := gzip.NewWriter(buf)
defer gzw.Close()
if err := json.NewEncoder(gzw).Encode(operations); err != nil {
return errors.Wrap(err, "error encoding restore item operations list to JSON")
}
if err := gzw.Close(); err != nil {
return errors.Wrap(err, "error closing gzip writer")
}
if err := backupStore.PutRestoreItemOperations(restore.Name, buf); err != nil {
return err
}
return nil
}
func downloadToTempFile(backupName string, backupStore persistence.BackupStore, logger logrus.FieldLogger) (*os.File, error) {
readCloser, err := backupStore.GetBackupContents(backupName)
if err != nil {
+6 -1
View File
@@ -111,6 +111,7 @@ func TestFetchBackupInfo(t *testing.T) {
NewFakeSingleObjectBackupStoreGetter(backupStore),
metrics.NewServerMetrics(),
formatFlag,
60*time.Minute,
)
if test.backupStoreError == nil {
@@ -193,6 +194,7 @@ func TestProcessQueueItemSkips(t *testing.T) {
nil, // backupStoreGetter
metrics.NewServerMetrics(),
formatFlag,
60*time.Minute,
)
_, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{
@@ -422,6 +424,7 @@ func TestRestoreReconcile(t *testing.T) {
NewFakeSingleObjectBackupStoreGetter(backupStore),
metrics.NewServerMetrics(),
formatFlag,
60*time.Minute,
)
r.clock = clocktesting.NewFakeClock(now)
@@ -453,6 +456,7 @@ func TestRestoreReconcile(t *testing.T) {
backupStore.On("PutRestoreResults", test.backup.Name, test.restore.Name, mock.Anything).Return(nil)
backupStore.On("PutRestoredResourceList", test.restore.Name, mock.Anything).Return(nil)
backupStore.On("PutRestoreItemOperations", mock.Anything, mock.Anything).Return(nil)
volumeSnapshots := []*volume.Snapshot{
{
@@ -586,6 +590,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
NewFakeSingleObjectBackupStoreGetter(backupStore),
metrics.NewServerMetrics(),
formatFlag,
60*time.Minute,
)
restore := &velerov1api.Restore{
@@ -746,7 +751,7 @@ func TestMostRecentCompletedBackup(t *testing.T) {
}
func NewRestore(ns, name, backup, includeNS, includeResource string, phase velerov1api.RestorePhase) *builder.RestoreBuilder {
restore := builder.ForRestore(ns, name).Phase(phase).Backup(backup)
restore := builder.ForRestore(ns, name).Phase(phase).Backup(backup).ItemOperationTimeout(60 * time.Minute)
if includeNS != "" {
restore = restore.IncludedNamespaces(includeNS)
@@ -0,0 +1,352 @@
/*
Copyright the Velero contributors.
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 controller
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clocks "k8s.io/utils/clock"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/itemoperationmap"
"github.com/vmware-tanzu/velero/pkg/metrics"
"github.com/vmware-tanzu/velero/pkg/persistence"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
"github.com/vmware-tanzu/velero/pkg/util/kube"
)
const (
defaultRestoreOperationsFrequency = 10 * time.Second
)
type restoreOperationsReconciler struct {
client.Client
namespace string
logger logrus.FieldLogger
clock clocks.WithTickerAndDelayedExecution
frequency time.Duration
itemOperationsMap *itemoperationmap.RestoreItemOperationsMap
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
backupStoreGetter persistence.ObjectBackupStoreGetter
metrics *metrics.ServerMetrics
}
func NewRestoreOperationsReconciler(
logger logrus.FieldLogger,
namespace string,
client client.Client,
frequency time.Duration,
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager,
backupStoreGetter persistence.ObjectBackupStoreGetter,
metrics *metrics.ServerMetrics,
itemOperationsMap *itemoperationmap.RestoreItemOperationsMap,
) *restoreOperationsReconciler {
abor := &restoreOperationsReconciler{
Client: client,
logger: logger,
namespace: namespace,
clock: clocks.RealClock{},
frequency: frequency,
itemOperationsMap: itemOperationsMap,
newPluginManager: newPluginManager,
backupStoreGetter: backupStoreGetter,
metrics: metrics,
}
if abor.frequency <= 0 {
abor.frequency = defaultRestoreOperationsFrequency
}
return abor
}
func (r *restoreOperationsReconciler) SetupWithManager(mgr ctrl.Manager) error {
s := kube.NewPeriodicalEnqueueSource(r.logger, mgr.GetClient(), &velerov1api.RestoreList{}, r.frequency, kube.PeriodicalEnqueueSourceOption{})
gp := kube.NewGenericEventPredicate(func(object client.Object) bool {
restore := object.(*velerov1api.Restore)
return (restore.Status.Phase == velerov1api.RestorePhaseWaitingForPluginOperations ||
restore.Status.Phase == velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed)
})
return ctrl.NewControllerManagedBy(mgr).
For(&velerov1api.Restore{}, builder.WithPredicates(kube.FalsePredicate{})).
Watches(s, nil, builder.WithPredicates(gp)).
Complete(r)
}
// +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=get;list;watch;update
// +kubebuilder:rbac:groups=velero.io,resources=restores/status,verbs=get
// +kubebuilder:rbac:groups=velero.io,resources=restorestoragelocations,verbs=get
func (r *restoreOperationsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.logger.WithField("restore operations for restore", req.String())
log.Debug("restoreOperationsReconciler getting restore")
original := &velerov1api.Restore{}
if err := r.Get(ctx, req.NamespacedName, original); err != nil {
if apierrors.IsNotFound(err) {
log.WithError(err).Error("restore not found")
return ctrl.Result{}, nil
}
return ctrl.Result{}, errors.Wrapf(err, "error getting restore %s", req.String())
}
restore := original.DeepCopy()
log.Debugf("restore: %s", restore.Name)
log = r.logger.WithFields(
logrus.Fields{
"restore": req.String(),
},
)
switch restore.Status.Phase {
case velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed:
// only process restores waiting for plugin operations to complete
default:
log.Debug("Restore has no ongoing plugin operations, skipping")
return ctrl.Result{}, nil
}
info, err := r.fetchBackupInfo(restore.Spec.BackupName)
if err != nil {
log.Warnf("Cannot check progress on Restore operations because backup info is unavailable %s; marking restore PartiallyFailed", err.Error())
restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed
err2 := r.updateRestoreAndOperationsJSON(ctx, original, restore, nil, &itemoperationmap.OperationsForRestore{ErrsSinceUpdate: []string{err.Error()}}, false, false)
if err2 != nil {
log.WithError(err2).Error("error updating Restore")
}
return ctrl.Result{}, errors.Wrap(err, "error getting backup info")
}
if info.location.Spec.AccessMode == velerov1api.BackupStorageLocationAccessModeReadOnly {
log.Infof("Cannot check progress on Restore operations because backup storage location %s is currently in read-only mode; marking restore PartiallyFailed", info.location.Name)
restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed
err := r.updateRestoreAndOperationsJSON(ctx, original, restore, nil, &itemoperationmap.OperationsForRestore{ErrsSinceUpdate: []string{"BSL is read-only"}}, false, false)
if err != nil {
log.WithError(err).Error("error updating Restore")
}
return ctrl.Result{}, nil
}
pluginManager := r.newPluginManager(r.logger)
defer pluginManager.CleanupClients()
backupStore, err := r.backupStoreGetter.Get(info.location, pluginManager, r.logger)
if err != nil {
return ctrl.Result{}, errors.Wrap(err, "error getting backup store")
}
operations, err := r.itemOperationsMap.GetOperationsForRestore(backupStore, restore.Name)
if err != nil {
err2 := r.updateRestoreAndOperationsJSON(ctx, original, restore, backupStore, &itemoperationmap.OperationsForRestore{ErrsSinceUpdate: []string{err.Error()}}, false, false)
if err2 != nil {
return ctrl.Result{}, errors.Wrap(err2, "error updating Restore")
}
return ctrl.Result{}, errors.Wrap(err, "error getting restore operations")
}
stillInProgress, changes, opsCompleted, opsFailed, errs := getRestoreItemOperationProgress(restore, pluginManager, operations.Operations)
// if len(errs)>0, need to update restore errors and error log
operations.ErrsSinceUpdate = append(operations.ErrsSinceUpdate, errs...)
restore.Status.Errors += len(operations.ErrsSinceUpdate)
completionChanges := false
if restore.Status.RestoreItemOperationsCompleted != opsCompleted || restore.Status.RestoreItemOperationsFailed != opsFailed {
completionChanges = true
restore.Status.RestoreItemOperationsCompleted = opsCompleted
restore.Status.RestoreItemOperationsFailed = opsFailed
}
if changes {
operations.ChangesSinceUpdate = true
}
// if stillInProgress is false, restore moves to terminal phase and needs update
// if operations.ErrsSinceUpdate is not empty, then restore phase needs to change to
// RestorePhaseWaitingForPluginOperationsPartiallyFailed and needs update
// If the only changes are incremental progress, then no write is necessary, progress can remain in memory
if !stillInProgress {
if len(operations.ErrsSinceUpdate) > 0 {
restore.Status.Phase = velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed
}
if restore.Status.Phase == velerov1api.RestorePhaseWaitingForPluginOperations {
log.Infof("Marking restore %s completed", restore.Name)
restore.Status.Phase = velerov1api.RestorePhaseCompleted
r.metrics.RegisterRestoreSuccess(restore.Spec.ScheduleName)
} else {
log.Infof("Marking restore %s FinalizingPartiallyFailed", restore.Name)
restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed
r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName)
}
}
err = r.updateRestoreAndOperationsJSON(ctx, original, restore, backupStore, operations, changes, completionChanges)
if err != nil {
return ctrl.Result{}, errors.Wrap(err, "error updating Restore")
}
return ctrl.Result{}, nil
}
// fetchBackupInfo checks the backup lister for a backup that matches the given name. If it doesn't
// find it, it returns an error.
func (r *restoreOperationsReconciler) fetchBackupInfo(backupName string) (backupInfo, error) {
return fetchBackupInfoInternal(r.Client, r.namespace, backupName)
}
func (r *restoreOperationsReconciler) updateRestoreAndOperationsJSON(
ctx context.Context,
original, restore *velerov1api.Restore,
backupStore persistence.BackupStore,
operations *itemoperationmap.OperationsForRestore,
changes bool,
completionChanges bool) error {
if len(operations.ErrsSinceUpdate) > 0 {
// FIXME: download/upload results
}
removeIfComplete := true
defer func() {
// remove local operations list if complete
if removeIfComplete && (restore.Status.Phase == velerov1api.RestorePhaseCompleted ||
restore.Status.Phase == velerov1api.RestorePhasePartiallyFailed) {
r.itemOperationsMap.DeleteOperationsForRestore(restore.Name)
} else if changes {
r.itemOperationsMap.PutOperationsForRestore(operations, restore.Name)
}
}()
// update restore and upload progress if errs or complete
if len(operations.ErrsSinceUpdate) > 0 ||
restore.Status.Phase == velerov1api.RestorePhaseCompleted ||
restore.Status.Phase == velerov1api.RestorePhasePartiallyFailed {
// update file store
if backupStore != nil {
if err := r.itemOperationsMap.UploadProgressAndPutOperationsForRestore(backupStore, operations, restore.Name); err != nil {
removeIfComplete = false
return err
}
}
// update restore
err := r.Client.Patch(ctx, restore, client.MergeFrom(original))
if err != nil {
removeIfComplete = false
return errors.Wrapf(err, "error updating Restore %s", restore.Name)
}
} else if completionChanges {
// If restore is still incomplete and no new errors are found but there are some new operations
// completed, patch restore to reflect new completion numbers, but don't upload detailed json file
err := r.Client.Patch(ctx, restore, client.MergeFrom(original))
if err != nil {
return errors.Wrapf(err, "error updating Restore %s", restore.Name)
}
}
return nil
}
func getRestoreItemOperationProgress(
restore *velerov1api.Restore,
pluginManager clientmgmt.Manager,
operationsList []*itemoperation.RestoreOperation) (bool, bool, int, int, []string) {
inProgressOperations := false
changes := false
var errs []string
var completedCount, failedCount int
for _, operation := range operationsList {
if operation.Status.Phase == itemoperation.OperationPhaseInProgress {
ria, err := pluginManager.GetRestoreItemActionV2(operation.Spec.RestoreItemAction)
if err != nil {
operation.Status.Phase = itemoperation.OperationPhaseFailed
operation.Status.Error = err.Error()
errs = append(errs, err.Error())
changes = true
failedCount++
continue
}
operationProgress, err := ria.Progress(operation.Spec.OperationID, restore)
if err != nil {
operation.Status.Phase = itemoperation.OperationPhaseFailed
operation.Status.Error = err.Error()
errs = append(errs, err.Error())
changes = true
failedCount++
continue
}
if operation.Status.NCompleted != operationProgress.NCompleted {
operation.Status.NCompleted = operationProgress.NCompleted
changes = true
}
if operation.Status.NTotal != operationProgress.NTotal {
operation.Status.NTotal = operationProgress.NTotal
changes = true
}
if operation.Status.OperationUnits != operationProgress.OperationUnits {
operation.Status.OperationUnits = operationProgress.OperationUnits
changes = true
}
if operation.Status.Description != operationProgress.Description {
operation.Status.Description = operationProgress.Description
changes = true
}
started := metav1.NewTime(operationProgress.Started)
if operation.Status.Started == nil || *(operation.Status.Started) != started {
operation.Status.Started = &started
changes = true
}
updated := metav1.NewTime(operationProgress.Updated)
if operation.Status.Updated == nil || *(operation.Status.Updated) != updated {
operation.Status.Updated = &updated
changes = true
}
if operationProgress.Completed {
if operationProgress.Err != "" {
operation.Status.Phase = itemoperation.OperationPhaseFailed
operation.Status.Error = operationProgress.Err
errs = append(errs, operationProgress.Err)
changes = true
failedCount++
continue
}
operation.Status.Phase = itemoperation.OperationPhaseCompleted
changes = true
completedCount++
continue
}
// cancel operation if past timeout period
if operation.Status.Created.Time.Add(restore.Spec.ItemOperationTimeout.Duration).Before(time.Now()) {
_ = ria.Cancel(operation.Spec.OperationID, restore)
operation.Status.Phase = itemoperation.OperationPhaseFailed
operation.Status.Error = "Asynchronous action timed out"
errs = append(errs, operation.Status.Error)
changes = true
failedCount++
continue
}
// if we reach this point, the operation is still running
inProgressOperations = true
} else if operation.Status.Phase == itemoperation.OperationPhaseCompleted {
completedCount++
} else if operation.Status.Phase == itemoperation.OperationPhaseFailed {
failedCount++
}
}
return inProgressOperations, changes, completedCount, failedCount, errs
}
@@ -0,0 +1,320 @@
/*
Copyright the Velero contributors.
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 controller
import (
"context"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
testclocks "k8s.io/utils/clock/testing"
ctrl "sigs.k8s.io/controller-runtime"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/itemoperationmap"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/metrics"
persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
riav2mocks "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks/restoreitemaction/v2"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
var (
restorePluginManager = &pluginmocks.Manager{}
restoreBackupStore = &persistencemocks.BackupStore{}
ria = &riav2mocks.RestoreItemAction{}
)
func mockRestoreOperationsReconciler(fakeClient kbclient.Client, fakeClock *testclocks.FakeClock, freq time.Duration) *restoreOperationsReconciler {
abor := NewRestoreOperationsReconciler(
logrus.StandardLogger(),
velerov1api.DefaultNamespace,
fakeClient,
freq,
func(logrus.FieldLogger) clientmgmt.Manager { return restorePluginManager },
NewFakeSingleObjectBackupStoreGetter(restoreBackupStore),
metrics.NewServerMetrics(),
itemoperationmap.NewRestoreItemOperationsMap(),
)
abor.clock = fakeClock
return abor
}
func TestRestoreOperationsReconcile(t *testing.T) {
fakeClock := testclocks.NewFakeClock(time.Now())
metav1Now := metav1.NewTime(fakeClock.Now())
defaultBackupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Result()
tests := []struct {
name string
restore *velerov1api.Restore
restoreOperations []*itemoperation.RestoreOperation
backup *velerov1api.Backup
backupLocation *velerov1api.BackupStorageLocation
operationComplete bool
operationErr string
expectError bool
expectPhase velerov1api.RestorePhase
}{
{
name: "WaitingForPluginOperations restore with completed operations is Completed",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-11").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-11")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperations).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: true,
expectPhase: velerov1api.RestorePhaseCompleted,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-11",
RestoreUID: "foo-11",
RestoreItemAction: "foo-11",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-11",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
{
name: "WaitingForPluginOperations restore with incomplete operations is still incomplete",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-12").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-12")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperations).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: false,
expectPhase: velerov1api.RestorePhaseWaitingForPluginOperations,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-12",
RestoreUID: "foo-12",
RestoreItemAction: "foo-12",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-12",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
{
name: "WaitingForPluginOperations restore with completed failed operations is PartiallyFailed",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-13").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-13")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperations).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: true,
operationErr: "failed",
expectPhase: velerov1api.RestorePhasePartiallyFailed,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-13",
RestoreUID: "foo-13",
RestoreItemAction: "foo-13",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-13",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
{
name: "WaitingForPluginOperationsPartiallyFailed restore with completed operations is PartiallyFailed",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-14").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-14")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: true,
expectPhase: velerov1api.RestorePhasePartiallyFailed,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-14",
RestoreUID: "foo-14",
RestoreItemAction: "foo-14",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-14",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
{
name: "WaitingForPluginOperationsPartiallyFailed restore with incomplete operations is still incomplete",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-15").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-15")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: false,
expectPhase: velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-15",
RestoreUID: "foo-15",
RestoreItemAction: "foo-15",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-15",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
{
name: "WaitingForPluginOperationsPartiallyFailed restore with completed failed operations is PartiallyFailed",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-16").
Backup("backup-1").
ItemOperationTimeout(60 * time.Minute).
ObjectMeta(builder.WithUID("foo-16")).
Phase(velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
backupLocation: defaultBackupLocation,
operationComplete: true,
operationErr: "failed",
expectPhase: velerov1api.RestorePhasePartiallyFailed,
restoreOperations: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-16",
RestoreUID: "foo-16",
RestoreItemAction: "foo-16",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1",
},
OperationID: "operation-16",
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &metav1Now,
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.restore == nil {
return
}
initObjs := []runtime.Object{}
initObjs = append(initObjs, test.restore)
initObjs = append(initObjs, test.backup)
if test.backupLocation != nil {
initObjs = append(initObjs, test.backupLocation)
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, initObjs...)
reconciler := mockRestoreOperationsReconciler(fakeClient, fakeClock, defaultRestoreOperationsFrequency)
restorePluginManager.On("CleanupClients").Return(nil)
restoreBackupStore.On("GetRestoreItemOperations", test.restore.Name).Return(test.restoreOperations, nil)
restoreBackupStore.On("PutRestoreItemOperations", mock.Anything, mock.Anything).Return(nil)
restoreBackupStore.On("PutRestoreMetadata", mock.Anything, mock.Anything).Return(nil)
for _, operation := range test.restoreOperations {
ria.On("Progress", operation.Spec.OperationID, mock.Anything).
Return(velero.OperationProgress{
Completed: test.operationComplete,
Err: test.operationErr,
}, nil)
restorePluginManager.On("GetRestoreItemActionV2", operation.Spec.RestoreItemAction).Return(ria, nil)
}
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.restore.Namespace, Name: test.restore.Name}})
gotErr := err != nil
assert.Equal(t, test.expectError, gotErr)
restoreAfter := velerov1api.Restore{}
err = fakeClient.Get(context.TODO(), types.NamespacedName{
Namespace: test.restore.Namespace,
Name: test.restore.Name,
}, &restoreAfter)
require.NoError(t, err)
assert.Equal(t, test.expectPhase, restoreAfter.Status.Phase)
})
}
}
+29
View File
@@ -28,6 +28,21 @@ type RestoreOperation struct {
Status OperationStatus `json:"status"`
}
func (in *RestoreOperation) DeepCopy() *RestoreOperation {
if in == nil {
return nil
}
out := new(RestoreOperation)
in.DeepCopyInto(out)
return out
}
func (in *RestoreOperation) DeepCopyInto(out *RestoreOperation) {
*out = *in
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
type RestoreOperationSpec struct {
// RestoreName is the name of the Velero restore this item operation
// is associated with.
@@ -46,3 +61,17 @@ type RestoreOperationSpec struct {
// OperationID returned by the RIA plugin
OperationID string "json:operationID"
}
func (in *RestoreOperationSpec) DeepCopy() *RestoreOperationSpec {
if in == nil {
return nil
}
out := new(RestoreOperationSpec)
in.DeepCopyInto(out)
return out
}
func (in *RestoreOperationSpec) DeepCopyInto(out *RestoreOperationSpec) {
*out = *in
in.ResourceIdentifier.DeepCopyInto(&out.ResourceIdentifier)
}
@@ -0,0 +1,170 @@
/*
Copyright the Velero contributors.
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 itemoperationmap
import (
"bytes"
"sync"
"github.com/pkg/errors"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/persistence"
"github.com/vmware-tanzu/velero/pkg/util/encode"
)
type RestoreItemOperationsMap struct {
opsMap map[string]*OperationsForRestore
opsLock sync.Mutex
}
// Returns a pointer to a new RestoreItemOperationsMap
func NewRestoreItemOperationsMap() *RestoreItemOperationsMap {
return &RestoreItemOperationsMap{opsMap: make(map[string]*OperationsForRestore)}
}
// returns a deep copy so we can minimize the time the map is locked
func (m *RestoreItemOperationsMap) GetOperationsForRestore(
backupStore persistence.BackupStore,
restoreName string) (*OperationsForRestore, error) {
var err error
// lock operations map
m.opsLock.Lock()
defer m.opsLock.Unlock()
operations, ok := m.opsMap[restoreName]
if !ok || len(operations.Operations) == 0 {
operations = &OperationsForRestore{}
operations.Operations, err = backupStore.GetRestoreItemOperations(restoreName)
if err == nil {
m.opsMap[restoreName] = operations
}
}
return operations.DeepCopy(), err
}
func (m *RestoreItemOperationsMap) PutOperationsForRestore(
operations *OperationsForRestore,
restoreName string) {
// lock operations map
m.opsLock.Lock()
defer m.opsLock.Unlock()
if operations != nil {
m.opsMap[restoreName] = operations
}
}
func (m *RestoreItemOperationsMap) DeleteOperationsForRestore(restoreName string) {
// lock operations map
m.opsLock.Lock()
defer m.opsLock.Unlock()
if _, ok := m.opsMap[restoreName]; ok {
delete(m.opsMap, restoreName)
}
return
}
// UploadProgressAndPutOperationsForRestore will upload the item operations for this restore to
// the object store and update the map for this restore with the modified operations
func (m *RestoreItemOperationsMap) UploadProgressAndPutOperationsForRestore(
backupStore persistence.BackupStore,
operations *OperationsForRestore,
restoreName string) error {
m.opsLock.Lock()
defer m.opsLock.Unlock()
if operations == nil {
return errors.New("nil operations passed in")
}
if err := operations.uploadProgress(backupStore, restoreName); err != nil {
return err
}
m.opsMap[restoreName] = operations
return nil
}
// UpdateForRestore will upload the item operations for this restore to
// the object store, if it has changes not yet uploaded
func (m *RestoreItemOperationsMap) UpdateForRestore(backupStore persistence.BackupStore, restoreName string) error {
// lock operations map
m.opsLock.Lock()
defer m.opsLock.Unlock()
operations, ok := m.opsMap[restoreName]
// if operations for this restore aren't found, or if there are no changes
// or errors since last update, do nothing
if !ok || (!operations.ChangesSinceUpdate && len(operations.ErrsSinceUpdate) == 0) {
return nil
}
if err := operations.uploadProgress(backupStore, restoreName); err != nil {
return err
}
return nil
}
type OperationsForRestore struct {
Operations []*itemoperation.RestoreOperation
ChangesSinceUpdate bool
ErrsSinceUpdate []string
}
func (in *OperationsForRestore) DeepCopy() *OperationsForRestore {
if in == nil {
return nil
}
out := new(OperationsForRestore)
in.DeepCopyInto(out)
return out
}
func (in *OperationsForRestore) DeepCopyInto(out *OperationsForRestore) {
*out = *in
if in.Operations != nil {
in, out := &in.Operations, &out.Operations
*out = make([]*itemoperation.RestoreOperation, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(itemoperation.RestoreOperation)
(*in).DeepCopyInto(*out)
}
}
}
if in.ErrsSinceUpdate != nil {
in, out := &in.ErrsSinceUpdate, &out.ErrsSinceUpdate
*out = make([]string, len(*in))
copy(*out, *in)
}
}
func (o *OperationsForRestore) uploadProgress(backupStore persistence.BackupStore, restoreName string) error {
if len(o.Operations) > 0 {
var restoreItemOperations *bytes.Buffer
restoreItemOperations, errs := encode.EncodeToJSONGzip(o.Operations, "restore item operations list")
if errs != nil {
return errors.Wrap(errs[0], "error encoding item operations json")
}
err := backupStore.PutRestoreItemOperations(restoreName, restoreItemOperations)
if err != nil {
return errors.Wrap(err, "error uploading item operations json")
}
}
o.ChangesSinceUpdate = false
o.ErrsSinceUpdate = nil
return nil
}
+19 -18
View File
@@ -407,13 +407,13 @@ func (_m *BackupStore) PutBackupMetadata(backup string, backupMetadata io.Reader
return r0
}
// PutRestoreItemOperations provides a mock function with given fields: backup, restore, restoreItemOperations
func (_m *BackupStore) PutRestoreItemOperations(backup string, restore string, restoreItemOperations io.Reader) error {
ret := _m.Called(backup, restore, restoreItemOperations)
// PutRestoreItemOperations provides a mock function with given fields: restore, restoreItemOperations
func (_m *BackupStore) PutRestoreItemOperations(restore string, restoreItemOperations io.Reader) error {
ret := _m.Called(restore, restoreItemOperations)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, io.Reader) error); ok {
r0 = rf(backup, restore, restoreItemOperations)
if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok {
r0 = rf(restore, restoreItemOperations)
} else {
r0 = ret.Error(0)
}
@@ -449,6 +449,20 @@ func (_m *BackupStore) PutRestoreResults(backup string, restore string, results
return r0
}
// PutRestoredResourceList provides a mock function with given fields: restore, results
func (_m *BackupStore) PutRestoredResourceList(restore string, results io.Reader) error {
ret := _m.Called(restore, results)
var r0 error
if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok {
r0 = rf(restore, results)
} else {
r0 = ret.Error(0)
}
return r0
}
type mockConstructorTestingTNewBackupStore interface {
mock.TestingT
Cleanup(func())
@@ -463,16 +477,3 @@ func NewBackupStore(t mockConstructorTestingTNewBackupStore) *BackupStore {
return mock
}
func (_m *BackupStore) PutRestoredResourceList(restore string, results io.Reader) error {
ret := _m.Called(restore, results)
var r0 error
if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok {
r0 = rf(restore, results)
} else {
r0 = ret.Error(0)
}
return r0
}
+2 -2
View File
@@ -80,7 +80,7 @@ type BackupStore interface {
PutRestoreLog(backup, restore string, log io.Reader) error
PutRestoreResults(backup, restore string, results io.Reader) error
PutRestoredResourceList(restore string, results io.Reader) error
PutRestoreItemOperations(backup, restore string, restoreItemOperations io.Reader) error
PutRestoreItemOperations(restore string, restoreItemOperations io.Reader) error
GetRestoreItemOperations(name string) ([]*itemoperation.RestoreOperation, error)
DeleteRestore(name string) error
@@ -547,7 +547,7 @@ func (s *objectBackupStore) PutRestoredResourceList(restore string, list io.Read
return s.objectStore.PutObject(s.bucket, s.layout.getRestoreResourceListKey(restore), list)
}
func (s *objectBackupStore) PutRestoreItemOperations(backup string, restore string, restoreItemOperations io.Reader) error {
func (s *objectBackupStore) PutRestoreItemOperations(restore string, restoreItemOperations io.Reader) error {
return seekAndPutObject(s.objectStore, s.bucket, s.layout.getRestoreItemOperationsKey(restore), restoreItemOperations)
}
@@ -95,6 +95,11 @@ func (r *RestartableRestoreItemAction) getDelegate() (riav2.RestoreItemAction, e
return r.getRestoreItemAction()
}
// Name returns the plugin's name.
func (r *RestartableRestoreItemAction) Name() string {
return r.Key.Name
}
// AppliesTo restarts the plugin's process if needed, then delegates the call.
func (r RestartableRestoreItemAction) AppliesTo() (velero.ResourceSelector, error) {
delegate, err := r.getDelegate()
@@ -157,6 +162,11 @@ func NewAdaptedV1RestartableRestoreItemAction(v1Restartable *riav1cli.Restartabl
return r
}
// Name restarts the plugin's name.
func (r *AdaptedV1RestartableRestoreItemAction) Name() string {
return r.V1Restartable.Key.Name
}
// AppliesTo delegates to the v1 AppliesTo call.
func (r *AdaptedV1RestartableRestoreItemAction) AppliesTo() (velero.ResourceSelector, error) {
return r.V1Restartable.AppliesTo()
@@ -199,3 +199,9 @@ func (c *RestoreItemActionGRPCClient) AreAdditionalItemsReady(additionalItems []
return res.Ready, nil
}
// This shouldn't be called on the GRPC client since the RestartableRestoreItemAction won't delegate
// this method
func (c *RestoreItemActionGRPCClient) Name() string {
return ""
}
@@ -263,3 +263,9 @@ func restoreResourceIdentifierToProto(id velero.ResourceIdentifier) *proto.Resou
Name: id.Name,
}
}
// This shouldn't be called on the GRPC server since the server won't ever receive this request, as
// the RestartableRestoreItemAction in Velero won't delegate this to the server
func (c *RestoreItemActionGRPCServer) Name() string {
return ""
}
@@ -29,7 +29,7 @@ import (
// BackupItemAction is an actor that performs an operation on an individual item being backed up.
type BackupItemAction interface {
// Name returns the name of this BIA. Plugins which implement this interface must defined Name,
// Name returns the name of this BIA. Plugins which implement this interface must define Name,
// but its content is unimportant, as it won't actually be called via RPC. Velero's plugin infrastructure
// will implement this directly rather than delegating to the RPC plugin in order to return the name
// that the plugin was registered under. The plugins must implement the method to complete the interface.
@@ -13,7 +13,7 @@ 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Code generated by mockery v2.16.0. DO NOT EDIT.
package v2
@@ -51,19 +51,19 @@ func (_m *RestoreItemAction) AppliesTo() (velero.ResourceSelector, error) {
}
// AreAdditionalItemsReady provides a mock function with given fields: AdditionalItems, restore
func (_m *RestoreItemAction) AreAdditionalItemsReady(AdditionalItems []velero.ResourceIdentifier, restore *v1.Restore) (bool, error) {
ret := _m.Called(AdditionalItems, restore)
func (_m *RestoreItemAction) AreAdditionalItemsReady(additionalItems []velero.ResourceIdentifier, restore *v1.Restore) (bool, error) {
ret := _m.Called(additionalItems, restore)
var r0 bool
if rf, ok := ret.Get(0).(func([]velero.ResourceIdentifier, *v1.Restore) bool); ok {
r0 = rf(AdditionalItems, restore)
r0 = rf(additionalItems, restore)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func([]velero.ResourceIdentifier, *v1.Restore) error); ok {
r1 = rf(AdditionalItems, restore)
r1 = rf(additionalItems, restore)
} else {
r1 = ret.Error(1)
}
@@ -108,6 +108,20 @@ func (_m *RestoreItemAction) Execute(input *velero.RestoreItemActionExecuteInput
return r0, r1
}
// Name provides a mock function with given fields:
func (_m *RestoreItemAction) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Progress provides a mock function with given fields: operationID, restore
func (_m *RestoreItemAction) Progress(operationID string, restore *v1.Restore) (velero.OperationProgress, error) {
ret := _m.Called(operationID, restore)
@@ -128,3 +142,18 @@ func (_m *RestoreItemAction) Progress(operationID string, restore *v1.Restore) (
return r0, r1
}
type mockConstructorTestingTNewRestoreItemAction interface {
mock.TestingT
Cleanup(func())
}
// NewRestoreItemAction creates a new instance of RestoreItemAction. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewRestoreItemAction(t mockConstructorTestingTNewRestoreItemAction) *RestoreItemAction {
mock := &RestoreItemAction{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -27,6 +27,12 @@ import (
// RestoreItemAction is an actor that performs an operation on an individual item being restored.
type RestoreItemAction interface {
// Name returns the name of this RIA. Plugins which implement this interface must define Name,
// but its content is unimportant, as it won't actually be called via RPC. Velero's plugin infrastructure
// will implement this directly rather than delegating to the RPC plugin in order to return the name
// that the plugin was registered under. The plugins must implement the method to complete the interface.
Name() string
// AppliesTo returns information about which resources this action should be invoked for.
// A RestoreItemAction's Execute function will only be invoked on items that match the returned
// selector. A zero-valued ResourceSelector matches all resources.
@@ -58,7 +64,7 @@ type RestoreItemAction interface {
// slice of AdditionalItems (previously returned by Execute())
// are ready. Returns true if all items are ready, and false
// otherwise. The second return value is to report errors
AreAdditionalItemsReady(AdditionalItems []velero.ResourceIdentifier, restore *api.Restore) (bool, error)
AreAdditionalItemsReady(additionalItems []velero.ResourceIdentifier, restore *api.Restore) (bool, error)
}
func AsyncOperationsNotSupportedError() error {
+17 -6
View File
@@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/volume"
)
@@ -49,12 +50,13 @@ func resourceKey(obj runtime.Object) string {
type Request struct {
*velerov1api.Restore
Log logrus.FieldLogger
Backup *velerov1api.Backup
PodVolumeBackups []*velerov1api.PodVolumeBackup
VolumeSnapshots []*volume.Snapshot
BackupReader io.Reader
RestoredItems map[itemKey]restoredItemStatus
Log logrus.FieldLogger
Backup *velerov1api.Backup
PodVolumeBackups []*velerov1api.PodVolumeBackup
VolumeSnapshots []*volume.Snapshot
BackupReader io.Reader
RestoredItems map[itemKey]restoredItemStatus
itemOperationsList *[]*itemoperation.RestoreOperation
}
type restoredItemStatus struct {
@@ -62,6 +64,15 @@ type restoredItemStatus struct {
itemExists bool
}
// GetItemOperationsList returns ItemOperationsList, initializing it if necessary
func (r *Request) GetItemOperationsList() *[]*itemoperation.RestoreOperation {
if r.itemOperationsList == nil {
list := []*itemoperation.RestoreOperation{}
r.itemOperationsList = &list
}
return r.itemOperationsList
}
// RestoredResourceList returns the list of restored resources grouped by the API
// Version and Kind
func (r *Request) RestoredResourceList() map[string][]string {
+27
View File
@@ -53,6 +53,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/discovery"
"github.com/vmware-tanzu/velero/pkg/features"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
@@ -312,6 +313,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
hooksContext: hooksCtx,
hooksCancelFunc: hooksCancelFunc,
kbClient: kr.kbClient,
itemOperationsList: req.GetItemOperationsList(),
}
return restoreCtx.execute()
@@ -357,6 +359,7 @@ type restoreContext struct {
hooksContext go_context.Context
hooksCancelFunc go_context.CancelFunc
kbClient crclient.Client
itemOperationsList *[]*itemoperation.RestoreOperation
}
type resourceClientKey struct {
@@ -1211,6 +1214,30 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
return warnings, errs, itemExists
}
// If async plugin started async operation, add it to the ItemOperations list
if executeOutput.OperationID != "" {
resourceIdentifier := velero.ResourceIdentifier{
GroupResource: groupResource,
Namespace: namespace,
Name: name,
}
now := metav1.Now()
newOperation := itemoperation.RestoreOperation{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: ctx.restore.Name,
RestoreUID: string(ctx.restore.UID),
RestoreItemAction: action.RestoreItemAction.Name(),
ResourceIdentifier: resourceIdentifier,
OperationID: executeOutput.OperationID,
},
Status: itemoperation.OperationStatus{
Phase: itemoperation.OperationPhaseInProgress,
Created: &now,
},
}
itemOperList := ctx.itemOperationsList
*itemOperList = append(*itemOperList, &newOperation)
}
if executeOutput.SkipRestore {
ctx.log.Infof("Skipping restore of %s: %v because a registered plugin discarded it", obj.GroupVersionKind().Kind, name)
return warnings, errs, itemExists
+180 -2
View File
@@ -45,6 +45,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/discovery"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
riav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v2"
@@ -1212,6 +1213,10 @@ type recordResourcesAction struct {
additionalItemsReadyTimeout time.Duration
}
func (a *recordResourcesAction) Name() string {
return ""
}
func (a *recordResourcesAction) AppliesTo() (velero.ResourceSelector, error) {
return a.selector, nil
}
@@ -1438,8 +1443,9 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) {
// pluggableAction is a restore item action that can be plugged with an Execute
// function body at runtime.
type pluggableAction struct {
selector velero.ResourceSelector
executeFunc func(*velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error)
selector velero.ResourceSelector
executeFunc func(*velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error)
progressFunc func(string, *velerov1api.Restore) (velero.OperationProgress, error)
}
func (a *pluggableAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
@@ -1452,6 +1458,10 @@ func (a *pluggableAction) Execute(input *velero.RestoreItemActionExecuteInput) (
return a.executeFunc(input)
}
func (a *pluggableAction) Name() string {
return ""
}
func (a *pluggableAction) AppliesTo() (velero.ResourceSelector, error) {
return a.selector, nil
}
@@ -1605,6 +1615,174 @@ func TestRestoreActionModifications(t *testing.T) {
}
}
// TestRestoreWithAsyncOperations runs restores which return operationIDs and
// verifies that the itemoperations are tracked as appropriate. Verification is done by
// looking at the restore request's itemOperationsList field.
func TestRestoreWithAsyncOperations(t *testing.T) {
// completedOperationAction is a *pluggableAction, whose Execute(...)
// method returns an operationID which will always be done when calling Progress.
completedOperationAction := &pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
obj, ok := input.Item.(*unstructured.Unstructured)
if !ok {
return nil, errors.Errorf("unexpected type %T", input.Item)
}
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: obj,
OperationID: obj.GetName() + "-1",
}, nil
},
progressFunc: func(operationID string, restore *velerov1api.Restore) (velero.OperationProgress, error) {
return velero.OperationProgress{
Completed: true,
Description: "Done!",
}, nil
},
}
// incompleteOperationAction is a *pluggableAction, whose Execute(...)
// method returns an operationID which will never be done when calling Progress.
incompleteOperationAction := &pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
obj, ok := input.Item.(*unstructured.Unstructured)
if !ok {
return nil, errors.Errorf("unexpected type %T", input.Item)
}
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: obj,
OperationID: obj.GetName() + "-1",
}, nil
},
progressFunc: func(operationID string, restore *velerov1api.Restore) (velero.OperationProgress, error) {
return velero.OperationProgress{
Completed: false,
Description: "Working...",
}, nil
},
}
// noOperationAction is a *pluggableAction, whose Execute(...)
// method does not return an operationID.
noOperationAction := &pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
obj, ok := input.Item.(*unstructured.Unstructured)
if !ok {
return nil, errors.Errorf("unexpected type %T", input.Item)
}
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: obj,
}, nil
},
}
tests := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
apiResources []*test.APIResource
tarball io.Reader
actions []riav2.RestoreItemAction
want []*itemoperation.RestoreOperation
}{
{
name: "action that starts a short-running process records operation",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
apiResources: []*test.APIResource{test.Pods()},
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).Done(),
actions: []riav2.RestoreItemAction{
completedOperationAction,
},
want: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-1",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-1"},
OperationID: "pod-1-1",
},
Status: itemoperation.OperationStatus{
Phase: "InProgress",
},
},
},
},
{
name: "action that starts a long-running process records operation",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
apiResources: []*test.APIResource{test.Pods()},
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-2").Result()).Done(),
actions: []riav2.RestoreItemAction{
incompleteOperationAction,
},
want: []*itemoperation.RestoreOperation{
{
Spec: itemoperation.RestoreOperationSpec{
RestoreName: "restore-1",
ResourceIdentifier: velero.ResourceIdentifier{
GroupResource: kuberesource.Pods,
Namespace: "ns-1",
Name: "pod-2"},
OperationID: "pod-2-1",
},
Status: itemoperation.OperationStatus{
Phase: "InProgress",
},
},
},
},
{
name: "action that has no operation doesn't record one",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
apiResources: []*test.APIResource{test.Pods()},
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-3").Result()).Done(),
actions: []riav2.RestoreItemAction{
noOperationAction,
},
want: []*itemoperation.RestoreOperation{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
for _, r := range tc.apiResources {
h.AddItems(t, r)
}
data := &Request{
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
PodVolumeBackups: nil,
VolumeSnapshots: nil,
BackupReader: tc.tarball,
}
warnings, errs := h.restorer.Restore(
data,
tc.actions,
nil, // volume snapshotter getter
)
assertEmptyResults(t, warnings, errs)
resultOper := *data.GetItemOperationsList()
// set want Created times so it won't fail the assert.Equal test
for i, wantOper := range tc.want {
wantOper.Status.Created = resultOper[i].Status.Created
}
assert.Equal(t, tc.want, *data.GetItemOperationsList())
})
}
}
// TestRestoreActionAdditionalItems runs restores with restore item actions that return additional items
// to be restored, and verifies that that the correct set of items is created in the API. Verification is
// done by looking at the namespaces/names of the items in the API; contents are not checked.
@@ -35,6 +35,10 @@ spec:
# to restore from. If specified, and BackupName is empty, Velero will
# restore from the most recent successful backup created from this schedule.
scheduleName: my-scheduled-backup-name
# ItemOperationTimeout specifies the time used to wait for
# asynchronous BackupItemAction operations
# The default value is 1 hour.
itemOperationTimeout: 1h
# Array of namespaces to include in the restore. If unspecified, all namespaces are included.
# Optional.
includedNamespaces:
@@ -185,6 +189,12 @@ status:
phase: ""
# An array of any validation errors encountered.
validationErrors: null
# Number of attempted RestoreItemAction operations for this restore.
restoreItemOperationsAttempted: 2
# Number of RestoreItemAction operations that Velero successfully completed for this restore.
restoreItemOperationsCompleted: 1
# Number of RestoreItemAction operations that ended in failure for this restore.
restoreItemOperationsFailed: 0
# Number of warnings that were logged by the restore.
warnings: 2
# Errors is a count of all error messages that were generated