From 68a5594dcd9b1926ac8a73d6e4eecbfa81ae6184 Mon Sep 17 00:00:00 2001 From: Dave Smith-Uchida Date: Tue, 30 Nov 2021 23:55:22 -0800 Subject: [PATCH 1/3] Adding basic item snapshotter support Added upload-progress-check-interval flag to control how often snapshots are checked for upload progress - defaults to 1 minute Added metrics for Item Snapshots in a backup item_snapshot_attempt_total - number of item snapshots attempted in a backup item_snapshot_success_total - number of successful item snapshots in a backup item_snapshot_failure_total - number of failed item snapshots in a backup Success requires that both the snapshot and the upload succeeded. These stats are set when the backup moves to a terminal phase. Added item snapshots to restore, controlled by UploadProgress feature flag Added error field to ItemSnapshotStatus Updated upload-progress.md doc Fixes #3756 Fixes #3757 Fixes #3759 Signed-off-by: Dave Smith-Uchida Fixed missing path in FSM diagram and fixed typo Signed-off-by: David L. Smith-Uchida Addressed review comments Signed-off-by: Dave Smith-Uchida Regenerated crds.go after rebase Signed-off-by: Dave Smith-Uchida Fixed typo Signed-off-by: Dave Smith-Uchida --- changelogs/unreleased/4467-dsmithuchida | 26 ++ config/crd/v1/bases/velero.io_backups.yaml | 10 + design/UploadFSM.png | Bin 82486 -> 68914 bytes design/upload-progress.md | 152 ++++----- internal/delete/delete_item_action_handler.go | 74 +++- .../delete/delete_item_action_handler_test.go | 20 +- pkg/apis/velero/v1/backup.go | 12 +- pkg/backup/item_backupper.go | 126 +++++-- pkg/backup/item_snapshot_info.go | 76 +++++ pkg/backup/request.go | 5 +- pkg/cmd/server/server.go | 15 +- pkg/controller/backup_controller.go | 323 ++++++++++++++++-- pkg/controller/backup_controller_test.go | 17 +- pkg/controller/backup_deletion_controller.go | 53 ++- .../backup_deletion_controller_test.go | 22 +- pkg/controller/backup_tracker.go | 44 ++- pkg/controller/backup_tracker_test.go | 40 ++- pkg/controller/restore_controller.go | 28 +- pkg/metrics/metrics.go | 24 ++ pkg/plugin/clientmgmt/manager.go | 12 +- pkg/plugin/clientmgmt/registry.go | 73 ++++ .../restartable_item_snapshotter.go | 11 + pkg/plugin/clientmgmt/util.go | 32 ++ .../framework/backup_item_action_server.go | 11 +- .../framework/backup_item_action_test.go | 2 +- .../framework/item_snapshotter_client.go | 14 +- .../item_snapshotter/v1/item_snapshotter.go | 5 +- .../v1/mocks/item_snapshotter.go | 42 +-- pkg/restore/restore.go | 314 ++++++++++------- pkg/volume/item_snapshot.go | 3 + 30 files changed, 1211 insertions(+), 375 deletions(-) create mode 100644 changelogs/unreleased/4467-dsmithuchida create mode 100644 pkg/backup/item_snapshot_info.go create mode 100644 pkg/plugin/clientmgmt/util.go diff --git a/changelogs/unreleased/4467-dsmithuchida b/changelogs/unreleased/4467-dsmithuchida new file mode 100644 index 0000000000..e0a4eb38a8 --- /dev/null +++ b/changelogs/unreleased/4467-dsmithuchida @@ -0,0 +1,26 @@ +Added Upload Progress Monitoring and ItemSnapshotters. This enables monitoring +of snapshots that need additional processing such as an upload to an object +store after the snapshot is taken. Backups are now split into two phases with +the new uploading phase happening after the main part of the backup is complete. +Once a backup has entered the uploading phase, another backup can be executed. +On restart of the server, any backups that are in an Uploading phase will +continue to be monitored. + +Added metrics for Item Snapshots in a backup + + item_snapshot_attempt_total - number of item snapshots attempted in a backup + item_snapshot_success_total - number of successful item snapshots in a backup + item_snapshot_failure_total - number of failed item snapshots in a backup + + Success requires that both the snapshot and the upload succeeded. These stats + are set when the backup moves to a terminal phase. + +Added upload-progress-check-interval flag to control how often snapshots are checked for upload progress - defaults to 1 minute + +This feature is currently enabled with the EnableUploadProgress flag. It +also requires ItemSnapshot plugins. + +Fixes #3756 +Fixes #3757 +Fixes #3759 + diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 03bfed1ffb..1a053f0567 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -435,12 +435,22 @@ spec: description: FormatVersion is the backup format version, including major, minor, and patch version. type: string + itemSnapshotsAttempted: + description: ItemSnapshotsAttempted is the total number of attempted + item snapshots for this backup. + type: integer + itemSnapshotsCompleted: + description: ItemSnapshotsCompleted is the total number of successfully + completed item snapshots for this backup. + type: integer phase: description: Phase is the current state of the Backup. enum: - New - FailedValidation - InProgress + - Uploading + - UploadingPartialFailure - Completed - PartiallyFailed - Failed diff --git a/design/UploadFSM.png b/design/UploadFSM.png index 51fad13c7692d147ec200f25225384ad77c91ab9..d3cd2122847ab5174c6858a107884def883aab7f 100644 GIT binary patch literal 68914 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 z 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 {$F5 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 diff --git a/design/upload-progress.md b/design/upload-progress.md index 22481a74ca..7d26969b71 100644 --- a/design/upload-progress.md +++ b/design/upload-progress.md @@ -24,9 +24,7 @@ Restic - Does not go through the volume snapshot path. Restic backups will bloc - Enable monitoring of operations that continue after snapshotting operations have completed - Keep non-usable backups (upload/persistence has not finished) from appearing as completed - Minimize change to volume snapshot and BackupItemAction plug-ins - -## Non-goals -- Unification of BackupItemActions and VolumeSnapshotters +- Introduce the ItemSnapshotter interface to supersede VolumeSnapshotter ## Models @@ -148,85 +146,70 @@ do not have a `velero-backup.json` object in the object store will be ignored. ## Plug-in API changes -### UploadProgress struct - - type UploadProgress struct { - completed bool // True when the operation has completed, either successfully or with a failure - err error // Set when the operation has failed - itemsCompleted, itemsToComplete int64 // The number of items that have been completed and the items to complete - // For a disk, an item would be a byte and itemsToComplete would be the - // total size to transfer (may be less than the size of a volume if - // performing an incremental) and itemsCompleted is the number of bytes - // transferred. On successful completion, itemsCompleted and itemsToComplete - // should be the same - started, updated time.Time // When the upload was started and when the last update was seen. Not all - // systems retain when the upload was begun, return Time 0 (time.Unix(0, 0)) - // if unknown. - } - -### VolumeSnapshotter changes - -A new method will be added to the VolumeSnapshotter interface (details depending on plug-in versioning spec) - - UploadProgress(snapshotID string) (UploadProgress, error) - -UploadProgress will report the current status of a snapshot upload. This should be callable at any time after the snapshot -has been taken. In the event a plug-in is restarted, if the snapshotID continues to be valid it should be possible to -retrieve the progress. - -`error` is set if there is an issue retrieving progress. If the snapshot is has encountered an error during the upload, -the error should be return in UploadProgress and error should be nil. - -### SnapshotItemAction plug-in +### ItemSnapshotter plug-in Currently CSI snapshots and the Velero Plug-in for vSphere are implemented as BackupItemAction plugins. The majority of BackupItemAction plugins do not take snapshots or upload data so rather than modify BackupItemAction we introduce a new -plug-ins, SnapshotItemAction. SnapshotItemAction will be used in place of BackupItemAction for -the CSI snapshots and the Velero Plug-in for vSphere and will return a snapshot ID in addition to the item itself. +plug-ins, SnapshotItemAction. ItemSnapshotter will be used in place of BackupItemAction for +the CSI snapshots and the Velero Plug-in for vSphere and will return a snapshot ID in addition to the item itself. The existing +VolumeSnapshotter plugins will be converted into ItemSnapshotter plugins as well. -The SnapshotItemAction plugin identifier as well as the Item and Snapshot ID will be stored in the +The ItemSnapshotter plugin identifier as well as the Item and Snapshot ID will be stored in the `-itemsnapshots.json.gz`. When checking for progress, this info will be used to select the appropriate -SnapshotItemAction plugin to query for progress. +ItemSnapshotter plugin to query for progress. -_NotApplicable_ should only be returned if the SnapshotItemAction plugin should not be handling the item. If the -SnapshotItemAction plugin should handle the item but, for example, the item/snapshot ID cannot be found to report progress, a -UploadProgress struct with the error set appropriately (in this case _NotFound_) should be returned. +_NotApplicable_ should only be returned if the ItemSnapshotter plugin should not be handling the item. If the +ItemSnapshotter plugin should handle the item but, for example, the item/snapshot ID cannot be found to report progress, a +SnapshotItemProgressOutput struct with the error set appropriately (in this case _NotFound_) should be returned. - // SnapshotItemAction is an actor that snapshots an individual item being backed up (it may also do other - operations on the item that is returned). + // ItemSnapshotter handles snapshots on an individual item being backed up. + type ItemSnapshotter interface { + + // Init prepares the ItemSnapshotter for usage using the provided map of + // configuration key-value pairs. It returns an error if the ItemSnapshotter + // cannot be initialized from the provided config. + Init(config map[string]string) error - type SnapshotItemAction interface { // AppliesTo returns information about which resources this action should be invoked for. - // A BackupItemAction's Execute function will only be invoked on items that match the returned + // An ItemSnapshotter's SnapshotItem method will only be invoked on items that match the returned // selector. A zero-valued ResourceSelector matches all resources. - AppliesTo() (ResourceSelector, error) + AppliesTo() (velero.ResourceSelector, error) - // Execute allows the ItemAction to perform arbitrary logic with the item being backed up, - // including mutating the item itself prior to backup. The item (unmodified or modified) - // should be returned, along with an optional slice of ResourceIdentifiers specifying + // AlsoHandles is called for each item this ItemSnapshotter should handle and returns any items + // which will be handled by this plugin when snapshotting the item. These items will be excluded from the + // items being backed up. AlsoHandles will be called before SnapshotItem is called. For example, a database may expose + // a database resource that can be snapshotted. If the database uses a PVC that will be snapshotted/backed up as + // part of the database snapshot, that PVC should be returned when AlsoHandles is invoked. This is different from + // AdditionalItems (returned in SnapshotItemOutput and CreateItemOutput) which are specifying additional resources + // that Velero should store in the backup or create. + AlsoHandles(input *AlsoHandlesInput) ([]velero.ResourceIdentifier, error) + + // SnapshotItem causes the ItemSnapshotter to snapshot the specified item. It may also + // perform arbitrary logic with the item being backed up, including mutating the item itself prior to backup. + // The item (unmodified or modified) should be returned, along with an optional slice of ResourceIdentifiers specifying // additional related items that should be backed up. - Execute(item runtime.Unstructured, backup *api.Backup) (runtime.Unstructured, snapshotID string, - []ResourceIdentifier, error) - - // Progress - Progress(input *SnapshotItemProgressInput) (UploadProgress, error) - } + // A caller can pass a context that includes a timeout. If the time to take the snapshot exceeds the + // time in the context, the plugin may abort the snapshot. The context timeout does not apply to upload + // time that occurs after SnapshotItem returns + SnapshotItem(ctx context.Context, input *SnapshotItemInput) (*SnapshotItemOutput, error) - // SnapshotItemProgressInput contains the input parameters for the SnapshotItemAction's Progress function. - type SnapshotItemProgressInput struct { - // Item is the item that was stored in the backup - Item runtime.Unstructured - // SnapshotID is the snapshot ID returned by SnapshotItemAction - SnapshotID string - // Backup is the representation of the restore resource processed by Velero. - Backup *velerov1api.Backup + // Progress will return the progress of a snapshot that is being uploaded + Progress(input *ProgressInput) (*ProgressOutput, error) + + // DeleteSnapshot removes a snapshot + DeleteSnapshot(ctx context.Context, input *DeleteSnapshotInput) error + + // CreateItemFromSnapshot creates a new item from the snapshot. The item to restore from the + // backup is passed in and may be modified during CreateItemFromSnapshot and the modified item + // returned in CreateItemOutput. This allows operations such as restoring a PVC to return a PVC + // that points to a newly created PV. AdditionalItems to restore may also be returned in CreateItemOutput + CreateItemFromSnapshot(ctx context.Context, input *CreateItemInput) (*CreateItemOutput, error) } - ## Changes in Velero backup format No changes to the existing format are introduced by this change. A `-itemsnapshots.json.gz` file will be -added that contains the items and snapshot IDs returned by ItemSnapshotAction. Also, the creation of the +added that contains the items and snapshot IDs returned by ItemSnapshotter.SnapshotItem. Also, the creation of the `velero-backup.json` object will not occur until the backup moves to one of the terminal phases (_Completed_, _PartiallyFailed_, or _Failed_). Reconciliation should ignore backups that do not have a `velero-backup.json` object. @@ -238,8 +221,8 @@ If the Backup resource is removed (e.g. Velero is uninstalled) before a backup c can currently happen but the current window is much smaller. ### `-itemsnapshots.json.gz` -The itemsnapshots file is similar to the existing `-itemsnapshots.json.gz` Each snapshot taken via -SnapshotItemAction will have a JSON record in the file. Exact format TBD. +The itemsnapshots file is similar to the existing `-volumesnapshots.json.gz` Each snapshot taken via +_ItemSnapshotter_ will have a JSON record in the file. Exact format TBD. ## CSI snapshots @@ -256,56 +239,55 @@ it will check the status of the Upload records for the snapshot and return progr ## Backup workflow changes The backup workflow remains the same until we get to the point where the `velero-backup.json` object is written. -At this point, we will queue the backup to a finalization go-routine. The next backup may then begin. The finalization -routine will run across all of the volume snapshots and call the _UploadProgress_ method on each of them. It will -then run across all items and call _BackupItemProgress.Progress_ for any that match with a BackupItemProgress. +At this point, we will queue the backup to a finalization go-routine. The next backup may then begin. It will +then run across all items and call _ItemSnapshotter.Progress_ for any that match with a BackupItemProgress. -If all snapshots and backup items have finished uploading (either successfully or failed), the backup will be completed +If all backup items have finished uploading (either successfully or failed), the backup will be completed and the backup will move to the appropriate terminal phase and upload the `velero-backup.json` object to the object store and the backup will be complete. If any of the snapshots or backup items are still being processed, the phase of the backup will be set to the appropriate -phase (_Uploading_ or _UploadingPartialFailure_). In the event of any of the upload progress checks return an error, the +phase (_Uploading_ or _UploadingPartialFailure_). In the event that any of the upload progress checks return an error, the phase will move to _UploadingPartialFailure_. The backup will then be requeued and will be rechecked again after some time has passed. ## Restart workflow On restart, the Velero server will scan all Backup resources. Any Backup resources which are in the _InProgress_ phase -will be moved to the _Failed_ phase. Any Backup resources in the _Oploading_ or _OploadingPartialFailure_ phase will +will be moved to the _Failed_ phase. Any Backup resources in the _Uploading_ or _UploadingPartialFailure_ phase will be treated as if they have been requeued and progress checked and the backup will be requeued or moved to a terminal phase as appropriate. # Implementation tasks -VolumeSnapshotter new plugin APIs -BackupItemProgress new plugin interface +ItemSnapshotter new plugin interface New backup phases Defer uploading `velero-backup.json` AWS EBS plug-in UploadProgress implementation Upload monitoring Implementation of `-itemsnapshots.json.gz` file Restart logic -Change in reconciliation logic to ignore backups that have not completed -CSI plug-in BackupItemProgress implementation -vSphere plug-in BackupItemProgress implementation (vSphere plug-in team) +CSI plug-in ItemSnapshotter implementation +vSphere plug-in ItemSnapshotter implementation (vSphere plug-in team) -# Future Fragile/Durable snapshot tracking +# Future Ephemeral/Durable snapshot tracking Futures are here for reference, they may change radically when actually implemented. -Some storage systems have the ability to provide different levels of protection for snapshots. These are termed "Fragile" +Some storage systems have the ability to provide different levels of protection for snapshots. These are termed "Ephemeral" and "Durable". Currently, Velero expects snapshots to be Durable (they should be able to survive the destruction of the cluster and the storage it is using). In the future we would like the ability to take advantage of snapshots that are -Fragile. For example, vSphere snapshots are Fragile (they reside in the same datastore as the virtual disk). The Velero -Plug-in for vSphere uses a vSphere local/fragile snapshot to get a consistent snapshot, then uploads the data to S3 to +Ephemeral. For example, vSphere snapshots are Ephemeral (they reside in the same datastore as the virtual disk). The Velero +Plug-in for vSphere uses a vSphere local/ephemeral snapshot to get a consistent snapshot, then uploads the data to S3 to make it Durable. In the current design, upload progress will not be complete until the snapshot is ready to use and Durable. It is possible, however, to restore data from a vSphere snapshot before it has been made Durable, and this is a capability we'd like to expose in the future. Other storage systems implement this functionality as well. We will be moving the control of the data movement from the vSphere plug-in into Velero. -Some storage system, such as EBS, are only capable of creating Durable snapshots. There is no usable intermediate Fragile stage. +Some storage system, such as EBS, are only capable of creating Durable snapshots. There is no usable intermediate +Ephemeral stage, however there is a time when a snapshot has been taken but is not yet +usable. -For a Velero backup, users should be able to specify whether they want a Durable backup or a Fragile backup (Fragile backups +For a Velero backup, users should be able to specify whether they want a Durable backup or a Ephemeral backup (Ephemeral backups may consume less resources, be quicker to restore from and are suitable for things like backing up a cluster before upgrading -software). We can introduce three snapshot states - Creating, Fragile and Durable. A snapshot would be created with a -desired state, Fragile or Durable. When the snapshot reaches the desired or higher state (e.g. request was for Fragile but +software). We can introduce three snapshot states - Creating, Ephemeral and Durable. A snapshot would be created with a +desired state, Ephemeral or Durable. When the snapshot reaches the desired or higher state (e.g. request was for Ephemeral but snapshot went to Durable as on EBS), then the snapshot would be completed. diff --git a/internal/delete/delete_item_action_handler.go b/internal/delete/delete_item_action_handler.go index b08c8e024e..eaf0140457 100644 --- a/internal/delete/delete_item_action_handler.go +++ b/internal/delete/delete_item_action_handler.go @@ -17,8 +17,15 @@ limitations under the License. package delete import ( + "context" + "encoding/json" "io" + "github.com/vmware-tanzu/velero/pkg/features" + "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + "github.com/vmware-tanzu/velero/pkg/volume" + "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/pkg/errors" @@ -34,28 +41,24 @@ import ( "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) -// Context provides the necessary environment to run DeleteItemAction plugins +// Context provides the necessary environment to run DeleteItemAction and ItemSnapshotter plugins type Context struct { - Backup *velerov1api.Backup - BackupReader io.Reader - Actions []velero.DeleteItemAction - Filesystem filesystem.Interface - Log logrus.FieldLogger - DiscoveryHelper discovery.Helper - resolvedActions []framework.DeleteItemResolvedAction + Backup *velerov1api.Backup + BackupReader io.Reader + Filesystem filesystem.Interface + Log logrus.FieldLogger + DiscoveryHelper discovery.Helper + DeleteItemResolvedActions []framework.DeleteItemResolvedAction + ItemSnapshots []*volume.ItemSnapshot + ItemSnapshotters []isv1.ItemSnapshotter } func InvokeDeleteActions(ctx *Context) error { - var err error - resolver := framework.NewDeleteItemActionResolver(ctx.Actions) - ctx.resolvedActions, err = resolver.ResolveActions(ctx.DiscoveryHelper) - // No actions installed and no error means we don't have to continue; + // No actions installed and no item snapshots means we don't have to continue; // just do the backup deletion without worrying about plugins. - if len(ctx.resolvedActions) == 0 && err == nil { - ctx.Log.Debug("No delete item actions present, proceeding with rest of backup deletion process") + if len(ctx.DeleteItemResolvedActions) == 0 && len(ctx.ItemSnapshots) == 0 { + ctx.Log.Debug("No delete item actions or item snapshots present, proceeding with rest of backup deletion process") return nil - } else if err != nil { - return errors.Wrapf(err, "error resolving actions") } // get items out of backup tarball into a temp directory @@ -121,19 +124,54 @@ func InvokeDeleteActions(ctx *Context) error { // Since we want to keep looping even on errors, log them instead of just returning. if err != nil { itemLog.WithError(err).Error("plugin error") - } } } } } + + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + ctx.Log.Info("Deleting item snapshots") + // Handle item snapshots + for _, snapshot := range ctx.ItemSnapshots { + rid := velero.ResourceIdentifier{} + json.Unmarshal([]byte(snapshot.Spec.ResourceIdentifier), &rid) + itemLogger := ctx.Log.WithFields(logrus.Fields{ + "namespace": rid.Namespace, + "resource": rid.Resource, + "item": rid.Name, + "snapshotID": snapshot.Status.ProviderSnapshotID, + }) + itemLogger.Info("Deleting item snapshot") + + itemSnapshotter := clientmgmt.ItemSnapshotterForSnapshot(snapshot, ctx.ItemSnapshotters) + + itemPath := archive.GetItemFilePath(dir, rid.Resource, rid.Namespace, rid.Name) + + // obj is the Unstructured item from the backup + obj, err := archive.Unmarshal(ctx.Filesystem, itemPath) + if err != nil { + itemLogger.WithError(err).Errorf("Could not unmarshal item: %v", rid) + continue + } + dsi := isv1.DeleteSnapshotInput{ + SnapshotID: snapshot.Status.ProviderSnapshotID, + ItemFromBackup: obj, + SnapshotMetadata: snapshot.Status.Metadata, + Params: nil, // TBD + } + if err := itemSnapshotter.DeleteSnapshot(context.TODO(), &dsi); err != nil { + itemLogger.WithError(err).Error("Error deleting snapshot") + } + } + } return nil } // getApplicableActions takes resolved DeleteItemActions and filters them for a given group/resource and namespace. func (ctx *Context) getApplicableActions(groupResource schema.GroupResource, namespace string) []framework.DeleteItemResolvedAction { var actions []framework.DeleteItemResolvedAction - for _, action := range ctx.resolvedActions { + for _, action := range ctx.DeleteItemResolvedActions { if action.ShouldUse(groupResource, namespace, nil, ctx.Log) { actions = append(actions, action) } diff --git a/internal/delete/delete_item_action_handler_test.go b/internal/delete/delete_item_action_handler_test.go index 8bd1206020..5942fb0e64 100644 --- a/internal/delete/delete_item_action_handler_test.go +++ b/internal/delete/delete_item_action_handler_test.go @@ -22,6 +22,8 @@ import ( "sort" "testing" + "github.com/vmware-tanzu/velero/pkg/plugin/framework" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -165,16 +167,20 @@ func TestInvokeDeleteItemActionsRunForCorrectItems(t *testing.T) { actions = append(actions, action) } + deleteItemActionResolver := framework.NewDeleteItemActionResolver(actions) + deleteItemResolvedActions, err := deleteItemActionResolver.ResolveActions(h.discoveryHelper) + require.NoError(t, err) + c := &Context{ - Backup: tc.backup, - BackupReader: tc.tarball, - Filesystem: fs, - DiscoveryHelper: h.discoveryHelper, - Actions: actions, - Log: log, + Backup: tc.backup, + BackupReader: tc.tarball, + Filesystem: fs, + DiscoveryHelper: h.discoveryHelper, + DeleteItemResolvedActions: deleteItemResolvedActions, + Log: log, } - err := InvokeDeleteActions(c) + err = InvokeDeleteActions(c) require.NoError(t, err) // Compare the plugins against the ids that we wanted. diff --git a/pkg/apis/velero/v1/backup.go b/pkg/apis/velero/v1/backup.go index 7fc0f7a4c6..f1cd7aff1e 100644 --- a/pkg/apis/velero/v1/backup.go +++ b/pkg/apis/velero/v1/backup.go @@ -207,7 +207,7 @@ const ( // BackupPhase is a string representation of the lifecycle phase // of a Velero backup. -// +kubebuilder:validation:Enum=New;FailedValidation;InProgress;Completed;PartiallyFailed;Failed;Deleting +// +kubebuilder:validation:Enum=New;FailedValidation;InProgress;Uploading;UploadingPartialFailure;Completed;PartiallyFailed;Failed;Deleting type BackupPhase string const ( @@ -305,6 +305,16 @@ type BackupStatus struct { // +optional FailureReason string `json:"failureReason,omitempty"` + // ItemSnapshotsAttempted is the total number of attempted + // item snapshots for this backup. + // +optional + ItemSnapshotsAttempted int `json:"itemSnapshotsAttempted,omitempty"` + + // ItemSnapshotsCompleted is the total number of successfully + // completed item snapshots for this backup. + // +optional + ItemSnapshotsCompleted int `json:"itemSnapshotsCompleted,omitempty"` + // Warnings is a count of all warning messages that were generated during // execution of the backup. The actual warnings are in the backup's log // file in object storage. diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index fd4b16028b..b26f05aa73 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -18,6 +18,7 @@ package backup import ( "archive/tar" + "context" "encoding/json" "fmt" "path/filepath" @@ -41,7 +42,9 @@ import ( "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/features" "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/volume" @@ -169,7 +172,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr // Used on filepath to backup up all groups and versions version := resourceVersion(obj) - updatedObj, err := ib.executeActions(log, obj, groupResource, name, namespace, metadata) + updatedObj, itemSnapshotInfo, err := ib.executeActions(log, obj, groupResource, name, namespace, metadata) if err != nil { backupErrs = append(backupErrs, err) @@ -181,6 +184,9 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr return false, kubeerrs.NewAggregate(backupErrs) } + if itemSnapshotInfo != nil { + ib.backupRequest.ItemSnapshots = append(ib.backupRequest.ItemSnapshots, itemSnapshotInfo) + } obj = updatedObj if metadata, err = meta.Accessor(obj); err != nil { return false, errors.WithStack(err) @@ -189,7 +195,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr name = metadata.GetName() namespace = metadata.GetNamespace() - if groupResource == kuberesource.PersistentVolumes { + if groupResource == kuberesource.PersistentVolumes && itemSnapshotInfo == nil { if err := ib.takePVSnapshot(obj, log); err != nil { backupErrs = append(backupErrs, err) } @@ -304,7 +310,32 @@ func (ib *itemBackupper) executeActions( groupResource schema.GroupResource, name, namespace string, metadata metav1.Object, -) (runtime.Unstructured, error) { +) (runtime.Unstructured, *ItemSnapshotInfo, error) { + var isi *ItemSnapshotInfo + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + for _, resolvedItem := range ib.backupRequest.ResolvedItemSnapshotters { + if !resolvedItem.ShouldUse(groupResource, namespace, metadata, log) { + continue + } + log.Info("Executing item snapshotter") + sii := isv1.SnapshotItemInput{ + Item: obj, + Params: nil, + Backup: ib.backupRequest.Backup, + } + sio, err := resolvedItem.ItemSnapshotter.SnapshotItem(context.TODO(), &sii) + if err != nil { + return nil, nil, errors.Wrapf(err, "error executing item snapshotter (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name) + } + isi = itemSnapshotInfo(resolvedItem.ItemSnapshotter, ib.backupRequest.Backup, groupResource, namespace, name, + obj, isv1.SnapshotPhaseInProgress, sio.SnapshotID, sio.SnapshotMetadata) + if err := ib.handleAdditionalItems(log, sio.AdditionalItems); err != nil { + return sio.UpdatedItem, nil, err + } + // TODO - Remove handled items from the list of things to do somehow (PR #4111) + break // Shouldn't be more than one item snapshotter for this type, but exit anyhow + } + } for _, action := range ib.backupRequest.ResolvedActions { if !action.ShouldUse(groupResource, namespace, metadata, log) { continue @@ -313,41 +344,49 @@ func (ib *itemBackupper) executeActions( updatedItem, additionalItemIdentifiers, err := action.Execute(obj, ib.backupRequest.Backup) if err != nil { - return nil, errors.Wrapf(err, "error executing custom action (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name) + return nil, nil, errors.Wrapf(err, "error executing custom action (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name) } obj = updatedItem - for _, additionalItem := range additionalItemIdentifiers { - gvr, resource, err := ib.discoveryHelper.ResourceFor(additionalItem.GroupResource.WithVersion("")) - if err != nil { - return nil, err - } + if err := ib.handleAdditionalItems(log, additionalItemIdentifiers); err != nil { + return nil, nil, err + } - client, err := ib.dynamicFactory.ClientForGroupVersionResource(gvr.GroupVersion(), resource, additionalItem.Namespace) - if err != nil { - return nil, err - } + } - item, err := client.Get(additionalItem.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - log.WithFields(logrus.Fields{ - "groupResource": additionalItem.GroupResource, - "namespace": additionalItem.Namespace, - "name": additionalItem.Name, - }).Warnf("Additional item was not found in Kubernetes API, can't back it up") - continue - } - if err != nil { - return nil, errors.WithStack(err) - } + return obj, isi, nil +} - if _, err = ib.backupItem(log, item, gvr.GroupResource(), gvr); err != nil { - return nil, err - } +func (ib *itemBackupper) handleAdditionalItems(log logrus.FieldLogger, additionalItemIdentifiers []velero.ResourceIdentifier) error { + for _, additionalItem := range additionalItemIdentifiers { + gvr, resource, err := ib.discoveryHelper.ResourceFor(additionalItem.GroupResource.WithVersion("")) + if err != nil { + return err + } + + client, err := ib.dynamicFactory.ClientForGroupVersionResource(gvr.GroupVersion(), resource, additionalItem.Namespace) + if err != nil { + return err } - } - return obj, nil + item, err := client.Get(additionalItem.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + log.WithFields(logrus.Fields{ + "groupResource": additionalItem.GroupResource, + "namespace": additionalItem.Namespace, + "name": additionalItem.Name, + }).Warnf("Additional item was not found in Kubernetes API, can't back it up") + continue + } + if err != nil { + return errors.WithStack(err) + } + + if _, err = ib.backupItem(log, item, gvr.GroupResource(), gvr); err != nil { + return err + } + } + return nil } // volumeSnapshotter instantiates and initializes a VolumeSnapshotter given a VolumeSnapshotLocation, @@ -526,6 +565,33 @@ func volumeSnapshot(backup *velerov1api.Backup, volumeName, volumeID, volumeType } } +func itemSnapshotInfo(itemSnapshotter isv1.ItemSnapshotter, backup *velerov1api.Backup, groupResource schema.GroupResource, + namespace string, name string, item runtime.Unstructured, phase isv1.SnapshotPhase, snapshotID string, + snapshotMetadata map[string]string) *ItemSnapshotInfo { + localCheckItemSnapshotter := itemSnapshotter.(clientmgmt.LocalItemSnapshotter) + + return &ItemSnapshotInfo{ + ItemSnapshotter: itemSnapshotter, + Spec: ItemSnapshotInfoSpec{ + ItemSnapshotter: localCheckItemSnapshotter.GetName(), + Backup: backup, + BackupName: backup.Name, + BackupUID: backup.UID, + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: groupResource, + Namespace: namespace, + Name: name, + }, + Location: velerov1api.VolumeSnapshotLocation{}, + }, + Status: ItemSnapshotInfoStatus{ + Phase: phase, + ProviderSnapshotID: snapshotID, + Metadata: snapshotMetadata, + }, + } +} + // resourceKey returns a string representing the object's GroupVersionKind (e.g. // apps/v1/Deployment). func resourceKey(obj runtime.Unstructured) string { diff --git a/pkg/backup/item_snapshot_info.go b/pkg/backup/item_snapshot_info.go new file mode 100644 index 0000000000..65777fad82 --- /dev/null +++ b/pkg/backup/item_snapshot_info.go @@ -0,0 +1,76 @@ +/* +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 backup + +import ( + "encoding/json" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + "github.com/vmware-tanzu/velero/pkg/volume" +) + +// ItemSnapshotInfo is used for our in-memory representation of ItemSnapshots and contains pointers to runtime +// structures such as ItemSnapshotter and Backup. Converted to/from volume.ItemSnapshot for externalization + +type ItemSnapshotInfo struct { + ItemSnapshotter isv1.ItemSnapshotter + Spec ItemSnapshotInfoSpec + Status ItemSnapshotInfoStatus +} + +type ItemSnapshotInfoSpec struct { + ItemSnapshotter string + Backup *velerov1api.Backup + BackupName string + BackupUID types.UID + ResourceIdentifier velero.ResourceIdentifier + Location velerov1api.VolumeSnapshotLocation +} + +type ItemSnapshotInfoStatus struct { + ProviderSnapshotID string + Metadata map[string]string + Phase isv1.SnapshotPhase + Error string +} + +func (recv ItemSnapshotInfo) GetItemSnapshot() (volume.ItemSnapshot, error) { + ridStr, err := json.Marshal(recv.Spec.ResourceIdentifier) + if err != nil { + return volume.ItemSnapshot{}, errors.WithMessagef(err, "failed to marshal resource identifier %v", recv.Spec.ResourceIdentifier) + } + return volume.ItemSnapshot{ + Spec: volume.ItemSnapshotSpec{ + ItemSnapshotter: recv.Spec.ItemSnapshotter, + BackupName: recv.Spec.BackupName, + BackupUID: string(recv.Spec.BackupUID), + Location: recv.Spec.Location.Name, + ResourceIdentifier: string(ridStr), + }, + Status: volume.ItemSnapshotStatus{ + ProviderSnapshotID: recv.Status.ProviderSnapshotID, + Metadata: recv.Status.Metadata, + Phase: recv.Status.Phase, + Error: recv.Status.Error, + }, + }, nil +} diff --git a/pkg/backup/request.go b/pkg/backup/request.go index 38cd499177..1539247bfb 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -19,6 +19,7 @@ package backup import ( "fmt" "sort" + "time" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" @@ -48,9 +49,11 @@ type Request struct { ResolvedActions []framework.BackupItemResolvedAction ResolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction VolumeSnapshots []*volume.Snapshot + ItemSnapshots []*ItemSnapshotInfo PodVolumeBackups []*velerov1api.PodVolumeBackup BackedUpItems map[itemKey]struct{} CSISnapshots []*snapshotv1api.VolumeSnapshot + NextCheckTime time.Time // Next time to check snapshot progress - if 0, don't check } // BackupResourceList returns the list of backed up resources grouped by the API diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 5aaee731c3..6cdb9a4a49 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -104,6 +104,12 @@ const ( // defaultCredentialsDirectory is the path on disk where credential // files will be written to defaultCredentialsDirectory = "/tmp/credentials" + + // the default interval to check upload progress per backup + defaultUploadProgressCheckInterval = time.Minute + + // the default interval to check upload progress for backups that are in Uploading or UploadingPartialFailure phase + defaultFinalizeBackupsInterval = time.Second ) type serverConfig struct { @@ -123,6 +129,8 @@ type serverConfig struct { defaultResticMaintenanceFrequency time.Duration garbageCollectionFrequency time.Duration defaultVolumesToRestic bool + uploadProgressCheckInterval time.Duration + finalizeBackupsInterval time.Duration } type controllerRunInfo struct { @@ -152,6 +160,8 @@ func NewCommand(f client.Factory) *cobra.Command { formatFlag: logging.NewFormatFlag(), defaultResticMaintenanceFrequency: restic.DefaultMaintenanceFrequency, defaultVolumesToRestic: restic.DefaultVolumesToRestic, + uploadProgressCheckInterval: defaultUploadProgressCheckInterval, + finalizeBackupsInterval: defaultFinalizeBackupsInterval, } ) @@ -217,7 +227,8 @@ func NewCommand(f client.Factory) *cobra.Command { command.Flags().DurationVar(&config.defaultResticMaintenanceFrequency, "default-restic-prune-frequency", config.defaultResticMaintenanceFrequency, "How often 'restic prune' is run for restic repositories by default.") command.Flags().DurationVar(&config.garbageCollectionFrequency, "garbage-collection-frequency", config.garbageCollectionFrequency, "How often garbage collection is run for expired backups.") command.Flags().BoolVar(&config.defaultVolumesToRestic, "default-volumes-to-restic", config.defaultVolumesToRestic, "Backup all volumes with restic by default.") - + command.Flags().DurationVar(&config.uploadProgressCheckInterval, "upload-progress-check-interval", config.uploadProgressCheckInterval, "How long to wait between checking upload progress for a backup.") + command.Flags().DurationVar(&config.finalizeBackupsInterval, "finalize-backups-interval", config.finalizeBackupsInterval, "How long to wait between checking backups for completion when snapshots have uploaded.") return command } @@ -656,6 +667,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string csiVSCLister, csiVSClassLister, backupStoreGetter, + s.config.uploadProgressCheckInterval, + s.config.finalizeBackupsInterval, ) return controllerRunInfo{ diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 4aea251563..d1d7083446 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -63,6 +63,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/util/encode" @@ -97,6 +98,8 @@ type backupController struct { volumeSnapshotClient *snapshotterClientSet.Clientset volumeSnapshotContentLister snapshotv1listers.VolumeSnapshotContentLister volumeSnapshotClassLister snapshotv1listers.VolumeSnapshotClassLister + uploadProgressCheckInterval time.Duration + finalizeBackupsInterval time.Duration } func NewBackupController( @@ -121,6 +124,8 @@ func NewBackupController( volumeSnapshotContentLister snapshotv1listers.VolumeSnapshotContentLister, volumesnapshotClassLister snapshotv1listers.VolumeSnapshotClassLister, backupStoreGetter persistence.ObjectBackupStoreGetter, + uploadProgressCheckInterval time.Duration, + finalizeBackupsInterval time.Duration, ) Interface { c := &backupController{ genericController: newGenericController(Backup, logger), @@ -145,6 +150,8 @@ func NewBackupController( volumeSnapshotContentLister: volumeSnapshotContentLister, volumeSnapshotClassLister: volumesnapshotClassLister, backupStoreGetter: backupStoreGetter, + uploadProgressCheckInterval: uploadProgressCheckInterval, + finalizeBackupsInterval: finalizeBackupsInterval, } c.syncHandler = c.processBackup @@ -157,7 +164,27 @@ func NewBackupController( backup := obj.(*velerov1api.Backup) switch backup.Status.Phase { +<<<<<<< HEAD case "", velerov1api.BackupPhaseNew, velerov1api.BackupPhaseInProgress: +======= + case "", velerov1api.BackupPhaseNew: + // only process new backups + case velerov1api.BackupPhaseUploading, velerov1api.BackupPhaseUploadingPartialFailure: + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + c.logger.WithFields(logrus.Fields{ + "backup": kubeutil.NamespaceAndName(backup), + "phase": backup.Status.Phase, + }).Info("Found uploading backup on restart, tracking upload progress") + request := c.prepareBackupRequest(backup) + c.backupTracker.Add(request.Namespace, request.Name, request) + } else { + // TODO - temporary to get existing behavior (leave it alone and log) if the phase is not "New" + c.logger.WithFields(logrus.Fields{ + "backup": kubeutil.NamespaceAndName(backup), + "phase": backup.Status.Phase, + }).Debug("Backup is not new, skipping") + } +>>>>>>> ca95d5ee... Adding basic item snapshotter support default: c.logger.WithFields(logrus.Fields{ "backup": kubeutil.NamespaceAndName(backup), @@ -176,6 +203,10 @@ func NewBackupController( }, ) + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + // Start the background task that checks upload progress and finalizes backups + go c.checkAndFinalizeBackupsLoop(c.finalizeBackupsInterval) + } return c } @@ -220,6 +251,9 @@ func getLastSuccessBySchedule(backups []*velerov1api.Backup) map[string]time.Tim return lastSuccessBySchedule } +// Value of time to use as "don't process" indicator (not a const because of Golang/Time ugliness) +var dontProcessTime = time.Unix(0, 0) + func (c *backupController) processBackup(key string) error { log := c.logger.WithField("key", key) @@ -294,8 +328,19 @@ func (c *backupController) processBackup(key string) error { return nil } - c.backupTracker.Add(request.Namespace, request.Name) - defer c.backupTracker.Delete(request.Namespace, request.Name) + // Set NextCheckTime to dontProcessTime so that checkAndFinalizeBackupsLoop does not process it after being added to + // the tracker + request.NextCheckTime = dontProcessTime + c.backupTracker.Add(request.Namespace, request.Name, request) + + // If upload progress tracking will continue after exit we do not want to remove from the tracker, but normal + // case is to remove if we exit for any reason + removeFromTracker := true + defer func() { + if removeFromTracker { + c.backupTracker.Delete(request.Namespace, request.Name) + } + }() log.Debug("Running backup") @@ -331,9 +376,125 @@ func (c *backupController) processBackup(key string) error { log.WithError(err).Error("error updating backup's final status") } + if request.Status.Phase == velerov1api.BackupPhaseUploading || request.Status.Phase == velerov1api.BackupPhaseUploadingPartialFailure { + removeFromTracker = false // Leave it in the tracker so it gets checked + // Set NextCheckTime here so that checkAndFinalizeBackupsLoop will pick it up now, after everything has been + // persisted otherwise we have a race + request.NextCheckTime = time.Now().Add(c.uploadProgressCheckInterval) + } + return nil } +// Execute periodically to check upload progress for backups that are in Uploading or UploadingPartialFailure phase +func (c *backupController) checkAndFinalizeBackupsLoop(wakeupInterval time.Duration) { + for { + time.Sleep(time.Second) // Wake up frequently so that we will process any backups moving into Upload phase quickly + // This wakeup is pretty lightweight as it just looks for things to process, we actually + // check progress less frequently, by default once per minute per backup + uploadingBackups := c.backupTracker.GetByPhase(velerov1api.BackupPhaseUploading) + uploadingBackups = append(uploadingBackups, c.backupTracker.GetByPhase(velerov1api.BackupPhaseUploadingPartialFailure)...) + for _, curBackup := range uploadingBackups { + if curBackup.NextCheckTime != dontProcessTime && curBackup.NextCheckTime.After(time.Now()) { + snapshotsInProgress := checkAndUpdateItemSnapshotsProgress(curBackup.ItemSnapshots, c.logger) + if !snapshotsInProgress { + c.completeBackupWithUploadedSnapshots(curBackup) + } else { + curBackup.NextCheckTime = time.Now().Add(c.uploadProgressCheckInterval) + } + } + } + } +} + +func (c *backupController) completeBackupWithUploadedSnapshots(curBackup *pkgbackup.Request) { + c.backupTracker.Delete(curBackup.Namespace, curBackup.Name) + log := c.logger.WithField(Backup, kubeutil.NamespaceAndName(curBackup)) + original := *curBackup.Backup + + if curBackup.Status.Phase == velerov1api.BackupPhaseUploading { + curBackup.Status.Phase = velerov1api.BackupPhaseCompleted + } + if curBackup.Status.Phase == velerov1api.BackupPhaseUploadingPartialFailure { + curBackup.Status.Phase = velerov1api.BackupPhasePartiallyFailed + } + backupScheduleName := curBackup.GetLabels()[velerov1api.ScheduleNameLabel] + + // Mark completion timestamp before serializing and uploading. + // Otherwise, the JSON file in object storage has a CompletionTimestamp of 'null'. + curBackup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} + recordBackupDurationMetric(curBackup.Backup, c.metrics) + + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + updateBackupStatusForSnapshotState(curBackup, log) + } + + log.Debug("Updating backup's final status") + if _, err := patchBackup(&original, curBackup.Backup, c.client); err != nil { + log.WithError(err).Error("error updating backup's final status after upload completion") + } + + switch curBackup.Status.Phase { + case velerov1api.BackupPhaseCompleted: + c.metrics.RegisterBackupSuccess(backupScheduleName) + case velerov1api.BackupPhasePartiallyFailed: + c.metrics.RegisterBackupPartialFailure(backupScheduleName) + case velerov1api.BackupPhaseFailed: + c.metrics.RegisterBackupFailed(backupScheduleName) + case velerov1api.BackupPhaseFailedValidation: + c.metrics.RegisterBackupValidationFailure(backupScheduleName) + } + + curBackup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} + + pluginManager := c.newPluginManager(c.logger) + defer pluginManager.CleanupClients() + backupStore, err := c.backupStoreGetter.Get(curBackup.StorageLocation, pluginManager, c.logger) + if err != nil { + log.WithError(err).Error("error getting backup store after upload completion") + } + if errs := persistBackupFinal(curBackup, backupStore); len(errs) > 0 { + for _, err := range errs { + log.WithError(err).Error("error persisting backup after upload completion") + } + // Set the phase to partially failed. Unclear what part of the backup did not persist properly, + // but probably the backup objects in the object store are not usable. It's possible for the metadata + // to be persisted properly with BackupPhaseCompleted and something else to have failed which will wind + // up with the object store and the Backup resource in the API server being out of sync, but this is + // unlikely enough to not worry about for now + curBackup.Status.Phase = velerov1api.BackupPhasePartiallyFailed + log.Debug("Updating backup's final status after persist failure") + if _, err := patchBackup(&original, curBackup.Backup, c.client); err != nil { + // If this failed we're in pretty bad shape + log.WithError(err).Error("error updating backup's final status after upload completion") + } + log.Error("Backup completed after upload completion, errors persisting backup") + } else { + log.Info("Backup completed after upload completion") + } +} + +func updateBackupStatusForSnapshotState(curBackup *pkgbackup.Request, log logrus.FieldLogger) { + // Update number of snapshots completed. If any of the snapshot uploads failed we change the backup state to + // BackupPhasePartiallyFailed + curBackup.Status.ItemSnapshotsAttempted = len(curBackup.ItemSnapshots) + for _, snap := range curBackup.ItemSnapshots { + switch snap.Status.Phase { + case isv1.SnapshotPhaseCompleted: + curBackup.Status.ItemSnapshotsCompleted++ + case isv1.SnapshotPhaseInProgress: + // This should never happen, but rather than panic we log an error + log.WithFields(logrus.Fields{ + "item": snap.Spec.ResourceIdentifier, + "snapshotID": snap.Status.ProviderSnapshotID}).Error("Snapshot found with phase SnapshotInProgress and backup is in completed state") + case isv1.SnapshotPhaseFailed: + // We can only get here if the backup is either BackupPhaseCompleted or BackupPhasePartiallyFailed so + // setting it here won't lose the status if it completely failed (BackupPhaseFailed) + curBackup.Status.Phase = velerov1api.BackupPhasePartiallyFailed + } + } +} + func patchBackup(original, updated *velerov1api.Backup, client velerov1client.BackupsGetter) (*velerov1api.Backup, error) { origBytes, err := json.Marshal(original) if err != nil { @@ -637,7 +798,7 @@ func (c *backupController) runBackup(backup *pkgbackup.Request) error { fatalErrs = append(fatalErrs, err) } - // Empty slices here so that they can be passed in to the persistBackup call later, regardless of whether or not CSI's enabled. + // Empty slices here so that they can be passed in to the persistBackupBeforeUploadComplete call later, regardless of whether or not CSI's enabled. // This way, we only make the Lister call if the feature flag's on. var volumeSnapshots []*snapshotv1api.VolumeSnapshot var volumeSnapshotContents []*snapshotv1api.VolumeSnapshotContent @@ -690,10 +851,6 @@ func (c *backupController) runBackup(backup *pkgbackup.Request) error { } - // Mark completion timestamp before serializing and uploading. - // Otherwise, the JSON file in object storage has a CompletionTimestamp of 'null'. - backup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} - backup.Status.VolumeSnapshotsAttempted = len(backup.VolumeSnapshots) for _, snap := range backup.VolumeSnapshots { if snap.Status.Phase == volume.SnapshotPhaseCompleted { @@ -738,17 +895,100 @@ func (c *backupController) runBackup(backup *pkgbackup.Request) error { return err } - if errs := persistBackup(backup, backupFile, logFile, backupStore, c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)), volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses); len(errs) > 0 { + if errs := persistBackupBeforeUploadComplete(backup, backupFile, logFile, backupStore, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses); len(errs) > 0 { fatalErrs = append(fatalErrs, errs...) } - c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup completed") + snapshotInProgress := false + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + snapshotInProgress = checkAndUpdateItemSnapshotsProgress(backup.ItemSnapshots, logger) + } + + if snapshotInProgress { + // Prepare for upload progress monitoring + switch { + case len(fatalErrs) > 0: + // If we had fatal errors in the backup, we will just fail here and not monitor upload progress + // ItemSnapshotsAttempted/Completed may be incorrect + backup.Status.Phase = velerov1api.BackupPhaseFailed + case logCounter.GetCount(logrus.ErrorLevel) > 0: + backup.Status.Phase = velerov1api.BackupPhaseUploadingPartialFailure + c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup partially failed but snapshot uploads outstanding, will complete in background") + default: + backup.Status.Phase = velerov1api.BackupPhaseUploading + c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Snapshot uploads outstanding, will complete in background") + } + } else { + updateBackupStatusForSnapshotState(backup, c.logger) + + switch { + case len(fatalErrs) > 0: + backup.Status.Phase = velerov1api.BackupPhaseFailed + case logCounter.GetCount(logrus.ErrorLevel) > 0: + backup.Status.Phase = velerov1api.BackupPhasePartiallyFailed + default: + backup.Status.Phase = velerov1api.BackupPhaseCompleted + } + } + if backup.Status.Phase != velerov1api.BackupPhaseUploading && + backup.Status.Phase != velerov1api.BackupPhaseUploadingPartialFailure { + // We're done so set the CompletionTimestamp now + backup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} + recordBackupDurationMetric(backup.Backup, c.metrics) + if errs := persistBackupFinal(backup, backupStore); len(errs) > 0 { + fatalErrs = append(fatalErrs, errs...) + } + c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup completed") + } else { + c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup completed, snapshots uploading in the background") + } // if we return a non-nil error, the calling function will update // the backup's phase to Failed. return kerrors.NewAggregate(fatalErrs) } +// Checks the list of snapshots to see if they have completed by calling the appropriate ItemSnapshotter. Returns +// true if any snapshots are still in progress. Updates status for snapshots that have completed or failed +func checkAndUpdateItemSnapshotsProgress(itemSnapshots []*pkgbackup.ItemSnapshotInfo, logger logrus.FieldLogger) bool { + snapshotsInProgress := false + for _, checkSnapshot := range itemSnapshots { + if checkSnapshot.Status.Phase == isv1.SnapshotPhaseInProgress { + sipi := isv1.ProgressInput{ + ItemID: checkSnapshot.Spec.ResourceIdentifier, + SnapshotID: checkSnapshot.Status.ProviderSnapshotID, + Backup: checkSnapshot.Spec.Backup, + } + log := logger.WithFields(logrus.Fields{ + "backup": kubeutil.NamespaceAndName(checkSnapshot.Spec.Backup), + "item": checkSnapshot.Spec.ResourceIdentifier, + "snapshotID": checkSnapshot.Status.ProviderSnapshotID}) + up, err := checkSnapshot.ItemSnapshotter.Progress(&sipi) + if err != nil { + // If we can't retrieve the status, mark the snapshot as failed. Might be worthwhile later to retry some + // number of times before failing + checkSnapshot.Status.Phase = isv1.SnapshotPhaseFailed + checkSnapshot.Status.Error = fmt.Sprintf("Could not retrieve snapshot progress err = %v", err) + log.WithError(err).Error("Error retrieving snapshot progress") + } else { + checkSnapshot.Status.Phase = up.Phase + switch checkSnapshot.Status.Phase { + case isv1.SnapshotPhaseInProgress: + snapshotsInProgress = true + case isv1.SnapshotPhaseCompleted: + // Do nothing + case isv1.SnapshotPhaseFailed: + checkSnapshot.Status.Error = up.Err + log.Errorf("Snapshot upload failed err=%s", checkSnapshot.Status.Error) + // We will fail the backup when we check the statuses of all the snapshots later + } + } + } + } + return snapshotsInProgress +} + +// Record backup metrics including tarball size, and volume snapshot attempts and failures func recordBackupMetrics(log logrus.FieldLogger, backup *velerov1api.Backup, backupFile *os.File, serverMetrics *metrics.ServerMetrics) { backupScheduleName := backup.GetLabels()[velerov1api.ScheduleNameLabel] @@ -760,9 +1000,6 @@ func recordBackupMetrics(log logrus.FieldLogger, backup *velerov1api.Backup, bac } serverMetrics.SetBackupTarballSizeBytesGauge(backupScheduleName, backupSizeBytes) - backupDuration := backup.Status.CompletionTimestamp.Time.Sub(backup.Status.StartTimestamp.Time) - backupDurationSeconds := float64(backupDuration / time.Second) - serverMetrics.RegisterBackupDuration(backupScheduleName, backupDurationSeconds) serverMetrics.RegisterVolumeSnapshotAttempts(backupScheduleName, backup.Status.VolumeSnapshotsAttempted) serverMetrics.RegisterVolumeSnapshotSuccesses(backupScheduleName, backup.Status.VolumeSnapshotsCompleted) serverMetrics.RegisterVolumeSnapshotFailures(backupScheduleName, backup.Status.VolumeSnapshotsAttempted-backup.Status.VolumeSnapshotsCompleted) @@ -779,20 +1016,29 @@ func recordBackupMetrics(log logrus.FieldLogger, backup *velerov1api.Backup, bac serverMetrics.RegisterBackupItemsErrorsGauge(backupScheduleName, backup.Status.Errors) } -func persistBackup(backup *pkgbackup.Request, +// Record the final backup metrics including backup duration and item snapshot metrics. These are handled +// separately because they will not be available until all uploads have completed. + +func recordBackupDurationMetric(backup *velerov1api.Backup, serverMetrics *metrics.ServerMetrics) { + backupScheduleName := backup.GetLabels()[velerov1api.ScheduleNameLabel] + backupDuration := backup.Status.CompletionTimestamp.Time.Sub(backup.Status.StartTimestamp.Time) + backupDurationSeconds := float64(backupDuration / time.Second) + serverMetrics.RegisterBackupDuration(backupScheduleName, backupDurationSeconds) + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + serverMetrics.RegisterItemSnapshotAttempts(backupScheduleName, backup.Status.ItemSnapshotsAttempted) + serverMetrics.RegisterItemSnapshotSuccesses(backupScheduleName, backup.Status.ItemSnapshotsCompleted) + serverMetrics.RegisterItemSnapshotFailures(backupScheduleName, backup.Status.ItemSnapshotsAttempted-backup.Status.ItemSnapshotsCompleted) + } +} + +func persistBackupBeforeUploadComplete(backup *pkgbackup.Request, backupContents, backupLog *os.File, backupStore persistence.BackupStore, - log logrus.FieldLogger, csiVolumeSnapshots []*snapshotv1api.VolumeSnapshot, csiVolumeSnapshotContents []*snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []*snapshotv1api.VolumeSnapshotClass, ) []error { persistErrs := []error{} - backupJSON := new(bytes.Buffer) - - if err := encode.EncodeTo(backup.Backup, "json", backupJSON); err != nil { - persistErrs = append(persistErrs, errors.Wrap(err, "error encoding backup")) - } // Velero-native volume snapshots (as opposed to CSI ones) nativeVolumeSnapshots, errs := encodeToJSONGzip(backup.VolumeSnapshots, "native volumesnapshots list") @@ -826,7 +1072,6 @@ func persistBackup(backup *pkgbackup.Request, if len(persistErrs) > 0 { // Don't upload the JSON files or backup tarball if encoding to json fails. - backupJSON = nil backupContents = nil nativeVolumeSnapshots = nil backupResourceList = nil @@ -837,7 +1082,6 @@ func persistBackup(backup *pkgbackup.Request, backupInfo := persistence.BackupInfo{ Name: backup.Name, - Metadata: backupJSON, Contents: backupContents, Log: backupLog, PodVolumeBackups: podVolumeBackups, @@ -854,6 +1098,43 @@ func persistBackup(backup *pkgbackup.Request, return persistErrs } +func persistBackupFinal(backup *pkgbackup.Request, backupStore persistence.BackupStore) []error { + persistErrs := []error{} + + backupJSON := new(bytes.Buffer) + + if err := encode.EncodeTo(backup.Backup, "json", backupJSON); err != nil { + persistErrs = append(persistErrs, errors.Wrap(err, "error encoding backup")) + } + + // Item snapshots - CSI and other Astrolabe objects are here + extItemSnapshots := []volume.ItemSnapshot{} + for _, isi := range backup.ItemSnapshots { + extIS, err := isi.GetItemSnapshot() + if err != nil { + persistErrs = append(persistErrs, err) + } else { + extItemSnapshots = append(extItemSnapshots, extIS) + } + } + itemSnapshots, errs := encodeToJSONGzip(extItemSnapshots, "item snapshots list") + if errs != nil { + persistErrs = append(persistErrs, errs...) + } + + if len(persistErrs) == 0 { + + backupInfo := persistence.BackupInfo{ + Name: backup.Name, + Metadata: backupJSON, + ItemSnapshots: itemSnapshots, + } + if err := backupStore.PutBackup(backupInfo); err != nil { + persistErrs = append(persistErrs, err) + } + } + return persistErrs +} func closeAndRemoveFile(file *os.File, log logrus.FieldLogger) { if file == nil { log.Debug("Skipping removal of file due to nil file pointer") diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index fa1a95a382..e93a87fb97 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -865,12 +865,19 @@ func TestProcessBackupCompletions(t *testing.T) { backupStore.On("BackupExists", test.backupLocation.Spec.StorageType.ObjectStorage.Bucket, test.backup.Name).Return(test.backupExists, test.existenceCheckError) // Ensure we have a CompletionTimestamp when uploading and that the backup name matches the backup in the object store. - // Failures will display the bytes in buf. + // Failures will display the bytes in buf. Only check this on the second PutBackup because the first will + // be before uploads have completed and will not have metadata + putBackupCalledAlready := false + hasNameAndCompletionTimestamp := func(info persistence.BackupInfo) bool { - buf := new(bytes.Buffer) - buf.ReadFrom(info.Metadata) - return info.Name == test.backup.Name && - strings.Contains(buf.String(), `"completionTimestamp": "2006-01-02T22:04:05Z"`) + if putBackupCalledAlready { + buf := new(bytes.Buffer) + buf.ReadFrom(info.Metadata) + return info.Name == test.backup.Name && + strings.Contains(buf.String(), `"completionTimestamp": "2006-01-02T22:04:05Z"`) + } + putBackupCalledAlready = true + return true } backupStore.On("PutBackup", mock.MatchedBy(hasNameAndCompletionTimestamp)).Return(nil) diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 67c53dd7b4..2bbd43f4fd 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -22,6 +22,11 @@ import ( "fmt" "time" + "github.com/vmware-tanzu/velero/pkg/plugin/framework" + "github.com/vmware-tanzu/velero/pkg/volume" + + "github.com/vmware-tanzu/velero/pkg/features" + jsonpatch "github.com/evanphx/json-patch" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -41,6 +46,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/filesystem" "github.com/vmware-tanzu/velero/pkg/util/kube" @@ -240,30 +246,53 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, errors.Wrap(err, "error getting the backup store") } - actions, err := pluginManager.GetDeleteItemActions() - log.Debugf("%d actions before invoking actions", len(actions)) + deleteItemActions, err := pluginManager.GetDeleteItemActions() + log.Debugf("%d deleteItemActions before invoking deleteItemActions", len(deleteItemActions)) if err != nil { return ctrl.Result{}, errors.Wrap(err, "error getting delete item actions") } - // don't defer CleanupClients here, since it was already called above. + deleteItemActionResolver := framework.NewDeleteItemActionResolver(deleteItemActions) + + var itemSnapshots []*volume.ItemSnapshot + var itemSnapshotters []isv1.ItemSnapshotter + + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + itemSnapshots, err = backupStore.GetItemSnapshots(backup.Name) + if err != nil { + errs = append(errs, errors.Wrap(err, "error getting backup's item itemSnapshots").Error()) + } + itemSnapshotters, err = pluginManager.GetItemSnapshotters() + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "error getting item snapshotters") + } + } - if len(actions) > 0 { + if len(deleteItemActions) > 0 || len(itemSnapshots) > 0 { + var deleteItemResolvedActions []framework.DeleteItemResolvedAction // Download the tarball backupFile, err := downloadToTempFile(backup.Name, backupStore, log) if err != nil { log.WithError(err).Errorf("Unable to download tarball for backup %s, skipping associated DeleteItemAction plugins", backup.Name) } else { - defer closeAndRemoveFile(backupFile, r.logger) - ctx := &delete.Context{ - Backup: backup, - BackupReader: backupFile, - Actions: actions, - Log: r.logger, - DiscoveryHelper: r.discoveryHelper, - Filesystem: filesystem.NewFileSystem(), + defer closeAndRemoveFile(backupFile, c.logger) + + deleteItemResolvedActions, err = deleteItemActionResolver.ResolveActions(c.helper) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "error resolving delete item deleteItemActions") } + ctx := &delete.Context{ + Backup: backup, + BackupReader: backupFile, + Actions: actions, + Log: c.logger, + DiscoveryHelper: c.helper, + Filesystem: filesystem.NewFileSystem(), + DeleteItemResolvedActions: deleteItemResolvedActions, + ItemSnapshots: itemSnapshots, + ItemSnapshotters: itemSnapshotters, + } // Optimization: wrap in a gofunc? Would be useful for large backups with lots of objects. // but what do we do with the error returned? We can't just swallow it as that may lead to dangling resources. err = delete.InvokeDeleteActions(ctx) diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index d2de589b24..f3416ce21f 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -19,6 +19,9 @@ package controller import ( "bytes" "fmt" + + "io/ioutil" + "testing" "time" "context" @@ -40,19 +43,15 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/vmware-tanzu/velero/pkg/plugin/velero" - "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks" - "github.com/vmware-tanzu/velero/pkg/volume" - - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" - "github.com/vmware-tanzu/velero/pkg/builder" informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions" "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" + "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks" velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/volume" ) type backupDeletionControllerTestData struct { @@ -125,6 +124,7 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { assert.True(t, strings.HasPrefix(err.Error(), "error getting the backup store")) }) +func TestBackupDeletionControllerProcessRequest(t *testing.T) { t.Run("missing spec.backupName", func(t *testing.T) { dbr := defaultTestDbr() dbr.Spec.BackupName = "" @@ -147,7 +147,11 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { // add the backup to the tracker so the execution of reconcile doesn't progress // past checking for an in-progress backup. this makes validation easier. - td.controller.backupTracker.Add(td.req.Namespace, input.Spec.BackupName) + + td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName, &pkgbackup.Request{}) + + require.NoError(t, td.sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(td.req)) + existing := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: td.req.Namespace, @@ -197,6 +201,8 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { dbr := defaultTestDbr() td := setupBackupDeletionControllerTest(t, dbr) + td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName, &pkgbackup.Request{}) + td.controller.backupTracker.Add(td.req.Namespace, dbr.Spec.BackupName) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) diff --git a/pkg/controller/backup_tracker.go b/pkg/controller/backup_tracker.go index 01b3ec25f0..1217cbaa14 100644 --- a/pkg/controller/backup_tracker.go +++ b/pkg/controller/backup_tracker.go @@ -20,50 +20,78 @@ import ( "fmt" "sync" - "k8s.io/apimachinery/pkg/util/sets" + v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" ) // BackupTracker keeps track of in-progress backups. type BackupTracker interface { // Add informs the tracker that a backup is in progress. - Add(ns, name string) + Add(ns, name string, backup *pkgbackup.Request) // Delete informs the tracker that a backup is no longer in progress. Delete(ns, name string) // Contains returns true if the tracker is tracking the backup. Contains(ns, name string) bool + // Get returns the backup for the given namespace/name + Get(ns, name string) *pkgbackup.Request + // GetByPhase returns all backups with the given phase + GetByPhase(phase v1.BackupPhase) []*pkgbackup.Request } type backupTracker struct { lock sync.RWMutex - backups sets.String + backups map[string]*pkgbackup.Request } // NewBackupTracker returns a new BackupTracker. func NewBackupTracker() BackupTracker { return &backupTracker{ - backups: sets.NewString(), + backups: map[string]*pkgbackup.Request{}, } } -func (bt *backupTracker) Add(ns, name string) { +func (bt *backupTracker) Add(ns, name string, backup *pkgbackup.Request) { bt.lock.Lock() defer bt.lock.Unlock() - bt.backups.Insert(backupTrackerKey(ns, name)) + if backup == nil { + panic("Cannot add a nil backup to BackupTracker") + } + bt.backups[backupTrackerKey(ns, name)] = backup } func (bt *backupTracker) Delete(ns, name string) { bt.lock.Lock() defer bt.lock.Unlock() - bt.backups.Delete(backupTrackerKey(ns, name)) + delete(bt.backups, backupTrackerKey(ns, name)) } func (bt *backupTracker) Contains(ns, name string) bool { bt.lock.RLock() defer bt.lock.RUnlock() - return bt.backups.Has(backupTrackerKey(ns, name)) + return bt.backups[backupTrackerKey(ns, name)] != nil +} + +func (bt *backupTracker) Get(ns, name string) *pkgbackup.Request { + bt.lock.RLock() + defer bt.lock.RUnlock() + + return bt.backups[backupTrackerKey(ns, name)] +} + +func (bt *backupTracker) GetByPhase(phase v1.BackupPhase) []*pkgbackup.Request { + bt.lock.RLock() + defer bt.lock.RUnlock() + + foundBackups := []*pkgbackup.Request{} + for _, backup := range bt.backups { + if backup.Status.Phase == phase { + foundBackups = append(foundBackups, backup) + } + } + return foundBackups } func backupTrackerKey(ns, name string) string { diff --git a/pkg/controller/backup_tracker_test.go b/pkg/controller/backup_tracker_test.go index 40d5f3b477..d77abb1d3c 100644 --- a/pkg/controller/backup_tracker_test.go +++ b/pkg/controller/backup_tracker_test.go @@ -19,6 +19,11 @@ package controller import ( "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/backup" + "github.com/stretchr/testify/assert" ) @@ -27,17 +32,48 @@ func TestBackupTracker(t *testing.T) { assert.False(t, bt.Contains("ns", "name")) - bt.Add("ns", "name") + name1Backup := &backup.Request{ + Backup: &velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "name"}, + }, + } + bt.Add("ns", "name", name1Backup) assert.True(t, bt.Contains("ns", "name")) + assert.Equal(t, name1Backup, bt.Get("ns", "name")) + + name2Backup := &backup.Request{ + Backup: &velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "name2"}, + }, + } - bt.Add("ns2", "name2") + bt.Add("ns2", "name2", name2Backup) assert.True(t, bt.Contains("ns", "name")) assert.True(t, bt.Contains("ns2", "name2")) + assert.Equal(t, name1Backup, bt.Get("ns", "name")) + assert.Equal(t, name2Backup, bt.Get("ns2", "name2")) + bt.Delete("ns", "name") assert.False(t, bt.Contains("ns", "name")) assert.True(t, bt.Contains("ns2", "name2")) bt.Delete("ns2", "name2") assert.False(t, bt.Contains("ns2", "name2")) + + assert.Equal(t, 0, len(bt.GetByPhase(velerov1api.BackupPhaseUploading))) + name3PhaseUploading := &backup.Request{ + Backup: &velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "name3"}, + Status: velerov1api.BackupStatus{ + Phase: velerov1api.BackupPhaseUploading, + }, + }, + } + bt.Add("ns", "name3", name3PhaseUploading) + reqs := bt.GetByPhase(velerov1api.BackupPhaseUploading) + assert.Equal(t, 1, len(reqs)) + assert.Equal(t, reqs[0].Name, "name3") + assert.Equal(t, velerov1api.BackupPhaseUploading, reqs[0].Status.Phase) + } diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 910bd56cd7..9b04443cea 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -28,6 +28,10 @@ import ( "sort" "time" + "github.com/vmware-tanzu/velero/pkg/features" + v1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + "github.com/vmware-tanzu/velero/pkg/volume" + jsonpatch "github.com/evanphx/json-patch" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -462,13 +466,16 @@ func (c *restoreController) runValidatedRestore(restore *api.Restore, info backu if err != nil { return errors.Wrap(err, "error getting restore item actions") } - actionsResolver := framework.NewRestoreItemActionResolver(actions) + restoreItemActionResolver := framework.NewRestoreItemActionResolver(actions) - itemSnapshotters, err := pluginManager.GetItemSnapshotters() - if err != nil { - return errors.Wrap(err, "error getting item snapshotters") + var itemSnapshotters []v1.ItemSnapshotter + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + itemSnapshotters, err = pluginManager.GetItemSnapshotters() + if err != nil { + return errors.Wrap(err, "error getting item snapshotters") + } } - snapshotItemResolver := framework.NewItemSnapshotterResolver(itemSnapshotters) + itemSnapshotterResolver := framework.NewItemSnapshotterResolver(itemSnapshotters) backupFile, err := downloadToTempFile(restore.Spec.BackupName, info.backupStore, restoreLog) if err != nil { @@ -488,6 +495,14 @@ func (c *restoreController) runValidatedRestore(restore *api.Restore, info backu return errors.Wrap(err, "error fetching volume snapshots metadata") } + var itemSnapshots []*volume.ItemSnapshot + + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + itemSnapshots, err = info.backupStore.GetItemSnapshots(restore.Spec.BackupName) + if err != nil { + return errors.Wrap(err, "error fetching item snapshots metadata") + } + } restoreLog.Info("starting restore") var podVolumeBackups []*velerov1api.PodVolumeBackup @@ -498,11 +513,12 @@ func (c *restoreController) runValidatedRestore(restore *api.Restore, info backu Log: restoreLog, Restore: restore, Backup: info.backup, + ItemSnapshots: itemSnapshots, PodVolumeBackups: podVolumeBackups, VolumeSnapshots: volumeSnapshots, BackupReader: backupFile, } - restoreWarnings, restoreErrors := c.restorer.RestoreWithResolvers(restoreReq, actionsResolver, snapshotItemResolver, + restoreWarnings, restoreErrors := c.restorer.RestoreWithResolvers(restoreReq, restoreItemActionResolver, itemSnapshotterResolver, c.snapshotLocationLister, pluginManager) // log errors and warnings to the restore log diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d486a88bd6..f8cf3eb58c 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -57,6 +57,9 @@ const ( csiSnapshotAttemptTotal = "csi_snapshot_attempt_total" csiSnapshotSuccessTotal = "csi_snapshot_success_total" csiSnapshotFailureTotal = "csi_snapshot_failure_total" + itemSnapshotAttemptTotal = "item_snapshot_attempt_total" + itemSnapshotSuccessTotal = "item_snapshot_success_total" + itemSnapshotFailureTotal = "item_snapshot_failure_total" // Restic metrics podVolumeBackupEnqueueTotal = "pod_volume_backup_enqueue_count" @@ -648,3 +651,24 @@ func (m *ServerMetrics) RegisterCSISnapshotFailures(backupSchedule, backupName s c.WithLabelValues(backupSchedule, backupName).Add(float64(csiSnapshotsFailed)) } } + +// RegisterItemSnapshotAttempts records attempts to snapshot items +func (m *ServerMetrics) RegisterItemSnapshotAttempts(backupSchedule string, itemSnapshotsAttempted int) { + if c, ok := m.metrics[itemSnapshotAttemptTotal].(*prometheus.CounterVec); ok { + c.WithLabelValues(backupSchedule).Add(float64(itemSnapshotsAttempted)) + } +} + +// RegisterItemSnapshotSuccesses records successful item snapshots (uploads may happen separately) +func (m *ServerMetrics) RegisterItemSnapshotSuccesses(backupSchedule string, itemSnapshotsCompleted int) { + if c, ok := m.metrics[itemSnapshotSuccessTotal].(*prometheus.CounterVec); ok { + c.WithLabelValues(backupSchedule).Add(float64(itemSnapshotsCompleted)) + } +} + +// RegisterItemSnapshotFailures records item snapshot failures +func (m *ServerMetrics) RegisterItemSnapshotFailures(backupSchedule string, itemSnapshotFailures int) { + if c, ok := m.metrics[itemSnapshotFailureTotal].(*prometheus.CounterVec); ok { + c.WithLabelValues(backupSchedule).Add(float64(itemSnapshotFailures)) + } +} diff --git a/pkg/plugin/clientmgmt/manager.go b/pkg/plugin/clientmgmt/manager.go index 9dc2ff3274..861f9ade06 100644 --- a/pkg/plugin/clientmgmt/manager.go +++ b/pkg/plugin/clientmgmt/manager.go @@ -20,7 +20,7 @@ import ( "strings" "sync" - v1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/sirupsen/logrus" @@ -55,10 +55,10 @@ type Manager interface { GetDeleteItemAction(name string) (velero.DeleteItemAction, error) // GetItemSnapshotter returns the item snapshotter plugin for name - GetItemSnapshotter(name string) (v1.ItemSnapshotter, error) + GetItemSnapshotter(name string) (isv1.ItemSnapshotter, error) // GetItemSnapshotters returns all item snapshotter plugins - GetItemSnapshotters() ([]v1.ItemSnapshotter, error) + GetItemSnapshotters() ([]isv1.ItemSnapshotter, error) // CleanupClients terminates all of the Manager's running plugin processes. CleanupClients() @@ -264,7 +264,7 @@ func (m *manager) GetDeleteItemAction(name string) (velero.DeleteItemAction, err return r, nil } -func (m *manager) GetItemSnapshotter(name string) (v1.ItemSnapshotter, error) { +func (m *manager) GetItemSnapshotter(name string) (isv1.ItemSnapshotter, error) { name = sanitizeName(name) restartableProcess, err := m.getRestartableProcess(framework.PluginKindItemSnapshotter, name) @@ -276,10 +276,10 @@ func (m *manager) GetItemSnapshotter(name string) (v1.ItemSnapshotter, error) { return r, nil } -func (m *manager) GetItemSnapshotters() ([]v1.ItemSnapshotter, error) { +func (m *manager) GetItemSnapshotters() ([]isv1.ItemSnapshotter, error) { list := m.registry.List(framework.PluginKindItemSnapshotter) - actions := make([]v1.ItemSnapshotter, 0, len(list)) + actions := make([]isv1.ItemSnapshotter, 0, len(list)) for i := range list { id := list[i] diff --git a/pkg/plugin/clientmgmt/registry.go b/pkg/plugin/clientmgmt/registry.go index fe8a7f4b44..f1765ebb68 100644 --- a/pkg/plugin/clientmgmt/registry.go +++ b/pkg/plugin/clientmgmt/registry.go @@ -21,6 +21,11 @@ import ( "os" "path/filepath" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/features" + + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -92,6 +97,50 @@ func (r *registry) discoverPlugins(commands []string) error { return err } + // For backwards compatibility, some plugins will export both ItemSnapshotter plugins and + // BackupItemActions for Persistent Volume Claims (CSI and vSphere plugin do this). Look for + // ItemSnapshotters for PVCs and if found, discard the BackupItemActions. We leave alternates like + // RestoreItemActions and DeleteItemActions as well as VolumeSnapshotters to handle restores from old backups. + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { + updatedPlugins := plugins // Default list is everything + for _, plugin := range plugins { + if plugin.Kind == framework.PluginKindItemSnapshotter { + updatedPlugins = []framework.PluginIdentifier{} // We'll build a new list and skip any BackupItemActions for PVCs + appliesTo, err := r.appliesTo(command, plugin) + if err != nil { + return err + } + for _, includedResource := range appliesTo.IncludedResources { + if includedResource == "persistentvolumeclaims" { + for _, plugin := range plugins { + use := true + if plugin.Kind == framework.PluginKindBackupItemAction { + biaAppliesTo, err := r.appliesTo(command, plugin) + if err != nil { + return err + } + for _, biaResource := range biaAppliesTo.IncludedResources { + if biaResource == "persistentvolumeclaims" { + r.logger.WithFields(logrus.Fields{ + "kind": plugin.Kind, + "name": plugin.Name, + "command": command, + }).Info("Plugin exports ItemSnapshotter and BackupItemAction, only using ItemSnapshotter") + use = false + } + } + } + if use { + updatedPlugins = append(updatedPlugins, plugin) + } + } + break // Once we've found persistentvolumeclaims we won't find it again + } + } + } + } + plugins = updatedPlugins + } for _, plugin := range plugins { r.logger.WithFields(logrus.Fields{ "kind": plugin.Kind, @@ -195,6 +244,30 @@ func (r *registry) listPlugins(command string) ([]framework.PluginIdentifier, er return lister.ListPlugins() } +// appliesTo returns the list of ResourceSelectors that this plugin can be applied to +func (r *registry) appliesTo(command string, pluginID framework.PluginIdentifier) (velero.ResourceSelector, error) { + process, err := r.processFactory.newProcess(command, r.logger, r.logLevel) + if err != nil { + return velero.ResourceSelector{}, err + } + defer process.kill() + + plugin, err := process.dispense(kindAndName{ + kind: pluginID.Kind, + name: pluginID.Name, + }) + if err != nil { + return velero.ResourceSelector{}, err + } + + applicable, ok := plugin.(velero.Applicable) + if !ok { + return velero.ResourceSelector{}, errors.Errorf("%T does not implement AppliesTo", plugin) + } + + return applicable.AppliesTo() +} + // register registers a PluginIdentifier with the registry. func (r *registry) register(id framework.PluginIdentifier) error { key := kindAndName{kind: id.Kind, name: id.Name} diff --git a/pkg/plugin/clientmgmt/restartable_item_snapshotter.go b/pkg/plugin/clientmgmt/restartable_item_snapshotter.go index e211bcf28e..abdffe1e61 100644 --- a/pkg/plugin/clientmgmt/restartable_item_snapshotter.go +++ b/pkg/plugin/clientmgmt/restartable_item_snapshotter.go @@ -27,6 +27,13 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/velero" ) +// LocalItemSnapshotter is used so we can identify plugins by name internally. This is defined locally so that we +// don't need to add it to the GRPC APIs or do a GRPC call to retrieve +type LocalItemSnapshotter interface { + GetName() string + isv1.ItemSnapshotter +} + type restartableItemSnapshotter struct { key kindAndName sharedPluginProcess RestartableProcess @@ -41,6 +48,10 @@ func newRestartableItemSnapshotter(name string, sharedPluginProcess RestartableP return r } +func (r *restartableItemSnapshotter) GetName() string { + return r.key.name +} + // getItemSnapshotter returns the item snapshotter for this restartableItemSnapshotter. It does *not* restart the // plugin process. func (r *restartableItemSnapshotter) getItemSnapshotter() (isv1.ItemSnapshotter, error) { diff --git a/pkg/plugin/clientmgmt/util.go b/pkg/plugin/clientmgmt/util.go new file mode 100644 index 0000000000..6615f852e2 --- /dev/null +++ b/pkg/plugin/clientmgmt/util.go @@ -0,0 +1,32 @@ +package clientmgmt + +import ( + "github.com/vmware-tanzu/velero/pkg/plugin/framework" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + "github.com/vmware-tanzu/velero/pkg/volume" +) + +// ItemSnapshotterForSnapshotFromResolved - Gets the ItemSnapshotter that was used to take a snapshot. We have two for different array types for convenience since +// Go doesn't allow us to cast arrays easily +func ItemSnapshotterForSnapshotFromResolved(itemSnapshot *volume.ItemSnapshot, resolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction) isv1.ItemSnapshotter { + var itemSnapshotters []isv1.ItemSnapshotter + for _, resolvedAction := range resolvedItemSnapshotters { + itemSnapshotter, ok := resolvedAction.ItemSnapshotter.(LocalItemSnapshotter) + if ok { + itemSnapshotters = append(itemSnapshotters, itemSnapshotter) + } + } + return ItemSnapshotterForSnapshot(itemSnapshot, itemSnapshotters) +} + +func ItemSnapshotterForSnapshot(itemSnapshot *volume.ItemSnapshot, itemSnapshotters []isv1.ItemSnapshotter) isv1.ItemSnapshotter { + for _, checkItemSnapshotter := range itemSnapshotters { + localCheckItemSnapshotter, ok := checkItemSnapshotter.(LocalItemSnapshotter) + if ok { + if itemSnapshot.Spec.ItemSnapshotter == localCheckItemSnapshotter.GetName() { + return checkItemSnapshotter + } + } + } + return nil +} diff --git a/pkg/plugin/framework/backup_item_action_server.go b/pkg/plugin/framework/backup_item_action_server.go index fb4b8a6af6..42eafe1f37 100644 --- a/pkg/plugin/framework/backup_item_action_server.go +++ b/pkg/plugin/framework/backup_item_action_server.go @@ -120,17 +120,8 @@ func (s *BackupItemActionGRPCServer) Execute(ctx context.Context, req *proto.Exe } for _, item := range additionalItems { - res.AdditionalItems = append(res.AdditionalItems, backupResourceIdentifierToProto(item)) + res.AdditionalItems = append(res.AdditionalItems, resourceIdentifierToProto(item)) } return res, nil } - -func backupResourceIdentifierToProto(id velero.ResourceIdentifier) *proto.ResourceIdentifier { - return &proto.ResourceIdentifier{ - Group: id.Group, - Resource: id.Resource, - Namespace: id.Namespace, - Name: id.Name, - } -} diff --git a/pkg/plugin/framework/backup_item_action_test.go b/pkg/plugin/framework/backup_item_action_test.go index 7d41e0e781..edc1bb43c6 100644 --- a/pkg/plugin/framework/backup_item_action_test.go +++ b/pkg/plugin/framework/backup_item_action_test.go @@ -191,7 +191,7 @@ func TestBackupItemActionGRPCServerExecute(t *testing.T) { // Verify additional items var expectedAdditionalItems []*proto.ResourceIdentifier for _, item := range test.implAdditionalItems { - expectedAdditionalItems = append(expectedAdditionalItems, backupResourceIdentifierToProto(item)) + expectedAdditionalItems = append(expectedAdditionalItems, resourceIdentifierToProto(item)) } assert.Equal(t, expectedAdditionalItems, resp.AdditionalItems) }) diff --git a/pkg/plugin/framework/item_snapshotter_client.go b/pkg/plugin/framework/item_snapshotter_client.go index dd341b9662..630a5858a7 100644 --- a/pkg/plugin/framework/item_snapshotter_client.go +++ b/pkg/plugin/framework/item_snapshotter_client.go @@ -183,12 +183,18 @@ func (recv ItemSnapshotterGRPCClient) Progress(input *isv1.ProgressInput) (*isv1 } func (recv ItemSnapshotterGRPCClient) DeleteSnapshot(ctx context.Context, input *isv1.DeleteSnapshotInput) error { + itemJSON, err := json.Marshal(input.ItemFromBackup.UnstructuredContent()) + if err != nil { + return errors.WithStack(err) + } + req := &proto.DeleteItemSnapshotRequest{ - Plugin: recv.plugin, - Params: input.Params, - SnapshotID: input.SnapshotID, + Plugin: recv.plugin, + ItemFromBackup: itemJSON, + Params: input.Params, + SnapshotID: input.SnapshotID, } - _, err := recv.grpcClient.DeleteSnapshot(ctx, req) // Returns Empty as first arg so just ignore + _, err = recv.grpcClient.DeleteSnapshot(ctx, req) // Returns Empty as first arg so just ignore if err != nil { return errors.WithStack(err) diff --git a/pkg/plugin/velero/item_snapshotter/v1/item_snapshotter.go b/pkg/plugin/velero/item_snapshotter/v1/item_snapshotter.go index 7d9ddcd00c..d97abcbc42 100644 --- a/pkg/plugin/velero/item_snapshotter/v1/item_snapshotter.go +++ b/pkg/plugin/velero/item_snapshotter/v1/item_snapshotter.go @@ -192,6 +192,9 @@ type ItemSnapshotter interface { // DeleteSnapshot removes a snapshot DeleteSnapshot(ctx context.Context, input *DeleteSnapshotInput) error - // CreateItemFromSnapshot creates a new item from the snapshot + // CreateItemFromSnapshot creates a new item from the snapshot. The item to restore from the + // backup is passed in and may be modified during CreateItemFromSnapshot and the modified item + // returned in CreateItemOutput. This allows operations such as restoring a PVC to return a PVC + // that points to a newly created PV. AdditionalItems to restore may also be returned in CreateItemOutput CreateItemFromSnapshot(ctx context.Context, input *CreateItemInput) (*CreateItemOutput, error) } diff --git a/pkg/plugin/velero/item_snapshotter/v1/mocks/item_snapshotter.go b/pkg/plugin/velero/item_snapshotter/v1/mocks/item_snapshotter.go index 940b854261..87c7849eb8 100644 --- a/pkg/plugin/velero/item_snapshotter/v1/mocks/item_snapshotter.go +++ b/pkg/plugin/velero/item_snapshotter/v1/mocks/item_snapshotter.go @@ -5,7 +5,7 @@ package mocks import ( context "context" mock "github.com/stretchr/testify/mock" - v1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" velero "github.com/vmware-tanzu/velero/pkg/plugin/velero" ) @@ -16,11 +16,11 @@ type ItemSnapshotter struct { } // AlsoHandles provides a mock function with given fields: input -func (_m *ItemSnapshotter) AlsoHandles(input *v1.AlsoHandlesInput) ([]velero.ResourceIdentifier, error) { +func (_m *ItemSnapshotter) AlsoHandles(input *isv1.AlsoHandlesInput) ([]velero.ResourceIdentifier, error) { ret := _m.Called(input) var r0 []velero.ResourceIdentifier - if rf, ok := ret.Get(0).(func(*v1.AlsoHandlesInput) []velero.ResourceIdentifier); ok { + if rf, ok := ret.Get(0).(func(*isv1.AlsoHandlesInput) []velero.ResourceIdentifier); ok { r0 = rf(input) } else { if ret.Get(0) != nil { @@ -29,7 +29,7 @@ func (_m *ItemSnapshotter) AlsoHandles(input *v1.AlsoHandlesInput) ([]velero.Res } var r1 error - if rf, ok := ret.Get(1).(func(*v1.AlsoHandlesInput) error); ok { + if rf, ok := ret.Get(1).(func(*isv1.AlsoHandlesInput) error); ok { r1 = rf(input) } else { r1 = ret.Error(1) @@ -60,20 +60,20 @@ func (_m *ItemSnapshotter) AppliesTo() (velero.ResourceSelector, error) { } // CreateItemFromSnapshot provides a mock function with given fields: ctx, input -func (_m *ItemSnapshotter) CreateItemFromSnapshot(ctx context.Context, input *v1.CreateItemInput) (*v1.CreateItemOutput, error) { +func (_m *ItemSnapshotter) CreateItemFromSnapshot(ctx context.Context, input *isv1.CreateItemInput) (*isv1.CreateItemOutput, error) { ret := _m.Called(ctx, input) - var r0 *v1.CreateItemOutput - if rf, ok := ret.Get(0).(func(context.Context, *v1.CreateItemInput) *v1.CreateItemOutput); ok { + var r0 *isv1.CreateItemOutput + if rf, ok := ret.Get(0).(func(context.Context, *isv1.CreateItemInput) *isv1.CreateItemOutput); ok { r0 = rf(ctx, input) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.CreateItemOutput) + r0 = ret.Get(0).(*isv1.CreateItemOutput) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *v1.CreateItemInput) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *isv1.CreateItemInput) error); ok { r1 = rf(ctx, input) } else { r1 = ret.Error(1) @@ -83,11 +83,11 @@ func (_m *ItemSnapshotter) CreateItemFromSnapshot(ctx context.Context, input *v1 } // DeleteSnapshot provides a mock function with given fields: ctx, input -func (_m *ItemSnapshotter) DeleteSnapshot(ctx context.Context, input *v1.DeleteSnapshotInput) error { +func (_m *ItemSnapshotter) DeleteSnapshot(ctx context.Context, input *isv1.DeleteSnapshotInput) error { ret := _m.Called(ctx, input) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.DeleteSnapshotInput) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *isv1.DeleteSnapshotInput) error); ok { r0 = rf(ctx, input) } else { r0 = ret.Error(0) @@ -111,20 +111,20 @@ func (_m *ItemSnapshotter) Init(config map[string]string) error { } // Progress provides a mock function with given fields: input -func (_m *ItemSnapshotter) Progress(input *v1.ProgressInput) (*v1.ProgressOutput, error) { +func (_m *ItemSnapshotter) Progress(input *isv1.ProgressInput) (*isv1.ProgressOutput, error) { ret := _m.Called(input) - var r0 *v1.ProgressOutput - if rf, ok := ret.Get(0).(func(*v1.ProgressInput) *v1.ProgressOutput); ok { + var r0 *isv1.ProgressOutput + if rf, ok := ret.Get(0).(func(*isv1.ProgressInput) *isv1.ProgressOutput); ok { r0 = rf(input) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.ProgressOutput) + r0 = ret.Get(0).(*isv1.ProgressOutput) } } var r1 error - if rf, ok := ret.Get(1).(func(*v1.ProgressInput) error); ok { + if rf, ok := ret.Get(1).(func(*isv1.ProgressInput) error); ok { r1 = rf(input) } else { r1 = ret.Error(1) @@ -134,20 +134,20 @@ func (_m *ItemSnapshotter) Progress(input *v1.ProgressInput) (*v1.ProgressOutput } // SnapshotItem provides a mock function with given fields: ctx, input -func (_m *ItemSnapshotter) SnapshotItem(ctx context.Context, input *v1.SnapshotItemInput) (*v1.SnapshotItemOutput, error) { +func (_m *ItemSnapshotter) SnapshotItem(ctx context.Context, input *isv1.SnapshotItemInput) (*isv1.SnapshotItemOutput, error) { ret := _m.Called(ctx, input) - var r0 *v1.SnapshotItemOutput - if rf, ok := ret.Get(0).(func(context.Context, *v1.SnapshotItemInput) *v1.SnapshotItemOutput); ok { + var r0 *isv1.SnapshotItemOutput + if rf, ok := ret.Get(0).(func(context.Context, *isv1.SnapshotItemInput) *isv1.SnapshotItemOutput); ok { r0 = rf(ctx, input) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.SnapshotItemOutput) + r0 = ret.Get(0).(*isv1.SnapshotItemOutput) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *v1.SnapshotItemInput) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *isv1.SnapshotItemInput) error); ok { r1 = rf(ctx, input) } else { r1 = ret.Error(1) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index d22ddbc378..a818d6dbc8 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -56,8 +56,10 @@ import ( listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/label" + "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/vmware-tanzu/velero/pkg/podexec" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/boolptr" @@ -76,6 +78,7 @@ type Request struct { Log logrus.FieldLogger Backup *velerov1api.Backup + ItemSnapshots []*volume.ItemSnapshot PodVolumeBackups []*velerov1api.PodVolumeBackup VolumeSnapshots []*volume.Snapshot BackupReader io.Reader @@ -287,6 +290,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( resticErrs: make(chan error), pvsToProvision: sets.NewString(), pvRestorer: pvRestorer, + itemSnapshots: req.ItemSnapshots, volumeSnapshots: req.VolumeSnapshots, podVolumeBackups: req.PodVolumeBackups, resourceTerminatingTimeout: kr.resourceTerminatingTimeout, @@ -345,6 +349,7 @@ type restoreContext struct { waitExecHookHandler hook.WaitExecHookHandler hooksContext go_context.Context hooksCancelFunc go_context.CancelFunc + itemSnapshots []*volume.ItemSnapshot } type resourceClientKey struct { @@ -715,17 +720,6 @@ func (ctx *restoreContext) getApplicableActions(groupResource schema.GroupResour return actions } -func (ctx *restoreContext) getApplicableItemSnapshotters(groupResource schema.GroupResource, namespace string) []framework.ItemSnapshotterResolvedAction { - var actions []framework.ItemSnapshotterResolvedAction - for _, action := range ctx.itemSnapshotterActions { - if action.ShouldUse(groupResource, namespace, nil, ctx.log) { - actions = append(actions, action) - } - } - - return actions -} - func (ctx *restoreContext) shouldRestore(name string, pvClient client.Dynamic) (bool, error) { pvLogger := ctx.log.WithField("pvName", name) @@ -992,122 +986,167 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso return warnings, errs } - if groupResource == kuberesource.PersistentVolumes { - switch { - case hasSnapshot(name, ctx.volumeSnapshots): - oldName := obj.GetName() - shouldRenamePV, err := shouldRenamePV(ctx, obj, resourceClient) + var itemSnapshot *volume.ItemSnapshot + if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { // Use the ResourceIdentifier for the original item to match for snapshots + itemRID := velero.ResourceIdentifier{ + GroupResource: groupResource, + Namespace: obj.GetNamespace(), + Name: name, + } + itemSnapshot, err = getItemSnapshot(itemRID, ctx.itemSnapshots) + if err != nil { + errs.Add(namespace, err) + return warnings, errs + } + } + if itemSnapshot != nil { + itemSnapshotter := clientmgmt.ItemSnapshotterForSnapshotFromResolved(itemSnapshot, ctx.itemSnapshotterActions) + if itemSnapshotter != nil { + cii := isv1.CreateItemInput{ + SnapshottedItem: obj, + SnapshotID: itemSnapshot.Status.ProviderSnapshotID, + ItemFromBackup: obj, + SnapshotMetadata: itemSnapshot.Status.Metadata, + Params: nil, // TBD + Restore: ctx.restore, + } + cio, err := itemSnapshotter.CreateItemFromSnapshot(go_context.TODO(), &cii) if err != nil { errs.Add(namespace, err) return warnings, errs } - - // Check to see if the claimRef.namespace field needs to be remapped, - // and do so if necessary. - _, err = remapClaimRefNS(ctx, obj) - if err != nil { - errs.Add(namespace, err) + unstructuredObj, ok := cio.UpdatedItem.(*unstructured.Unstructured) + if !ok { + errs.Add(namespace, fmt.Errorf("%s: unexpected type %T", resourceID, cio.UpdatedItem)) return warnings, errs } - var shouldRestoreSnapshot bool - if !shouldRenamePV { - // Check if the PV exists in the cluster before attempting to create - // a volume from the snapshot, in order to avoid orphaned volumes (GH #609) - shouldRestoreSnapshot, err = ctx.shouldRestore(name, resourceClient) + obj = unstructuredObj + w, e := handleAdditionalItems(ctx, namespace, cio.AdditionalItems) + warnings.Merge(&w) + errs.Merge(&e) + } else { + errs.Add(namespace, fmt.Errorf("could not find ItemSnapshotter %s needed for snapshot of item %s, snapshot ID %s", + itemSnapshot.Spec.ItemSnapshotter, itemSnapshot.Spec.ResourceIdentifier, itemSnapshot.Status.ProviderSnapshotID)) + } + } else { + if groupResource == kuberesource.PersistentVolumes { + switch { + case hasSnapshot(name, ctx.volumeSnapshots): + oldName := obj.GetName() + shouldRenamePV, err := shouldRenamePV(ctx, obj, resourceClient) if err != nil { - errs.Add(namespace, errors.Wrapf(err, "error waiting on in-cluster persistentvolume %s", name)) + errs.Add(namespace, err) return warnings, errs } - } else { - // If we're renaming the PV, we're going to give it a new random name, - // so we can assume it doesn't already exist in the cluster and therefore - // we should proceed with restoring from snapshot. - shouldRestoreSnapshot = true - } - - if shouldRestoreSnapshot { - // Reset the PV's binding status so that Kubernetes can properly - // associate it with the restored PVC. - obj = resetVolumeBindingInfo(obj) - // Even if we're renaming the PV, obj still has the old name here, because the pvRestorer - // uses the original name to look up metadata about the snapshot. - ctx.log.Infof("Restoring persistent volume from snapshot.") - updatedObj, err := ctx.pvRestorer.executePVAction(obj) + // Check to see if the claimRef.namespace field needs to be remapped, + // and do so if necessary. + _, err = remapClaimRefNS(ctx, obj) if err != nil { - errs.Add(namespace, fmt.Errorf("error executing PVAction for %s: %v", resourceID, err)) + errs.Add(namespace, err) return warnings, errs } - obj = updatedObj - // VolumeSnapshotter has modified the PV name, we should rename the PV. - if oldName != obj.GetName() { - shouldRenamePV = true - } - } - - if shouldRenamePV { - var pvName string - if oldName == obj.GetName() { - // pvRestorer hasn't modified the PV name, we need to rename the PV. - pvName, err = ctx.pvRenamer(oldName) + var shouldRestoreSnapshot bool + if !shouldRenamePV { + // Check if the PV exists in the cluster before attempting to create + // a volume from the snapshot, in order to avoid orphaned volumes (GH #609) + shouldRestoreSnapshot, err = ctx.shouldRestore(name, resourceClient) if err != nil { - errs.Add(namespace, errors.Wrapf(err, "error renaming PV")) + errs.Add(namespace, errors.Wrapf(err, "error waiting on in-cluster persistentvolume %s", name)) return warnings, errs } } else { - // VolumeSnapshotter could have modified the PV name through - // function `SetVolumeID`, - pvName = obj.GetName() + // If we're renaming the PV, we're going to give it a new random name, + // so we can assume it doesn't already exist in the cluster and therefore + // we should proceed with restoring from snapshot. + shouldRestoreSnapshot = true } - ctx.renamedPVs[oldName] = pvName - obj.SetName(pvName) + if shouldRestoreSnapshot { + // Reset the PV's binding status so that Kubernetes can properly + // associate it with the restored PVC. + obj = resetVolumeBindingInfo(obj) - // Add the original PV name as an annotation. - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - annotations["velero.io/original-pv-name"] = oldName - obj.SetAnnotations(annotations) - } + // Even if we're renaming the PV, obj still has the old name here, because the pvRestorer + // uses the original name to look up metadata about the snapshot. + ctx.log.Infof("Restoring persistent volume from snapshot.") + updatedObj, err := ctx.pvRestorer.executePVAction(obj) + if err != nil { + errs.Add(namespace, fmt.Errorf("error executing PVAction for %s: %v", resourceID, err)) + return warnings, errs + } + obj = updatedObj - case hasResticBackup(obj, ctx): - ctx.log.Infof("Dynamically re-provisioning persistent volume because it has a restic backup to be restored.") - ctx.pvsToProvision.Insert(name) + // VolumeSnapshotter has modified the PV name, we should rename the PV. + if oldName != obj.GetName() { + shouldRenamePV = true + } + } - // Return early because we don't want to restore the PV itself, we - // want to dynamically re-provision it. - return warnings, errs + if shouldRenamePV { + var pvName string + if oldName == obj.GetName() { + // pvRestorer hasn't modified the PV name, we need to rename the PV. + pvName, err = ctx.pvRenamer(oldName) + if err != nil { + errs.Add(namespace, errors.Wrapf(err, "error renaming PV")) + return warnings, errs + } + } else { + // VolumeSnapshotter could have modified the PV name through + // function `SetVolumeID`, + pvName = obj.GetName() + } - case hasDeleteReclaimPolicy(obj.Object): - ctx.log.Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.") - ctx.pvsToProvision.Insert(name) + ctx.renamedPVs[oldName] = pvName + obj.SetName(pvName) - // Return early because we don't want to restore the PV itself, we - // want to dynamically re-provision it. - return warnings, errs + // Add the original PV name as an annotation. + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations["velero.io/original-pv-name"] = oldName + obj.SetAnnotations(annotations) + } - default: - ctx.log.Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.") + case hasResticBackup(obj, ctx): + ctx.log.Infof("Dynamically re-provisioning persistent volume because it has a restic backup to be restored.") + ctx.pvsToProvision.Insert(name) - // Check to see if the claimRef.namespace field needs to be remapped, and do so if necessary. - _, err = remapClaimRefNS(ctx, obj) - if err != nil { - errs.Add(namespace, err) + // Return early because we don't want to restore the PV itself, we + // want to dynamically re-provision it. return warnings, errs - } - obj = resetVolumeBindingInfo(obj) - // We call the pvRestorer here to clear out the PV's claimRef.UID, - // so it can be re-claimed when its PVC is restored and gets a new UID. - updatedObj, err := ctx.pvRestorer.executePVAction(obj) - if err != nil { - errs.Add(namespace, fmt.Errorf("error executing PVAction for %s: %v", resourceID, err)) + + case hasDeleteReclaimPolicy(obj.Object): + ctx.log.Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.") + ctx.pvsToProvision.Insert(name) + + // Return early because we don't want to restore the PV itself, we + // want to dynamically re-provision it. return warnings, errs + + default: + ctx.log.Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.") + + // Check to see if the claimRef.namespace field needs to be remapped, and do so if necessary. + _, err = remapClaimRefNS(ctx, obj) + if err != nil { + errs.Add(namespace, err) + return warnings, errs + } + obj = resetVolumeBindingInfo(obj) + // We call the pvRestorer here to clear out the PV's claimRef.UID, + // so it can be re-claimed when its PVC is restored and gets a new UID. + updatedObj, err := ctx.pvRestorer.executePVAction(obj) + if err != nil { + errs.Add(namespace, fmt.Errorf("error executing PVAction for %s: %v", resourceID, err)) + return warnings, errs + } + obj = updatedObj } - obj = updatedObj } } @@ -1146,37 +1185,9 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso obj = unstructuredObj - for _, additionalItem := range executeOutput.AdditionalItems { - itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) - - if _, err := ctx.fileSystem.Stat(itemPath); err != nil { - ctx.log.WithError(err).WithFields(logrus.Fields{ - "additionalResource": additionalItem.GroupResource.String(), - "additionalResourceNamespace": additionalItem.Namespace, - "additionalResourceName": additionalItem.Name, - }).Warn("unable to restore additional item") - warnings.Add(additionalItem.Namespace, err) - - continue - } - - additionalResourceID := getResourceID(additionalItem.GroupResource, additionalItem.Namespace, additionalItem.Name) - additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath) - if err != nil { - errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID)) - } - - additionalItemNamespace := additionalItem.Namespace - if additionalItemNamespace != "" { - if remapped, ok := ctx.restore.Spec.NamespaceMapping[additionalItemNamespace]; ok { - additionalItemNamespace = remapped - } - } - - w, e := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace) - warnings.Merge(&w) - errs.Merge(&e) - } + w, e := handleAdditionalItems(ctx, namespace, executeOutput.AdditionalItems) + warnings.Merge(&w) + errs.Merge(&e) } // This comes after running item actions because we have built-in actions that restore @@ -1377,6 +1388,42 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso return warnings, errs } +func handleAdditionalItems(ctx *restoreContext, namespace string, additionalItems []velero.ResourceIdentifier) (Result, Result) { + warnings, errs := Result{}, Result{} + for _, additionalItem := range additionalItems { + itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) + + if _, err := ctx.fileSystem.Stat(itemPath); err != nil { + ctx.log.WithError(err).WithFields(logrus.Fields{ + "additionalResource": additionalItem.GroupResource.String(), + "additionalResourceNamespace": additionalItem.Namespace, + "additionalResourceName": additionalItem.Name, + }).Warn("unable to restore additional item") + warnings.Add(additionalItem.Namespace, err) + + continue + } + + additionalResourceID := getResourceID(additionalItem.GroupResource, additionalItem.Namespace, additionalItem.Name) + additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath) + if err != nil { + errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID)) + } + + additionalItemNamespace := additionalItem.Namespace + if additionalItemNamespace != "" { + if remapped, ok := ctx.restore.Spec.NamespaceMapping[additionalItemNamespace]; ok { + additionalItemNamespace = remapped + } + } + + w, e := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace) + warnings.Merge(&w) + errs.Merge(&e) + } + return warnings, errs +} + func isAlreadyExistsError(ctx *restoreContext, obj *unstructured.Unstructured, err error, client client.Dynamic) (bool, error) { if err == nil { return false, nil @@ -1574,6 +1621,19 @@ func hasSnapshot(pvName string, snapshots []*volume.Snapshot) bool { return false } +func getItemSnapshot(resourceIdentifier velero.ResourceIdentifier, itemSnapshots []*volume.ItemSnapshot) (*volume.ItemSnapshot, error) { + for _, snapshot := range itemSnapshots { + snapshotRID := velero.ResourceIdentifier{} + if err := json.Unmarshal([]byte(snapshot.Spec.ResourceIdentifier), &snapshotRID); err != nil { + return nil, err + } + if snapshotRID == resourceIdentifier { + return snapshot, nil + } + } + return nil, nil +} + func hasResticBackup(unstructuredPV *unstructured.Unstructured, ctx *restoreContext) bool { if len(ctx.podVolumeBackups) == 0 { return false diff --git a/pkg/volume/item_snapshot.go b/pkg/volume/item_snapshot.go index 909c219944..ae4812e468 100644 --- a/pkg/volume/item_snapshot.go +++ b/pkg/volume/item_snapshot.go @@ -54,4 +54,7 @@ type ItemSnapshotStatus struct { // Phase is the current state of the ItemSnapshot. Phase isv1.SnapshotPhase `json:"phase,omitempty"` + + // Error is the error message if the Phase is SnapshotPhaseFailed + Error string `json:"error,omitempty"` } From 8e94f74bca3eea6e93087b8c0f5f65c8db031eae Mon Sep 17 00:00:00 2001 From: Dave Smith-Uchida Date: Tue, 5 Apr 2022 10:22:07 -0700 Subject: [PATCH 2/3] Refactored end of backup metrics updates into its own method Signed-off-by: Dave Smith-Uchida --- pkg/controller/backup_controller.go | 43 +++++++++++------------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index d1d7083446..9ecb4edbe2 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -164,11 +164,7 @@ func NewBackupController( backup := obj.(*velerov1api.Backup) switch backup.Status.Phase { -<<<<<<< HEAD case "", velerov1api.BackupPhaseNew, velerov1api.BackupPhaseInProgress: -======= - case "", velerov1api.BackupPhaseNew: - // only process new backups case velerov1api.BackupPhaseUploading, velerov1api.BackupPhaseUploadingPartialFailure: if features.IsEnabled(velerov1api.UploadProgressFeatureFlag) { c.logger.WithFields(logrus.Fields{ @@ -184,12 +180,11 @@ func NewBackupController( "phase": backup.Status.Phase, }).Debug("Backup is not new, skipping") } ->>>>>>> ca95d5ee... Adding basic item snapshotter support default: c.logger.WithFields(logrus.Fields{ "backup": kubeutil.NamespaceAndName(backup), "phase": backup.Status.Phase, - }).Debug("Backup is not new or in-progress, skipping") + }).Debug("Backup is not new, uploading or in-progress, skipping") return } @@ -360,16 +355,7 @@ func (c *backupController) processBackup(key string) error { request.Status.FailureReason = err.Error() } - switch request.Status.Phase { - case velerov1api.BackupPhaseCompleted: - c.metrics.RegisterBackupSuccess(backupScheduleName) - case velerov1api.BackupPhasePartiallyFailed: - c.metrics.RegisterBackupPartialFailure(backupScheduleName) - case velerov1api.BackupPhaseFailed: - c.metrics.RegisterBackupFailed(backupScheduleName) - case velerov1api.BackupPhaseFailedValidation: - c.metrics.RegisterBackupValidationFailure(backupScheduleName) - } + c.updateMetrics(request.Status.Phase, backupScheduleName) log.Debug("Updating backup's final status") if _, err := patchBackup(original, request.Backup, c.client); err != nil { @@ -386,6 +372,19 @@ func (c *backupController) processBackup(key string) error { return nil } +func (c *backupController) updateMetrics(phase velerov1api.BackupPhase, backupScheduleName string) { + switch phase { + case velerov1api.BackupPhaseCompleted: + c.metrics.RegisterBackupSuccess(backupScheduleName) + case velerov1api.BackupPhasePartiallyFailed: + c.metrics.RegisterBackupPartialFailure(backupScheduleName) + case velerov1api.BackupPhaseFailed: + c.metrics.RegisterBackupFailed(backupScheduleName) + case velerov1api.BackupPhaseFailedValidation: + c.metrics.RegisterBackupValidationFailure(backupScheduleName) + } +} + // Execute periodically to check upload progress for backups that are in Uploading or UploadingPartialFailure phase func (c *backupController) checkAndFinalizeBackupsLoop(wakeupInterval time.Duration) { for { @@ -429,22 +428,12 @@ func (c *backupController) completeBackupWithUploadedSnapshots(curBackup *pkgbac updateBackupStatusForSnapshotState(curBackup, log) } + c.updateMetrics(curBackup.Status.Phase, backupScheduleName) log.Debug("Updating backup's final status") if _, err := patchBackup(&original, curBackup.Backup, c.client); err != nil { log.WithError(err).Error("error updating backup's final status after upload completion") } - switch curBackup.Status.Phase { - case velerov1api.BackupPhaseCompleted: - c.metrics.RegisterBackupSuccess(backupScheduleName) - case velerov1api.BackupPhasePartiallyFailed: - c.metrics.RegisterBackupPartialFailure(backupScheduleName) - case velerov1api.BackupPhaseFailed: - c.metrics.RegisterBackupFailed(backupScheduleName) - case velerov1api.BackupPhaseFailedValidation: - c.metrics.RegisterBackupValidationFailure(backupScheduleName) - } - curBackup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} pluginManager := c.newPluginManager(c.logger) From e33b5064a55d9caf081a423ccf19dd48b6e6390c Mon Sep 17 00:00:00 2001 From: Dave Smith-Uchida Date: Tue, 5 Apr 2022 13:14:26 -0700 Subject: [PATCH 3/3] Factored out ItemSnapshotterForSnapshotFromResolved Signed-off-by: Dave Smith-Uchida --- config/crd/v1/crds/crds.go | 2 +- pkg/controller/backup_deletion_controller.go | 13 ++++++------- .../backup_deletion_controller_test.go | 16 +++++++--------- pkg/controller/restore_controller.go | 3 +-- pkg/controller/restore_controller_test.go | 4 ++-- pkg/plugin/clientmgmt/util.go | 14 -------------- pkg/restore/restore.go | 19 +++++++------------ 7 files changed, 24 insertions(+), 47 deletions(-) diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 6ead090277..fd0ec15032 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,7 +29,7 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec}\xcdr\x1c9r\xf0\x9dO\x91\xc1\xef\xa0\xdd\bvs'\xbe\x83\x1d\xbci(M\xb8c\xc6\x1a\x86(\xcb\a\x87\x0f\xe8\xaa\xecn\fQ@-\x80j\xaa\xed\xf0\xbb;\x90\x00\xea\xafQUh\x0e\xb9;\xeb\x10.RW\x01\x89D\"\xff\x91\x05^\xadV\xab+V\xf3\xaf\xa8\rW\xf2\x0eX\xcd\xf1\x9bE\xe9~\x99\xf5\xd3?\x9b5W\xb7\xc7\x1f\xae\x9e\xb8,\xef\xe0\xbe1VU\x9fѨF\x17\xf8\x01w\\r˕\xbc\xaaв\x92Yvw\x05\xc0\xa4T\x96\xb9\xc7\xc6\xfd\x04(\x94\xb4Z\t\x81z\xb5G\xb9~j\xb6\xb8m\xb8(Q\x13\xf08\xf5\xf1/\xeb\u007fZ\xff\xe5\n\xa0\xd0Hÿ\xf0\n\x8deU}\a\xb2\x11\xe2\n@\xb2\n\xef`ˊ\xa7\xa66\xeb#\n\xd4j\xcdՕ\xa9\xb1ps\xed\xb5j\xea;\xe8^\xf8!\x01\x0f\xbf\x86\x1fi4=\x10\xdc؟{\x0f\u007f\xe1\xc6ҋZ4\x9a\x89v&zf\xb8\xdc7\x82\xe9\xf8\xf4\n\xc0\x14\xaa\xc6;\xf8䦨Y\x81\xe5\x15@X\x0eM\xb9\n\b\x1f\u007f\xf0\x10\x8a\x03V\xcc\xe3\x02\xa0j\x94\xef\x1f6_\xff\xff\xe3\xe01@\x89\xa6м\xb6D\x14\x8f\x18p\x03\f\xbeҲ@\a\xf2\x83=0\v\x1ak\x8d\x06\xa55`\x0f\b\x05\xabm\xa3\x11\xd4\x0e~n\xb6\xa8%Z4-h\x80B4Ƣ\x06c\x99E`\x16\x18ԊK\v\\\x82\xe5\x15\u009f\xde?l@m\u007f\xc3\xc2\x1a`\xb2\x04f\x8c*8\xb3X\xc2Q\x89\xa6B?\xf6\xcf\xeb\x16j\xadU\x8d\xda\xf2Hg\xdfz\\\xd5{:Z\xde;G\x01\xdf\vJ\xc7N\xe8\x97\x11\xa8\x88e \x9a[\x8f=p\xd3-\x978d\x00\x18\\'&\x03\xf2kxD\xed\xc0\x809\xa8F\x94\x8e\v\x8f\xa8\x1d\xc1\n\xb5\x97\xfc\xbfZ\xd8\x06\xac\xa2I\x05\xb3\x18\x18\xa0k\\ZԒ\t82\xd1\xe0\r\x91\xa4b'\xd0\xe8f\x81F\xf6\xe0Q\x17\xb3\x86\u007fU\x1a\x81˝\xba\x83\x83\xb5\xb5\xb9\xbb\xbd\xdds\x1b\xa5\xa9PU\xd5HnO\xb7$\x18|\xdbX\xa5\xcdm\x89G\x14\xb7\x86\xefWL\x17\an\xb1p\x1by\xcbj\xbe\"\xd4%IԺ*\xff_d\x00\xf3n\x80\xab=9f4Vs\xb9\xef\xbd \xae\x9f\xd9\x01'\x00\x9e\xbf\xfcP\xbf\x8a\x8e\xd0\ue463\xce珏_\xfa\xbc\xc7͘\xfaD\xf7\x1eCv[\xe0\b\xc6\xe5\x0e\xb5\xdfĝV\x15\xc1DYz\xee#\xd6\x15\x1c\xe5\x98\xfc\xa6\xd9Vܺ}\xffk\x83\xc61\xb9Z\xc3=\xa9\x18\xd8\"4u\xe98s\r\x1b\t\xf7\xacBq\xcf\f\xbe\xf9\x068J\x9b\x95#l\xde\x16\xf4\xb5㸳\xa7Z\xefE\xd4e\x13\xfb\xe5\x15\xc2c\x8d\xc5@`\xdc(\xbe\xe3\x05\x89\x05\xec\x94\xee\xf4\x85WW\xeb\x01ȴ\xc8\xfa\xc9v\xac\x11\xf6+\x89\xba\xf9\xa2>\xa3\xb1\xbc\x18\xf7\x1a!\xf5!9(\"\x85\x06\x9e\x0fh\x0f\xa8\x1d\xff\xd0\v\x12\xc93\x98@[j\xb0$\x89dO\b,`O\xa2-\x04\xd4*j!\x03\xdbSDv}\x06\xc9\xd3v\xab\x94@6\xd6\x12\xf8\xad\x10M\x89e\xab\xb6\xcfh0Z\xdddz\x01d\xd2\x18\x97Nj\x9c\x11q\xe8\xc9\xee\xadS̉\xc51\x8d\xe0\xf8\x96K\x0f\x8ft\xee\x01\x93\x1b\xe4\x1a\xb7X%p\x9bd3ߜ\xa9d[\x81w`u\x83\x13\x94aZ\xb3\xd3\x04]\xa2y\xcf%K\xdb?h\x11\xc1\v\xb2?\xad\xae \xcaxk\xc5\xf49F\xf0G&\xcaA\xa9\xa7%B\xfc\x8b\xeb\xd3\xe9=(\xc8K\x82-\x1eؑ+\x1d\x96\x1e\xcc\xd0\x16\x01\xbfa\xd1XL\xf1?\xb3P\xf2\xdd\x0e\xb5\x83S\x1f\x98A\xe3M\xdf4A\xa6E\xd95=\xbd\x99g\xeb\xe86\xd2q*\xad|\nu'\xd0c\xb9\x8a\xcd!ꌆs[dɏ\xbcl\x98\x00.\x8de\xb2\xf0\xeba-^\xe7끹M>\xc3٫È\xb9ۉ\x81jT\x12Ai\xa8\x9c=8\xef:\xb6`]\x9bZ\xf6\x969\xed\xa4<\x8b\xeaF\xa0\tS\x95\xa4s;\x1dp3\t\xba\xdd\x11\xefK\b\xb6E\x01\x06\x05\x16V\xe949\x966ٷ\x1c\xbd6Ań\x86\xebt\xb7[j\xb7\xb0\x19\x90\xe0\xd4\xf6\xf3\x81\x17\ao\xe6\x1d\a\x11\x1c(\x15\x1a\x92rV\xd7\xe24\xb5HX\xda\xf90ɜ\xa0wmA\xe4\xc7\xf0R\xc2ߵ\f\xddص\x05-9\xa4l\xcb\x0e`\xd5\xec\xb2\xffo\x126\xaa\xfd\x170\xed\xe6l\xe8\xeb2\xad#)w\xee\xfcf\aX\xd5\xf6t\x03\xdcƧK\x10\x9d\xb3\xd2\xcd\xff\x0f\xbc1\x97s\xfcf<\xf2U9~vW\x96 \xba]i\xa7\xff\a\xdc\x142\x16\x8f\xc1Vdo\xc8/\xfdQ7\xc0w톔7\xb0\xe3¢\x1e\xed\xcc\uf497\xd7 F\x8e\xbds\xadb\xb68|\xfc\xe6\xa5q\x03\f\x9e\xf0\xe4=\x16&\xc1m\x0es\x13MD:\xe7ġ\xdc\n1\xd9\x13\x9e\bLH\x96,\x8e\xcee\x05ߞ\xf0\x94\xd3mD@\x87\x137!\t\xe4(\xe9\x1e\x10!(\xb6\xce'\x1eP\xe2+\xea\xa2\xe5\xc5A\xbe\"\x89-\xd2\xfe\x05\xcbl\xb7\xad\x974\xa4\x8d}g\xfc\x169)8\xf0:s\xa1\xcéA\x92\x96\x98\xfa\xfa\xca\x04/ۉ<\xdfo\xe4\xb47\xbd\xf6\a=4\t+ȹX\x84[kU\xa01\xf3,\x92\xa1\xad\x17\x92\x84m\x82\x90\xf9\x00\xc6\x1f\x98\xcc'%c\xcbwH\x1d\x91.t\xe5?~\xebe/\x9d\xf0\xbb\xdfK\xccw)^@2[Ul|2\x98\x85\xe2\xbd\x1f\x19\xc5$\x00\xf2\xa1\x81\xde7$\xea\xf9\x1ed`\xa4?\x82\x99\xae\xb8\xdc\xd0\x04\xf0ë\x9b\xf5VI\xe2K\x1c\xf7\xfb8\xb6#z\xfb\x80\xa47\xd7#R\x94\xb9\xd78ع\xf3<\xb7s\x143AJe\xfb\xe9\x04\a\xb7V\xe5;\x03;\xae\x8d\xed#\x9a\xcb\x14͂\xf4w\xed\xd2\xc8I~\xd4\xfaE\x81ӯ~d/\x91uP\xcf\xf1|u\xf203\xd5\xe8P\b\x81\xef\x80[@Y\xa8FR\xfaʼn:M\xe1\xb7\xc0+\xe8l\x92\xe5)\b\xd7P6U\x1e\x01V\xc4u\\\xce\xe6i\xfa\xdd\u007fb\\\xbcŶY^\xa1jf\rg\xd7\x06\xdb\xf6ŏ\x1c\x1c\x94W\xec\x1b\xaf\x9a\nX\xe5H\x9f\x1b\xf6\xec|q\xcc`\xc7\xe1\x99qK\x96\xc3\xc1%3b\x95\x13\xaaZ\xa0͕\xc8-\xee\x94&y6\xbc\xc4\xd60\a.P\x12\x18\xec\x18\x17\x8d\xceԐ\x17\xd1\xf6\x92X#(\x8b\xd7\v\"\xf2&_\x11)2\x12\xb1\x99\xce⼶\xaeu\xbe\xab\xf8\xa01\xcf=[JJG\xf7\xac\xd6\xdc\xf1\x92zm\x0f-\xb0\x18\x93\xa7\xef.\xdaY\xfb\xee\xa2-\xb4\xef.\xdad\xfb\xee\xa2-\xb7\xef.Zh\xdf]\xb4ؾ\xbbh\xdf]\xb4\xb9ns\xdaz\t#_q?\xf1r\x11\x8b\x8c\xe3\xe99\x14g\xe0\x87j\x8a{_}\x9f[a\xb9I\x8fJ\xd4Ն\xb2\xfe\x15}\x91\x90\u202e\xe8\xa23%mɥ\x13\x90\xc8\u07be\x80x\xa1\b3\xab\x9c2]}\x9bS\xf0\xb3T\xe63\xac3m\xcblb\xa1\xa9\x8a\x93$\xe8\x10\xbflpno\xbf\x86dX\xafC~n\xc4\xf4\xef^\x83\x9aQ\x8a\xb3P\x803_\x98;G\xafQ\xe81$\x98\x1e\x14\x8c\xfea\xe8\xb5P%3]\x1b\x13N\x82в\xe3\x0f\xeb\xe1\x1b\xabB\xa5\f\x84\xbbX.\xe7Ï\xbcZ\x9a\x17W\xd0\f+d&T\xf4\xa5GF\xf9\x85\xc2\xf952\xf3E-\x97Tƌ\xeb^&\x81.\xd7\xc3\xe4D\x8e\v\xb5//\xa8xɬv\xfc\xdd\ac95-/\xaadY,\b̬_\x19V\xa6̃\xbc\xa0j%\x8b8\xcb\x15*\x17ץ\x84:\x90\xd9udW\xa3$\xeaLf\x01O֠\xccU\x97,d\xa5\xce+O\xf2kJfAS\xbd\xc9r%\xc9\xebՋ\xbe\x86\x0f<\xadj\x16\xabA\x16}\xe4y\xfc\x16\xeb=.\xa9\xf2X\xa4\xd8\v+:ڊ\x8d\x89y/\xad\xe3\x18\xd6iL\x00ͩޘ\xa8Θ\x808[\xb3\x91[\x931\x01{\xc1\xec\xcer\xc9\xcc\xcb\U00107430h\xdf\xc4ߊ\xa3^\xba0\xa5\a\xee⒇\xfe먻\xdb\xcb\xe85ͻ\x9f)ϓ\xdb\xc3\xe5\xeeg\xd5\b\xcbkA\xe9\xfc#/\x93A\xa3=\xe0\t\x9e\xb9\x10N\xad\xfe\xa6\xe83\xa7\xed\x89 \xfd\xfa\xb9e\xcf\xf5ȉf\x06\x9eQ\b`)\xe6:[y\xe1\xbf\xe5-\xd4\n\x9d\xcew\x02\x17>\xf9\f\x9f\xfc\xdex\x0e\xa6/\xb9R\x19O{\xc0\xcaA\x89ߎ^\x10~\xcc;\x88ޗ\xa5g\u007fmP\x9f@\x1dQw\x1e\xc3\xc2w\x04^\xd0L#\xba\u00ad\xa0?\xfc\x17\xe4#ǹ\x138x/\xbd\tK\x82\x1d\xe1Hp\x9c̋v\xaf\x9dzsq\xc0D\xd7t\xe2C\xb5\xa3\x13\xef\x97|\xcf\xdc\"\xfc\xb7\r\x1d.\x0f\x1e\x16\xcd\xf6\x9b\x04\x10/\x0f!f@\xe6\x16\xd5\xe7\x1d@-\x16ѿU(\xb1\x14Ld{QyE\xf2oQ\x1c\u007fAQ\xfc\x05A\xc5eaE6\x99r\x8a\xdf\xdf$\xb8x\xc3\xf0\xe2-\x02\x8c\x97\x85\x18\v GE\xed9\xe5\xeaY\x87\xab\xd9\xe7\v9\x87\xa3\xcbG\x00\xf3e\xe8\x19\xe5\xe7\x19\x87\x03K\x98f\x94\x99_V^\x9eA\xc37\n>\xde(\xfcx\x8b\x00\xe4mC\x90\xc5 d\x91sf_\xbf8\xbb\xact\x89z6\x19\x9f\xcbj\xb3L6\x8a\x17\x86s\x8e\xbe\xa8\x8dw\xa4\xb8^\x03\xd74\x95Rn\xbf\xfe,\xe0g.K\xbf\x1f\x8e\xa9zv\x9c\xee\x04\xa2\xfa\xf7֩\xe8\xfc\xb34\xd0ѡ\x82\xc1\x9ai\xba4j{\xf2\a\x93f\r\x1fYq\x18v\x84\x033\xb0S\xbaJ:L\xd7\xed\x89\xccm\x1c\xe5\x9e\\\xaf\x01~R\xed\xa1W\xffF\x05ëZ\x9c\\\x1c\x00\xd7\xc3!/c\x80$\xf3\x18\xc9jsP\U0007a6c5X\xefq\xd8;qx\x17/\xbb)\x84j\xca\x16\xfa\xc4\xe61y\x82\x87\xaf\xe4\x93\xd05!EweJ\xf0:b\xcc7\xbeQ\xe5\xc7\xd7?\xcc3Vi\xb6\xc7_\x94\xbfwh\x89\x12\xc3ރK\xa7\x82\xae\x88\x87\xeb\xf1ۋ\x94\r\r7 \x8d\x80u53A\x1a\xbasN\x87eJ\x89\xccȟ\xb5ba1_\xbe\xfc\xe2\x17`y\x85\xeb\x0f\x8d?8]\xd5L\x1btԌ\v\xf3\x83\xb6\xee\xbf\a\xf5\x9c\xcam\xa8\xb0\xe6\x1f\xc7xk\xa4\xba\x1c:\x9f\xbd\b{\u007fCRd\xbcH\xa2%F\xfd\x9a\x1e\xd5\v\xccz\x9b\xe4\xa5<\x19\x90O\xc1\xe9]$G)\v\xfa\xae\xe6u\xaf\xf9\x99\xd2\xdaSWmYf\x1b\xb3|\xd9\x16u\x8bW\xeb\x85\xea\xaeF\xd3\x1d=\x1e\x84\xbf\xd3\xe6E\xf7m\x85b\x94\xc1u\x87\xf3\xfbt\u007f>\x82.\xb5ӥG\x8d\xca`ڋ\xb3\x9e\x99i\v^\x92\x86\xb4\x03\xe7G\x92'\xeb\xa0a\txD\tJR}\v\xdd~\xe3/^\x1c\x8fI@\xedC\t\x054M-\x14+\xa3\x84G\x9b\x15.\xeb\xfbB\xfaK\x1fQ\xbf330\xe9r\xb0\x9d\xd2)\"\x9c+LoX\xee\xa0d\x16WI\xa0Y\xba/\xc9l\x85\xe1CF7\xef\xaduqA\xcaW\x1e\xee\xdf\xe3fjd\xb4\xbfVY&@6\xd5\xd6\x1bt\x16;\xa4\xf6\xefq3\x129\x13*\x9ef\xc4\xcb/\x8cK\x8b\xfb\xb3\x9c\xe2\xf9\xca\xee#\xff\\\xbc\xb2v\xe4\xd4\xcaLS\x14ḫ\x11\"\xe5ڷ\x9c\xfb\xfaˤZ\xbe\xc5;Ψ\x93W\x81T\b\x18/\xa2\xf3\x95\x80\x15\x1a\xc3\xf6\xf1r\xb3gg\x81\xf6(\x91\x1c\x9fT\xbe\xd1\a\x86]\xe5\xd8\xf0j/\x9f\xc1b\x85mX\x98 \x9e\xfc\xf7z\xbdK\xf9\x05B\xeda\xc7\x05u\r\xd70\x06\xd3|!M\xbe\xd5\\\xe7\x98\xf2\x8fmGG\x1bJ>\xd3Ftו\xa2\xe0{\xee\xec\xa0ۤ=\xd3[\xb6\xc7U\xa1\x84@*3?\xc7\xeb-\x855\xd4\xe7}Ff\x16\x97\xf6S\xbfo\xc8t\xf8\xdd\xf67c0\u007fA!\xdd^i\xb9\xc6\xee:\xd83\x84\x14M|\x91\xe9\xf6TH^\x9cz\x8ei\xbfo\x14\xb0\xa0W=\x9cx\x8f\xeaMp\x06\xd3\xd1l\xc5~S\xfa\x06*.\xdd?\xce\xe3\xa7TD\x1c|\x11\xfetg\xdd\x02\xde\x0f\xaeO[&\xdd3\xa4\x18\x05b\xcaUM\x97Ʈ\xe0\x13\x9e{V\xbe\xda\x15KJ\xbe\xa5n\x8bu]6\xf2A\xab\xbd\x8b\x87\x13/[\xe5\x95x\xf7\xc0\xb4\xe5L\x88\x93\x9fdr\xf6ċ\x0f\xe8\fפ\xf7\x92&k\xc0r\x89\xb2\xa1[\x17zs\xe99\x81\xeaT\xb7\xaa\xb1\x03Uҩ\xa2tڟ\x80\xadᓲ\x183\xba|\b\xd3)_4v\x85\xbb\x9d\xd2\xd6G\xfa\xab\x15\xf0]\xf0\x86\x12p\x9dLЉ\x94\xbf\xbc\x15\xb8\xed\x0e\xe5;\xee\xa5@G\x93\x10\xd2\rO\x15;\xf9\x9aEV\x14\xce\xd9\xc6[c\x99H\xe8\xb7\xdfU\x03En\xa7\xe3>,\xff-ᇝ\x11|\xd3\xef\xdf~8\xdeZ7\x02\xe7)G5\xe5^\xb7'-\x1dP\xa51Jx\xd6\xdcZ\xa7O\xfbGv`\x9d\x06\x15\x02\x8c\xd3)\x13\xd7\x04\xceivz\xefl\xeff:\x858\x8co\xda\xceS\xa6;,N\xb9m\xd9\x12\t&\x96\xe5\xbfY\xe2&\x8eu[Y\x1c\x98\xdc;\xa6Ҫ\xd9\x1f\"_NXƩ\f\\㐂Z4{\xc7\xea\xe1\xb8\xc46Z\xf6R0\xe1\x00\xa5\xec\xa1ˊ\xa7ILCJ8^ ~\x1b.\xfe[\xed\xb4\xaaVa/\xe8\x94\xe3&\xa4F4W\xce\xffw\x81\xfc\x04\xd0\xee\x86-b\x83\xbaF\t\xcc\x04|2>\xa8\x9a\xdfֹ<\x85e\xda\xe6F\x15\x8f\x83\xce\v\x01\x05AN\xe3\xfb\x18\x12?\xfeò\xfb\xf1U\xee7`\xb8\x8cw\x97\xfbĒg\x05\xe3\xe2\f\x8d\x14\xab'\x0f\xb0\xce\"\x84A<0D\xffo\x1b\n\x1c[\v\xf31ǧ\xfc:\xea>\xaa\xceuR\xdeA\f~`\x82\x1e\u007f\xe2;\u007f\xa6V8\xac\xff\xfcw\xaf\xba=f\xf9,\xeff\xdd\x15\xf2DZ\xbf\x03>`\xad\xb1`\xc9\xc0\x03\xe0A\xa0\xf3#\f\xe2\xd0\x13zw\x91\xcb{|Y\x10\xf7\x9a\x11\\\xbcV\xffu\xe2\x9a\xe3\xcbb\xb77\v\xdc^wu\xcfLK.\xf7K2\xf6\xef\xa1[\"r\v\x10\x12\xb1[b\x19m4\xb7\x18\xbb\xf5B\xb7\x88\xe3\xc4mףp\ue542\xb7\xa4\x1d8{H\n\xb4\xec\xc9v\x98)<\xe9\x12b\xac(б\xeb\xa7\xf1\x9fϸ\xbe\xa6\x1f\xf1/d\xd0\xcfBIon\xcd\x1d\xfc\xc7\u007f^Aȸ~\x8d\u007f\n\xc3=\xfc\xdf\x00\x00\x00\xff\xff\xa6\x16s\x9fjd\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms\xe3\xb8rw\xff\x8a.\xe70\xefUY\xf2\xdb\xca!)\xdff=ފj7\xb3\xae\xb1\x9fsH\xe5\x00\x91-\tk\x10\xe0\x03@y\x94T\xfe{\n\r\xf0S \ti\xe4\xf7vS\x83ˌH\xa0\x01\xf4w7\x9a\xf0\xd5b\xb1\xb8b%\u007fAm\xb8\x92w\xc0J\x8e_-J\xf7\xcb,_\xff\xd5,\xb9\xba\xdd\xffp\xf5\xcae~\a\xf7\x95\xb1\xaa\xf8\x82FU:\xc3O\xb8\xe1\x92[\xae\xe4U\x81\x96\xe5̲\xbb+\x00&\xa5\xb2\xcc=6\xee'@\xa6\xa4\xd5J\bԋ-\xca\xe5k\xb5\xc6u\xc5E\x8e\x9a\x80\xd7S\xef\xff\xb2\xfc\x97\xe5_\xae\x002\x8d4\xfc\x99\x17h,+\xca;\x90\x95\x10W\x00\x92\x15x\ak\x96\xbdV\xa5Y\xeeQ\xa0VK\xae\xaeL\x89\x99\x9bk\xabUU\xdeA\xfb\xc2\x0f\t\xeb\xf0{\xf8\x91F\xd3\x03\xc1\x8d\xfd\xb9\xf3\xf0\x17n,\xbd(E\xa5\x99hf\xa2g\x86\xcbm%\x98\xae\x9f^\x01\x98L\x95x\a\x9f\xdd\x14%\xcb0\xbf\x02\bۡ)\x17a\xc1\xfb\x1f<\x84l\x87\x05\xf3k\x01P%ʏ\x8f\xab\x97\u007f~\xea=\x06\xc8\xd1d\x9a\x97\x96\x90\xe2\x17\x06\xdc\x00\x83\x17\xda\x16\xe8\x80~\xb0;fAc\xa9Ѡ\xb4\x06\xec\x0e!c\xa5\xad4\x82\xda\xc0\xcf\xd5\x1a\xb5D\x8b\xa6\x01\r\x90\x89\xcaX\xd4`,\xb3\b\xcc\x02\x83Rqi\x81K\xb0\xbc@\xf8\xd3\xc7\xc7\x15\xa8\xf5o\x98Y\x03L\xe6\xc0\x8cQ\x19g\x16s\xd8+Q\x15\xe8\xc7\xfey\xd9@-\xb5*Q[^\xe3ٷ\x0eWu\x9e\x0e\xb6\xf7\xc1a\xc0\xf7\x82ܱ\x13\xfam\x04,b\x1e\x90\xe6\xf6cwܴ\xdb%\x0e\xe9\x01\x06\u05c9ɰ\xf8%<\xa1v`\xc0\xecT%rDž{\xd4\x0ea\x99\xdaJ\xfe\xdf\rl\x03VѤ\x82Y\f\f\xd06.-j\xc9\x04왨\xf0\x86PR\xb0\x03ht\xb3@%;\xf0\xa8\x8bY¿+\x8d\xc0\xe5F\xdd\xc1\xce\xda\xd2\xdc\xdd\xden\xb9\xad\xa5)SEQIn\x0f\xb7$\x18|]Y\xa5\xcdm\x8e{\x14\xb7\x86o\x17Lg;n1s\x84\xbce%_\xd0\xd2%IԲ\xc8\xff\xa9f\x00\xf3\xa1\xb7V{p\xcch\xac\xe6r\xdbyA\\?A\x01'\x00\x9e\xbf\xfcP\xbf\x8b\x16\xd1\xee\x91\xc3Η\x87\xa7\xe7.\xefq3\xc4>\xe1\xbdÐ-\t\x1c¸ܠ\xf6D\xdchU\x10L\x94\xb9\xe7>b]\xc1Q\x0e\xd1o\xaau\xc1\xad\xa3\xfb\xdf*4\x8e\xc9\xd5\x12\xeeI\xc5\xc0\x1a\xa1*sǙKXI\xb8g\x05\x8a{f\xf0\xdd\t\xe00m\x16\x0e\xb1i$\xe8j\xc7ag\x8f\xb5\u038bZ\x97\x8d\xd0\xcb+\x84\xa7\x12\xb3\x9e\xc0\xb8Q|\xc33\x12\v\xd8(\xdd\xea\v\xaf\xae\x96=\x90q\x91\xf5\x93mX%\xec\v\x89\xbayV_\xd0X\x9e\r{\r\x16\xf5):\xa8^\x14\x1axۡݡv\xfcC/H$\x8f`\x02\x91\xd4`N\x12\xc9^\x11XX=\x89\xb6\x10P\xaaZ\v\x19X\x1f\xea\xc5.\x8f yܮ\x95\x12ȆZ\x02\xbff\xa2\xca1o\xd4\xf6\x11\x0e\x06\xbb{8\x1a@&\x8dq\xe9\xa4\xc6\x19\x11\xb7<پu\x8a9\xb29\xa6\x11\x1c\xdfr\xe9\xe1\x91\xce\xdda\x94@\xaeq\x8bEdm\xa3l\xe6\x9b3\x95l-\xf0\x0e\xac\xaep\x043Lkv\x18\xc1Km\xdeS\xd1\xd2\xf4\x0fZD\xf0\x8c\xecO\xa3+\b3\xdeZ1}\xbc\"\xf8=#e\xa7\xd4\xeb\x1c\"\xfe\xcd\xf5i\xf5\x1ed\xe4%\xc1\x1awlϕ\x0e[\x0ffh\x8d\x80_1\xab,\xc6\xf8\x9fY\xc8\xf9f\x83\xda\xc1)w̠\xf1\xa6o\x1c!\xe3\xa2\xec\x9a\x1e'\xe6\xd1>ZB:N\xa5\x9d\x8f-\xdd\t\xf4P\xae\xea\xe6\x16ꌆs[d\xce\xf7<\xaf\x98\x00.\x8de2\xf3\xfbaͺ\x8e\xf7\x03SD>Z\xb3W\x87\xf5\xca\x1d%z\xaaQI\x04\xa5\xa1p\xf6\xe0\xb8\xebЂ\xb5ml\xdbk洓\xf2,\xaa+\x81&L\x95\x93\xcemu\xc0\xcd(\xe8\x86\"ޗ\x10l\x8d\x02\f\n̬\xd2qt\xcc\x11ٷ\x14\xbd6\x82ň\x86ku\xb7\xdbj\xbb\xb1\t\x90\xe0\xd4\xf6ێg;o\xe6\x1d\a\x11\x1c\xc8\x15\x1a\x92rV\x96\xe20\xb6I\x98\xa3|\x98dJ\xd0\xdb6#\xf2Cx1\xe1o[\x82nlی\x96\xecc\xb6a\a\xb0jr\xdb\xff?\x11[\xab\xfd3\x98vu4\xf4\xb2L\xebPʝ;\xbf\xda\x00\x16\xa5=\xdc\x00\xb7\xf5\xd39\x88\xceYi\xe7\xff\x03\x13\xe6t\x8e_\rG^\x94\xe3'\xa92\a\xd1Q\xa5\x99\xfe\x0fH\x142\x16O\xc1V$\x13\xe4\x97\xee\xa8\x1b\xe0\x9b\x86 \xf9\rl\xb8\xb0\xa8\a\x94\xf9&y\xb9\x042R\xec\x9dk\x05\xb3\xd9\xee\xe1\xab\xf3\xbcL\x9bpJ\xc4\xcbp\xb0\xf7_k\u007f\xbeo\x98g\xe0\x02\x05\xa8\\c\xe1\x03\xdfg\xc2f\xfb\x84<\xaa\x8f\x9f?a>\x85\x1eH㼣\x8d|\x1c,\xb6;up\xcaS\xb7\x11\\\x9f&\xbe\xf1)\x8d\x1b`\xf0\x8a\a\xef\xb10\t\x8e8\xccM4\x12\xe9\x1c#\x87r+\xc4d\xafx 0!Y2;:\x95\x15|{\xc5CJ\xb7\x01\x02ݚ\xb8\tI \x87I\xf7\x80\x10A\xb1u:\xf2\x80\x12_\xb5.\x9a\xdf\x1c\xa4+\x92\xbaո?c\x9b\r\xd9:IC\"\xec\a\xe3I\xe4\xa4`\xc7\xcbč:3\a\x06IZ\xea\xd4\xd7\v\x134\xd0ۊD=݃\f\x8c\xf4{0\xd3\x05\x97+\x9a\x00~\xb8\xb8Yo\x94$\x9e\xe3\xb8\xdf\xd7c[\xa47\x0fHzS=\"E\x99{\x8d=\xca\x1d繝\xa3\x98\bR*\xdbM'8\xb8\xa5\xca?\x18\xd8pmlw\xa1\xa9LQ\xcdH\u007f\xdbN\x8d\x9c\xe4\x83\xd6g\x05N\xbf\xfa\x91\x9dD\xd6N\xbd\xd5竣\x87\x99\xb1F\x87B\b|\x03\xdc\x02\xcaLU\x92\xd2/N\xd4i\nO\x02\xaf\xa0\x93Q\x96\xa6 \\CY\x15i\bX\x10\xd7q9\x99\xa7\xe9v\xff\x89q\xf1\x1ed\xb3\xbc@UM\x1aζ\xf5\xc8\xf6\xecG\xf6\x0e\xca\v\xf6\x95\x17U\x01\xacp\xa8O\r{6\xbe8\xa6Gqxcܒ\xe5ppɌX優\x14hS%r\x8d\x1b\xa5I\x9e\rϱ1́\v\x94\x04\x06\x1b\xc6E\xa5\x135\xe4I\xb8=%\xd6\b\xca\xe2rAD\xda\xe4\vBEB\"6\xd1Y\x9c\xd6֥Nw\x15\x1f5\xa6\xb9gsI\xe9\xda=+5w\xbc\xa4.\xed\xa1\x05\x16c\xf2\xf0\xddE;j\xdf]\xb4\x99\xf6\xddE\x1bm\xdf]\xb4\xf9\xf6\xddE\v\xed\xbb\x8bV\xb7\xef.\xdaw\x17m\xaa۔\xb6\x9e[\x91\xaf\xb8\x1fy9\xbb\x8a\x84\xe3\xe9\xa9%N\xc0\x0f\xd5\x14\xf7\xbe\xfa>\xb5\xc2r\x15\x1f\x15\xa9\xab\re\xfd\v\xfa\"!\xc6\x01m\xd1EkJ\x9a\x92K' 5{\xfb\x02\xe2\x99\"̤r\xcax\xf5mJ\xc1\xcf\\\x99O\xbfδ)\xb3\xa9\vMU=I\x04\x0f\xf5\x97\r\xce\xed\xed\u0590\xf4\xebu\xc8ϭW\xfa\x0f\xafAM(ř)\xc0\x99.̝\xc2\xd7 \xf4\xe8#L\xf7\nF\u007f7\xf8\x9a\xa9\x92\x19\xaf\x8d\t'Ah\xd9\xfe\x87e\xff\x8dU\xa1R\x06\u07b8\xddE\xb6\xf2\xb6CIgXr\xdb-{\xad\xf9-|b2\xc4#(\r\x92\vB\xe7\x04\xb7\xf6\xd0\v\xbf\x96>\x84;Y.\xa7Ï\xb4Z\x9a\xb3+h\xfa\x152#*\xfa\xd4#\xa3\xf4B\xe1\xf4\x1a\x99颖S*c\x86u/\xa3@\xe7\xebaR\"Ǚڗ3*^\x12\xab\x1d\xbf\xf9`,\xa5\xa6\xe5\xacJ\x96ق\xc0\xc4\xfa\x95~e\xca4\xc8\x13\xaaV\x92\x903_\xa1rr]J\xa8\x03\x99\xdcGr5J\xa4\xced\x12\xf0h\r\xcaTu\xc9LV\xea\xb8\xf2$\xbd\xa6d\x124՛\xccW\x92\\\xae^\xf4\x12>\U00038a99\xad\x06\x99\xf5\x91\xa7\xd77[\xefqJ\x95\xc7,\xc6ά\xe8h*6F\xe6=\xb5\x8e\xa3_\xa71\x024\xa5zc\xa4:c\x04\xe2d\xcdFjM\xc6\b\xec\x19\xb3;\xc9%\x13/\xe3\x1fB¬}\x13\u007f/\x8e:wcJ\xf7\xdc\xc59\x0f\xfd\xd7AwG\xcb\xdak\x9av?c\x9e'\xb7\xbb\xd3\xddϢ\x12\x96\x97\x82\xd2\xf9{\x9eG\x83F\xbb\xc3\x03\xbcq!\x9cZ\xfdM\xd1gN\xeb\x03A\xfa\xf5KÞˁ\x13\xcd\f\xbc\xa1\x10\xc0b\xccu\xb4\xf3\xcc\u007f˛\xa9\x05:\x9d\xef\x04.|\xf2\x19>\xf9\xbd\xf1\x1cL_r\xc52\x9ev\x87\x85\x83R\u007f;zB\xf81\xed z_\x96\x9e\xfd\xadB}\x00\xb5G\xddz\f3\xdf\x11xA3\x95h\v\xb7\x82\xfe\xf0_\x90\x0f\x1c\xe7V\xe0\xe0\xa3\xf4&,\nv\xb0F\x82\xe3d^4\xb4v\xea\xcd\xc5\x01#]\xe3\x89\x0fՌ\x8e\xbc\x9f\xf3=S\x8b\xf0\xdf7t8=x\x985\xdb\xef\x12@\x9c\x1fBL\x80L-\xaaO;\x80\x9a-\xa2\u007f\xafPb.\x98H\xf6\xa2Ҋ\xe4ߣ8\xfe\x84\xa2\xf8\x13\x82\x8a\xd3\u008ad4\xa5\x14\xbf\xbfKp\xf1\x8e\xe1\xc5{\x04\x18\xe7\x85\x183 \aE\xed)\xe5\xeaI\x87\xab\xc9\xe7\v)\x87\xa3\xf3G\x00\xd3e\xe8\t\xe5\xe7\t\x87\x03s+M(3?\xad\xbc<\x01\x87\xef\x14|\xbcS\xf8\xf1\x1e\x01\xc8\xfb\x86 \xb3A\xc8,\xe7L\xbe>;\xbb\xact\x8ez2\x19\x9f\xcaj\x93L6\x88\x17\xfas\x0e\xbe\xa8\xad\xefHq\xbdz\xaei,\xa5\xdc|\xfd\x99\xc1\xcf\\\xe6\x9e\x1e\x8e\xa9:v\x9c\xee\x04\xa2\xfa\xf7Ʃh\xfd\xb38\xd0\xc1\xa1\x82\xc1\x92i\xba4j}\xf0\a\x93f\t\x0f,\xdb\xf5;\u008e\x19\xd8(]D\x1d\xa6\xeb\xe6D\xe6\xb6\x1e\xe5\x9e\\/\x01~R͡W\xf7F\x05ËR\x1c\\\x1c\x00\xd7\xfd!\xe71@\x94y\x8cd\xa5٩\xfa\xba\x9b\x99X\xef\xa9\xdf;rxW_v\x93\tU\xe5\r\xf4\x11\xe21y\x80\xc7\x17\xf2I蚐\xac\xbd2%x\x1du\xcc7\xbcQ\xe5\xc7\xcb\x1f\xe6\x19\xab4\xdb\xe2/\xca\xdf;4\x87\x89~\xefޥSAWԇ\xeb\xf5\xb7\x171\x1b\x1an@\x1a\x00kkf\x824\xb4\xe7\x9cn\x951%2!\u007f֊\x99\xcdY#\xae\xa2\x83\xceQ\x86\xf4!\x83\xb9\x8c\x82\xe8m%U\x05\xae\xa2\x83\xbeY\xfb]r_t\xad\xe0\xcc6\x1e]\x9f\xa6\x92\xbd\xe3\xeb`\xad\xb3Ƣ\x89x\xf5\xf2\x02>\xe3\xb1\xf3\xeb\v\x921\xa7\xfch\xecB_\xd7e%\x1f\xb5\xdaj4NJo\x01\u007f\xad}\x8a\xa9w\x8fL[\xceD\x10\xecHφN\x91wa\xb08\xf8\xa5\x8e\xee!\xf2\xe2\x13:\x0fe\xd4M\x8d\xcaO\x19\xf6:G\x9fЭͱp\xe9E\x9e\n\x92ת\xb2=\x9b\xd1ڜ\b\x9b\xd5s.ᳲX\xa7\xeey\x1f\xa6c64v\x81\x9b\x8d\xd2֧t\x16\v\xe0\x9b\xe0\xf6F\xe0:\xe5GG\x8f\xfe\x96^ත\xbeh\xd5\x14E\xb4\x9a\xb4-]\xe5U\xb0\x83/NeY\xe6\xa2*\xbc5\x96\x89\x88!\xfb\xa6b7\x8a/\x1c\x0fc\xfe\u05c8\xc3}\x84\xf0U\xb7\u007fsC@#\xc8\x04\xcec\x8e>\x1e\xf0F<*\xd4@%\xe5(\xe1Msk\x9d\xe1\xec\x9e͂u\xa6R\b0\xcex\x8c\xdc\a9%\xdd\xf4ީ\x99\xd5x\xae\xb8\x1f\xc86\x9dǴT\u061crdY\x13\nF\xb6\xe5?N\xe3\xa6\x1e\xebH\x99\xed\x98\xdc:\xa6Ҫ\xda\xeej\xbe\x1cq\x81\xc6R\xad\x95[\x14\x94\xa2\xda:V\x0f\xe7b\xb6Ҳ\x93k\v'eyg\xb9,{\x1d]i\xc8\xfd\xd77\xc5߆\x1b\x1e\x17\x1b\xad\x8aE\xa0\x05\x1dg݄\x1c\x98\xe6\xca\x05zv\x17E\xb9k\xedUj\xc4\x06e\x89\x12\x98\t\xebI\xf8rn\x9a\xacS\t)˴M\r\x1f\x9fz\x9dg\"G\x82\x1c_\xefS\xc8\xf0\xf9/\b\xef\x87w\xf6߀Ჾ\xa4\xdeg\x10=+\x18\x17Pj\xa4\xa4L\xf4\xa4\xf2(\x14\xec\x05~\xfd\xe5\xff}c\xbe}c\xa7\x1eR\x82\x87\x97A\xf7A\x19\xb6\x93\xf2\x16bp\xf8#\xf8\xf8\x13\xdf\xf8\xc3\xd3̭\xfa\xcf\xff\xf0\xf2\xea}\x92s\xfaa\xd2/%\x97\xb3q0\xe1\x13\x96\x1a3\x16u\xaf\x00\x1e\x05:o\xc4 \xf6]\xde\x0f'\xb9=\xfb\xf3\xa2\xf5K\x86\xea\xf5\xdfO\xb8\x8c\x1f\xb7?/H\u007f\xb7\b\xfd\xb2\xbb{cZr\xb9\x9d\x93\xb1\xff\b\xdd\"!z\x80\x10\t\xd2#\xdbh\xc2\xf6\xd9 \xbd\x13\xa3\xd7k\x1c\xb9\xd6|\x10\xb7_(J\x8fځ\xa3\x87\xa4@\xf3\x8el\x87\x99\u00936\xf3ɲ\f\x1d\xbb~\x1e\xfe\x9d\x94\xebk\xfaQ\xff)\x14\xfa\x99)\xe9ͭ\xb9\x83\xff\xfc\xaf+\b\xa9\xf5\x97\xfao\x9e\xb8\x87\xff\x17\x00\x00\xff\xff\x0f!\x8a\x00Sf\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKo#\xb9\x11\xbe\xebW\x14f\x0f\xbe\x8cZ\xb3\xc9!\x81.\x81F\x93\x00\x83x\xd6\xc6\xc8q\x0eI\x80\xa5Ȓ\xc45\x9b\xec\xf0!\xad\x12\xe4\xbf\aŇ\xba\xd5ݲ\xe4A\xb2ˋ->\x8aU_\xbdٓ\xe9t:a\x8d|F\xeb\xa4\xd1s`\x8dğ=j\xfa媗\u07fbJ\x9a\xd9\xfe\xfbɋ\xd4b\x0e\xcb༩\xbf\xa23\xc1r\xfc\x84\x1b\xa9\xa5\x97FOj\xf4L0\xcf\xe6\x13\x00\xa6\xb5\xf1\x8c\xa6\x1d\xfd\x04\xe0F{k\x94B;ݢ\xae^\xc2\x1a\xd7A*\x816\x12/W\xef?T\xbf\xab>L\x00\xb8\xc5x\xfcI\xd6\xe8<\xab\x9b9\xe8\xa0\xd4\x04@\xb3\x1a\xe7\xb0f\xfc%4\xce\x1b˶\xa8\fOwU{ThM%\xcd\xc45\xc8\xe9\xea\xad5\xa1\x99C\xbb\x90(d\xb6\x92H\x1f#\xb1U\"v\x9f\x89\xc5u%\x9d\xff\xf3\xe5=\xf7\xd2\xf9\xb8\xafQ\xc12u\x89\xad\xb8\xc5\xed\x8c\xf5?\xb4WOa\xedTZ\x91z\x1b\x14\xb3\x17\x8eO\x00\x1c7\r\xce!\x9en\x18G1\x01ȘEjS`BD-0\xf5h\xa5\xf6h\x97F\x85Z\x9f\xee\x12踕\x8d\x8f('Y \v\x03E\x1ap\x9e\xf9\xe0\xc0\x05\xbe\x03\xe6`\xb1gR\xb1\xb5\xc2\xd9_4+\xffGz\x00?9\xa3\x1f\x99\xdf͡J\xa7\xaaf\xc7\\YM:z\xec\xcc\xf8#\t༕z;\xc6\xd2=s\xfe\x99))NZ\a\xe9\xc0\xef\x10\x14s\x1e\x8c\xb6e,\x1e?\x97ț\x1c(\xfb[ƪ\x82E\xf6\\\xb3\x81\x0f \xa4\xa3\x02\xc0E\xa2C\xb0\xa8<\xa3\xf59x\x1b\xde$>7z#\xb7C\xa1\xbb5\xcd%\x8b\xb9B\xba\x87\xdc2\xdeD\xa1\x89\xac\xa3\xb1f/\x05\xda)\xf9\x87\xdcH\x9e9\t6e\xae\x8dD%\xdcP\xd2\v^\x16E\xb1(ȫ\x99\xba\xa2\xc3\xe5ic,\x8d\x99\xd4ɂ[\x021\xd8\xd8:\xa7T\xedQ\x8bS5rƍ\x89Qˡ\x80\x83\xf4\xbb\x14\x0e\u0558\xdf\xc1\xab\xbeG\xe3\x05\x8fc\xd3=ޟvH;S\x02Ep\xc8-\xfahm\xa8\xc8|Ȕ*\x80/\xc1ŀڏ\x13e\xc4B\xad\x9c~\xc1\xe3\x10h\xb8\xa6\xdc\\\xc2\\g\xf9\x8eJ\xe7°\xc5\rZ\xd4~4\xa8Sgb5z\x8cq]\x18\xee(\xa4sl\xbc\x9b\x99=ڽ\xc4\xc3\xec`\xec\x8b\xd4\xdb)\x01>\xcd\x1e4\x8bm\xc5\xec\xbb\xf8\xe7\x82\xc8O\x0f\x9f\x1e\xe6\xb0\x10\x02\x8cߡ%\xadm\x82*\x86֩o\xde\xc7\x1c\xfb\x1e\x82\x14\u007f\xb8\xfb\x16\\L\x93<\xe7\x06lV\xd1\xfa\x8fT\xa8E\xa6\b\xa2UҊ\xb1@\x99\x92\x94]gm\xa6X3f\x88c\x15fwP`\xa2\f2\x16Q_p\x18L_q\xb3\\\xec^\xf1\xb1RHK-$\xa7B\xec\xdc7J\x83!\xce\xea\xed\x11\xc1\xfa\x15\xf8\xa5\x880.x\x12 \xe7\xc3+\x1c?t\xf7\xb6mY\nO9\xc79\xf4T@9\xd0H9\x90\xd9!r1(p\xa35y\xa37\xc0N\xa1\xee\xce\xf5c\xfc\x1b#\xc4:\xf0\x17\x1c\x01~ \xcaǸ\xb1`\x9c\x8e\x11/\xc1a\f\xbe\xd7\u0600\xeb6\xce\xd9\x12\xed-\xbc,\x17\xb4\xf1\x94&\x19,\x17\xb0\x0eZ(,\x1c\x1dv\xa8\xa9C\x90\x9b\xe3\xf8]4\x9e\xeeW\x05\xd5Xa\xe4\x1a\xbf`;.C\x8a\xe1sX\x1fGj\x82\x1b\x84l,n\xe4\xcf7\b\xf9\x187\x16\xc0\x1b\xe6w \xb5\x93\x02\x81\x8d\xc0\x9f\x8a\xb5\v\x82\x9e\xf2\xffC\x8e\"ߠ\x9e\u05fc=\xb1\xf3\x16\x87/\x18_\xf1\x9fǼ\xed\x84B\xf9\x9d#\xffy-xɏG%ڟ\x1e\f\xfe\x94*,>\x92*Ϙy\x1e\x9ex\xa5R+\xcf\x16c\xceLu\x81\xb1\x16]c\xb4\xa0\xe6\xe9\xb6:\xade\xf9\u007fW\xad\x8d\xabuz\x1e\xe5zkE\v7\xb5*\xf1\x89\xe6\xcd\xcdJz\xb8\xea\xb6\x02f\xed\xa8Sl\xfb\x95\x9e\x8c\xbfH\x9b\xf2\xaeӧP?\xac!\xe8X\xa9Ō_\xc1\xdf5|\xa2ޖ\xb2\x93\x98\x13\xdfv\xcc\x00\xa4\x03m\x0et\xbcC/\x92\x00\xa3S\xbe\xa6n\x8di\x91\x9b\xe1\xb8t\x90JQƶX\x9b\xfdhƦBӢ:\x02sd:\xfb\xdfT\x1f\xaaw\xbfZ\x17\xa4\x98\xf3\xd4Ԡ\xf8\x8a{9|\xe5\x19\xa2{?8Q\x1c\xff\xe4\x0e\xf4\xe3\xc7\xd2,\xcfl\xde\xf6\xe3\b\x18\x1b\xa9\xa8\x16\x1c\x89\x13m\xc50|\x8f\xfc\xb8\xba\xbfs\xb1\x84G\xed\xc7ʾ\x03Z\x8c\x1d\x13\n\xaa\xe2M~\x97\bΣ\x1d1\x80\x93\xf6\xa2\xceA\x19\xbd\xed9N\x1a\xf9\x95\x82*\xb4dPƂ@O\xa9Io\x81\xef\x98\xdeb\xfb\n\x95\xf9\u007f\x9dS2\x9f\x9eʹ\x16\"\xf5%\xf3\xb8I\xa3Or\xacL\x1f\xbc\x00\xb7\x9b\xc7_\u007f\v\xf7E\xb3\x17ۜ+\xb8\x0f\xf6\x97,M\xa0N}\xfb\"\u070eooo\x87\xcf\xcd7 \xf1ַ\xf0W\xde5\xe0\xc0\\\xfb*\xfe\xeb\xe1PS\xb5z\xb5\x04\xfe\x92v\xa5\xe7\xc3|\x04\xd8\xda\x04\xff\x9agލ\x19t~\xee\u007f\v\x8f\xf1#Ƶ\"\x83\xf6\x14\x8d\xf0`\xa9\x95l_\xc5bP\x18\xcb-\xb7?/-z\xdfZ\xbak\xc3/17\xc85\x9ak\a\x93)_v\xf4\x9aA\xee΄\xf5饸p\x9e36\xfc\xfb?\x936yS\x86l<\x8a\x1f\xfa\x9f\xdaޥ\x00R\xbe\x97ş\x9c\xaa\x9a\xf4\xad\x10\xfe\xf6\x8fI\xba\x18\xc5s\xf9\xc0E\x93\xff\r\x00\x00\xff\xff\x04\x0e\x95\xf5\xa5\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\x80\xef\xfa\x15\x98}\x0f{y%\xefN\x0f\xed\xe8\xd6\xcd\xee!\xd36\xe3I2\xb9tz\xa0I\xd8\xe2F\"Y\x00t\xeav\xfa\xdf;$%\u007f\xc8v6=\x947\x81 \x00>\xf8\x10\xab\xba\xae+\x15\xec\x13\x12[\xefZP\xc1\xe2\x1f\x82.}q\xf3\xfc\x037\xd6/\xb6\x1f\xabg\xebL\v7\x91\xc5\x0f\xf7\xc8>\x92\xc6ϸ\xb6Ί\xf5\xae\x1aP\x94Q\xa2\xda\n@9\xe7E%1\xa7O\x00흐\xef{\xa4z\x83\xaey\x8e+\\E\xdb\x1b\xa4l|r\xbd\xfd\xd0|\xdf|\xa8\x004a>\xfeh\adQCh\xc1ž\xaf\x00\x9c\x1a\xb0\x05\x83=\n\xae\x94~\x8e\x81\xf0\xf7\x88,\xdcl\xb1G\xf2\x8d\xf5\x15\a\xd4\xc9\xf1\x86|\f-\x1c6\xca\xf91\xa8r\xa1\xcf\xd9ԧl꾘ʻ\xbde\xf9\xe9\x9a\xc6\xcfv\xd4\n}$\xd5_\x0e(+\xb0u\x9b\xd8+\xba\xa8R\x01\xb0\xf6\x01[\xb8Ka\x05\xa5\xd1T\x00#\x8f\x1cf\rʘLX\xf5K\xb2N\x90n|\x1f\x87\x89l\r\x06Y\x93\r\x92\t>v\x98\xaf\b~\r\xd2!\x14w \x1eV8F`\xf29\x80\xaf\xec\xddRI\xd7B\x93x5E5\x052*\x14ԟ\xe6b٥\x80YȺ͵\x10X\x94D\x9e\x82\xc8~\xadw@G|O\x03\xc8\xfaM\xe8\x14\x9fz\u007f\xc8\x1b\xd7<\x17\x9d\xed\xc7BZw8\xa8v\xd4\xf5\x01ݏ\xcbۧ\xef\x1eN\xc4p\x1a\xeb\x85ԂePS\xa4\t\\\xa1\x06\xde!x\x82\xc1\xd3D\x95\x9b\xbd\xd1@> \x89\x9dJ\xab\xac\xa3\xae:\x92\xceBx\x9f\xa2,Z`R;!ghc\x11\xa0\x19/V`Z\x06\xc2@\xc8\xe8J\x83\x9d\x18\x86\xa4\xa4\x1c\xf8\xd5W\xd4\xd2\xc0\x03R2\x03\xdc\xf9؛ԅ[$\x01B\xed7\xce\xfe\xb9\xb7\xcd\xe9\x9e\xc9i\xaf䐟i\xe5\xa2s\xaa\x87\xad\xea#\xfe\x1f\x9430\xa8\x1d\x10&/\x10ݑ\xbd\xac\xc2\r\xfc\x920Y\xb7\xf6-t\"\x81\xdb\xc5bce\x9a&\xda\x0fCtVv\x8b<\x18\xec*\x8a'^\x18\xdcb\xbf`\xbb\xa9\x15\xe9\xce\nj\x89\x84\v\x15l\x9dCwy\xa24\x83\xf9\x1f\x8d\xf3\x87ߟ\xc4zV e\xe5F\u007f%\x03\xa9\xcdK\xda\xcb\xd1r\x8b\x03\xe8$Jt\xee\xbf<<\xc2\xe4:'cN?s?\x1c\xe4C\n\x120\xeb\xd6H%\x89k\xf2C\xb6\x89\xce\x04o\x9d\xe4\x0f\xdd[ts\xfc\x1cW\x83\x15\x9eJ2媁\x9b>\xd1U]ו\n\xe6\x16#\x19\xef֠\x82\xc1/\x8cN\xbe\xa8\xb9\xfb\x91\x1a\xe3W\xbb7՝q\xed\x1a\xae\x12\xb1\x1f\xae\x91|\x8a\x1a\xdf\xe2\xd68\xc3ƻj@V\xadb\xb5\xae\x00\x94s\x9e\x95L\x93|\x02h\xef8zk1\xd6\x1d\xba\xe6.mp\x93\x8cm1f\xe7\xd3ѻ\xd7\xcd\x0f\xcd\xeb\n@G\xcc\xdb?\x9b\x01\x89\xd5\x10\xd6\xe0\x92\xb5\x15\x80S\x03\xae\xa1\xf5\xf7\xcez\xd5F\xfc+!15;\xb4\x18}c|E\x01\xb5\x1c\xdaE\x9f\xc2\x1a\x0e\ve\xef\b\xa8\\\xe6\xed\xe8溸\xc9+\xd6\x10\xff\xb2\xb4\xfa\xc1\x8c\x16\xc1\xa6\xa8\xec9\x88\xbcH\xc6uɪx\xb6\\\x01\x90\xf6\x01\xd7\xf0Q`\x04\xa5\xb1\xad\x00ƻgX\xf5x\xbbݛ\xe2J\xf78\xa8\x82\x17\xc0\at?}z\u007f\xfb\xfd\xcd\xc94@\x8b\xa4\xa3\t\x9c#8\xc3\f\x86@\xc1\x88\x00\xd8\xefA\x81r\xa0\"\x9b\xad\xd2\f\xdb\xe8\a\xd8(}\x97\xc2\xde+\x80\xdf\xfc\x89\x9a\x81\xd8G\xd5\xe1+\xa0\xa4{P⯘\x82\xf5\x1dl\x8d\xc5f\xbf)D\x1f0\xb2\x99\xa2\\\xc6\x11\xb9\x8efg\xc0_\xca݊\x15\xb4\xc2*$\xe0\x1e\xa7\xf8`;\x86\x03\xfc\x16\xb87\x04\x11CDBWxv\xe2\x18\xc4H\xb9\xf1\x06\r\xdc`\x147@\xbdO\xb6\x152\xee02DԾs\xe6\xef\xbdo\x92\bɡV\xf1D\x87\xc30\x8e1:ea\xa7l\xc2W\xa0\\\v\x83z\x80\x889N\xc9\x1d\xf9\xcb&\xd4\xc0\xaf>\"\x18\xb7\xf5k\xe8\x99\x03\xadW\xab\xce\xf0TT\xda\x0fCr\x86\x1fV\xb9>\xcc&\xb1\x8f\xb4jq\x87vE\xa6\xabUԽaԜ\"\xaeT0u\x86\xeera5C\xfbM\x1cː^\x9e`\xe5\a\xa1\x19q4\xae;ZȜ\u007f$\x03\xc2\xfaB\x98\xb2\xb5\xdc\xe2\x10h\x99\x92\xe8\\\xbf\xbb\xf9\f\xd3\xd19\x19\xf3\xe8\x17\xe6\xec7\xd2!\x05\x120\xe3\xb6\x18K\x123\xf3\xc4'\xba6x\xe38\u007fhk\xd0\xcd\xc3Oi3\x18\xa6\x89̒\xab\x06\xae\xb2\xd2\xc0\x06!\x85V1\xb6\r\xbcwp\xa5\x06\xb4W\x8a\xf0\u007fO\x80D\x9aj\t\xec\xf3Rp,\x92s\xe3\x12\xb5\xa3\x85I\xc9.\xe4kV\xea7\x01\xb5dO\x02(;\xcd\xd6\xe8\\\x1a\xb0\xf5\x11ԡ\xf2\xc7\x006'\x9e\x97+7\x83S\xb1C\x9e\xcfΰ|\xceFr\xfc}\xafN\x85\xe6[l\xbaF\xb4\x82F E=\xbek\xce<^\xc6\x00\x8b\xec]D2\x91X\xc2 q\x15)\x10\x91:\xc6t~\xb4\ftiX>\xa0\x86\x9f3\xe6\x0f\xbe{t\xfd\xca;\x16\xba?jt\xebm\x1a\xf0Ʃ@\xbd\u007f\xc2\xf6=\xe3\xf0<\xcb\xe9A\xde?R\xe7\x86\xd7(R\x8e\x97/1\x1a\\#%{\xe1\xb8\v\xb4\x9eF~\xbe\x9eΑ<\x80S\x8edK\xd1t\x04i\v\xa2CF:\xc8˽\xe1~\xd1#\xc0}ot\x9f7\xe6\x04\x8br\x11ym\xb2\x0e|=|\xa9\v\x13q\x81du&\xdf´\x80?\x9b\xbeP͗\x0e\xa8\xc7\n{\x96\"\xb0\xe2D_\xa1\t\xd9~\n\xb5N1\xa2\xe3\xd1K~#\xe7\x1b\x9e+\nS%\xfdv\xfd\xe1\tex{\xb0\xcc]\xa02\xae\xa0\t\x11k2\x9d\xbc\xec\xb2&ڐk\xf6<\x18e\x9cv\x1a\xa7\x81Z\xcc(~\t&f\x05|\x02\u2efda\x110t\xe5q\x9a\xf7R\xd9!R~\xf8\xb5\x9a\xb7\x1c26\b-Zdla\xf3P\x94\xf8\x81\x18\x87s\xdc[\x1f\a\xc5k\x90G\xabf\xb3@#\xe9w\xd5\xc6\xe2\x1a8\xa6K,[\xbcx\xe8\x15-\x94\xe1ɝ?\x89\xcd\x121\xf6\xc5\xf8(3\xe0\xa2^\xd6\xf0\x11\xef\x17f?E\xaf\x91\b\xcf\xcb\xe8\xe2M\x16\x8b\xe0l\x92\xa4\xb3h\x8f\xa246\xac\xc73i\xb3\xef\x94&\xc4c)\xc1?\xffV\x87\xaaRZc`l?\xce\u007f\x14^\xbc8\xe9\xfc\xf3\xa7\xf6\xae5\xe5\x1f\a~\xff\xa3*\ac{;5\xf42\xf9_\x00\x00\x00\xff\xff\xcbT\xc3P]\r\x00\x00"), diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 2bbd43f4fd..7d83606c89 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -238,6 +238,8 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque backupScheduleName := backup.GetLabels()[velerov1api.ScheduleNameLabel] r.metrics.RegisterBackupDeletionAttempt(backupScheduleName) + var errs []string + pluginManager := r.newPluginManager(log) defer pluginManager.CleanupClients() @@ -275,9 +277,9 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err != nil { log.WithError(err).Errorf("Unable to download tarball for backup %s, skipping associated DeleteItemAction plugins", backup.Name) } else { - defer closeAndRemoveFile(backupFile, c.logger) + defer closeAndRemoveFile(backupFile, r.logger) - deleteItemResolvedActions, err = deleteItemActionResolver.ResolveActions(c.helper) + deleteItemResolvedActions, err = deleteItemActionResolver.ResolveActions(r.discoveryHelper) if err != nil { return ctrl.Result{}, errors.Wrap(err, "error resolving delete item deleteItemActions") } @@ -285,9 +287,8 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque ctx := &delete.Context{ Backup: backup, BackupReader: backupFile, - Actions: actions, - Log: c.logger, - DiscoveryHelper: c.helper, + Log: r.logger, + DiscoveryHelper: r.discoveryHelper, Filesystem: filesystem.NewFileSystem(), DeleteItemResolvedActions: deleteItemResolvedActions, ItemSnapshots: itemSnapshots, @@ -302,8 +303,6 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque } } - var errs []string - if backupStore != nil { log.Info("Removing PV snapshots") diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index f3416ce21f..c62041494f 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -20,18 +20,20 @@ import ( "bytes" "fmt" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" + "github.com/vmware-tanzu/velero/pkg/builder" + "io/ioutil" "testing" "time" "context" - "io/ioutil" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "strings" - "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -123,7 +125,7 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "error getting the backup store")) }) - +} func TestBackupDeletionControllerProcessRequest(t *testing.T) { t.Run("missing spec.backupName", func(t *testing.T) { dbr := defaultTestDbr() @@ -148,9 +150,7 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { // add the backup to the tracker so the execution of reconcile doesn't progress // past checking for an in-progress backup. this makes validation easier. - td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName, &pkgbackup.Request{}) - - require.NoError(t, td.sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(td.req)) + td.controller.backupTracker.Add(td.req.Namespace, input.Spec.BackupName, &pkgbackup.Request{}) existing := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ @@ -201,9 +201,7 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { dbr := defaultTestDbr() td := setupBackupDeletionControllerTest(t, dbr) - td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName, &pkgbackup.Request{}) - - td.controller.backupTracker.Add(td.req.Namespace, dbr.Spec.BackupName) + td.controller.backupTracker.Add(td.req.Namespace, dbr.Spec.BackupName, &pkgbackup.Request{}) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 9b04443cea..554e34e7c5 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -475,7 +475,6 @@ func (c *restoreController) runValidatedRestore(restore *api.Restore, info backu return errors.Wrap(err, "error getting item snapshotters") } } - itemSnapshotterResolver := framework.NewItemSnapshotterResolver(itemSnapshotters) backupFile, err := downloadToTempFile(restore.Spec.BackupName, info.backupStore, restoreLog) if err != nil { @@ -518,7 +517,7 @@ func (c *restoreController) runValidatedRestore(restore *api.Restore, info backu VolumeSnapshots: volumeSnapshots, BackupReader: backupFile, } - restoreWarnings, restoreErrors := c.restorer.RestoreWithResolvers(restoreReq, restoreItemActionResolver, itemSnapshotterResolver, + restoreWarnings, restoreErrors := c.restorer.RestoreWithResolvers(restoreReq, restoreItemActionResolver, itemSnapshotters, c.snapshotLocationLister, pluginManager) // log errors and warnings to the restore log diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index d7ee568a5c..31fe0f1daa 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -895,11 +895,11 @@ func (r *fakeRestorer) Restore( func (r *fakeRestorer) RestoreWithResolvers(req pkgrestore.Request, resolver framework.RestoreItemActionResolver, - itemSnapshotterResolver framework.ItemSnapshotterResolver, + itemSnapshotters []isv1.ItemSnapshotter, snapshotLocationLister listers.VolumeSnapshotLocationLister, volumeSnapshotterGetter pkgrestore.VolumeSnapshotterGetter, ) (pkgrestore.Result, pkgrestore.Result) { - res := r.Called(req.Log, req.Restore, req.Backup, req.BackupReader, resolver, itemSnapshotterResolver, + res := r.Called(req.Log, req.Restore, req.Backup, req.BackupReader, resolver, itemSnapshotters, snapshotLocationLister, volumeSnapshotterGetter) r.calledWithArg = *req.Restore diff --git a/pkg/plugin/clientmgmt/util.go b/pkg/plugin/clientmgmt/util.go index 6615f852e2..2b0b536fef 100644 --- a/pkg/plugin/clientmgmt/util.go +++ b/pkg/plugin/clientmgmt/util.go @@ -1,24 +1,10 @@ package clientmgmt import ( - "github.com/vmware-tanzu/velero/pkg/plugin/framework" isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" "github.com/vmware-tanzu/velero/pkg/volume" ) -// ItemSnapshotterForSnapshotFromResolved - Gets the ItemSnapshotter that was used to take a snapshot. We have two for different array types for convenience since -// Go doesn't allow us to cast arrays easily -func ItemSnapshotterForSnapshotFromResolved(itemSnapshot *volume.ItemSnapshot, resolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction) isv1.ItemSnapshotter { - var itemSnapshotters []isv1.ItemSnapshotter - for _, resolvedAction := range resolvedItemSnapshotters { - itemSnapshotter, ok := resolvedAction.ItemSnapshotter.(LocalItemSnapshotter) - if ok { - itemSnapshotters = append(itemSnapshotters, itemSnapshotter) - } - } - return ItemSnapshotterForSnapshot(itemSnapshot, itemSnapshotters) -} - func ItemSnapshotterForSnapshot(itemSnapshot *volume.ItemSnapshot, itemSnapshotters []isv1.ItemSnapshotter) isv1.ItemSnapshotter { for _, checkItemSnapshotter := range itemSnapshotters { localCheckItemSnapshotter, ok := checkItemSnapshotter.(LocalItemSnapshotter) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index a818d6dbc8..56d2f8046a 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -95,7 +95,7 @@ type Restorer interface { RestoreWithResolvers( req Request, restoreItemActionResolver framework.RestoreItemActionResolver, - itemSnapshotterResolver framework.ItemSnapshotterResolver, + itemSnapshotters []isv1.ItemSnapshotter, snapshotLocationLister listers.VolumeSnapshotLocationLister, volumeSnapshotterGetter VolumeSnapshotterGetter, ) (Result, Result) @@ -166,14 +166,14 @@ func (kr *kubernetesRestorer) Restore( volumeSnapshotterGetter VolumeSnapshotterGetter, ) (Result, Result) { resolver := framework.NewRestoreItemActionResolver(actions) - snapshotItemResolver := framework.NewItemSnapshotterResolver(nil) - return kr.RestoreWithResolvers(req, resolver, snapshotItemResolver, snapshotLocationLister, volumeSnapshotterGetter) + itemSnapshotters := []isv1.ItemSnapshotter{} + return kr.RestoreWithResolvers(req, resolver, itemSnapshotters, snapshotLocationLister, volumeSnapshotterGetter) } func (kr *kubernetesRestorer) RestoreWithResolvers( req Request, restoreItemActionResolver framework.RestoreItemActionResolver, - itemSnapshotterResolver framework.ItemSnapshotterResolver, + itemSnapshotters []isv1.ItemSnapshotter, snapshotLocationLister listers.VolumeSnapshotLocationLister, volumeSnapshotterGetter VolumeSnapshotterGetter, ) (Result, Result) { @@ -219,11 +219,6 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( return Result{}, Result{Velero: []string{err.Error()}} } - resolvedItemSnapshotterActions, err := itemSnapshotterResolver.ResolveActions(kr.discoveryHelper) - if err != nil { - return Result{}, Result{Velero: []string{err.Error()}} - } - podVolumeTimeout := kr.resticTimeout if val := req.Restore.Annotations[velerov1api.PodVolumeOperationTimeoutAnnotation]; val != "" { parsed, err := time.ParseDuration(val) @@ -284,7 +279,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( fileSystem: kr.fileSystem, namespaceClient: kr.namespaceClient, restoreItemActions: resolvedActions, - itemSnapshotterActions: resolvedItemSnapshotterActions, + itemSnapshotters: itemSnapshotters, volumeSnapshotterGetter: volumeSnapshotterGetter, resticRestorer: resticRestorer, resticErrs: make(chan error), @@ -327,7 +322,7 @@ type restoreContext struct { fileSystem filesystem.Interface namespaceClient corev1.NamespaceInterface restoreItemActions []framework.RestoreItemResolvedAction - itemSnapshotterActions []framework.ItemSnapshotterResolvedAction + itemSnapshotters []isv1.ItemSnapshotter volumeSnapshotterGetter VolumeSnapshotterGetter resticRestorer restic.Restorer resticWaitGroup sync.WaitGroup @@ -1000,7 +995,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } } if itemSnapshot != nil { - itemSnapshotter := clientmgmt.ItemSnapshotterForSnapshotFromResolved(itemSnapshot, ctx.itemSnapshotterActions) + itemSnapshotter := clientmgmt.ItemSnapshotterForSnapshot(itemSnapshot, ctx.itemSnapshotters) if itemSnapshotter != nil { cii := isv1.CreateItemInput{ SnapshottedItem: obj,