add cli flag for to-resource
adds a cli flag for restoring to a different resource (mailbox, user, site) from the one stored in the backup.
This commit is contained in:
parent
4580c8f6c0
commit
effecdd290
@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased] (beta)
|
## [Unreleased] (beta)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Restore commands now accept an optional resource override with the `--to-resource` flag. This allows restores to recreate backup data witthin different mailboxes, sites, and users.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- SharePoint document libraries deleted after the last backup can now be restored.
|
- SharePoint document libraries deleted after the last backup can now be restored.
|
||||||
|
- Restore requires the protected resource to have access to the service being restored.
|
||||||
|
|
||||||
## [v0.11.0] (beta) - 2023-07-18
|
## [v0.11.0] (beta) - 2023-07-18
|
||||||
|
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import (
|
|||||||
const (
|
const (
|
||||||
CollisionsFN = "collisions"
|
CollisionsFN = "collisions"
|
||||||
DestinationFN = "destination"
|
DestinationFN = "destination"
|
||||||
|
ToResourceFN = "to-resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
CollisionsFV string
|
CollisionsFV string
|
||||||
DestinationFV string
|
DestinationFV string
|
||||||
|
ToResourceFV string
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddRestoreConfigFlags adds the restore config flag set.
|
// AddRestoreConfigFlags adds the restore config flag set.
|
||||||
@ -25,5 +27,8 @@ func AddRestoreConfigFlags(cmd *cobra.Command) {
|
|||||||
"Sets the behavior for existing item collisions: "+string(control.Skip)+", "+string(control.Copy)+", or "+string(control.Replace))
|
"Sets the behavior for existing item collisions: "+string(control.Skip)+", "+string(control.Copy)+", or "+string(control.Replace))
|
||||||
fs.StringVar(
|
fs.StringVar(
|
||||||
&DestinationFV, DestinationFN, "",
|
&DestinationFV, DestinationFN, "",
|
||||||
"Overrides the destination where items get restored; '/' places items into their original location")
|
"Overrides the folder where items get restored; '/' places items into their original location")
|
||||||
|
fs.StringVar(
|
||||||
|
&ToResourceFV, ToResourceFN, "",
|
||||||
|
"Overrides the protected resource (mailbox, site, etc) where data gets restored")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,6 +84,7 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
|
|||||||
|
|
||||||
"--" + flags.CollisionsFN, testdata.Collisions,
|
"--" + flags.CollisionsFN, testdata.Collisions,
|
||||||
"--" + flags.DestinationFN, testdata.Destination,
|
"--" + flags.DestinationFN, testdata.Destination,
|
||||||
|
"--" + flags.ToResourceFN, testdata.ToResource,
|
||||||
|
|
||||||
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
||||||
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
||||||
@ -125,6 +126,7 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
|
|||||||
|
|
||||||
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
||||||
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
||||||
|
assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource)
|
||||||
|
|
||||||
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
||||||
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
||||||
|
|||||||
@ -70,6 +70,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
|
|||||||
|
|
||||||
"--" + flags.CollisionsFN, testdata.Collisions,
|
"--" + flags.CollisionsFN, testdata.Collisions,
|
||||||
"--" + flags.DestinationFN, testdata.Destination,
|
"--" + flags.DestinationFN, testdata.Destination,
|
||||||
|
"--" + flags.ToResourceFN, testdata.ToResource,
|
||||||
|
|
||||||
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
||||||
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
||||||
@ -80,6 +81,9 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
|
|||||||
"--" + flags.AzureClientSecretFN, testdata.AzureClientSecret,
|
"--" + flags.AzureClientSecretFN, testdata.AzureClientSecret,
|
||||||
|
|
||||||
"--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase,
|
"--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase,
|
||||||
|
|
||||||
|
// bool flags
|
||||||
|
"--" + flags.RestorePermissionsFN,
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.SetOut(new(bytes.Buffer)) // drop output
|
cmd.SetOut(new(bytes.Buffer)) // drop output
|
||||||
@ -99,6 +103,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
|
|||||||
|
|
||||||
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
||||||
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
||||||
|
assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource)
|
||||||
|
|
||||||
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
||||||
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
||||||
@ -109,6 +114,7 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() {
|
|||||||
assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV)
|
assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV)
|
||||||
|
|
||||||
assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV)
|
assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV)
|
||||||
|
assert.True(t, flags.RestorePermissionsFV)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
|
|||||||
|
|
||||||
"--" + flags.CollisionsFN, testdata.Collisions,
|
"--" + flags.CollisionsFN, testdata.Collisions,
|
||||||
"--" + flags.DestinationFN, testdata.Destination,
|
"--" + flags.DestinationFN, testdata.Destination,
|
||||||
|
"--" + flags.ToResourceFN, testdata.ToResource,
|
||||||
|
|
||||||
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
"--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID,
|
||||||
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
"--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey,
|
||||||
@ -85,6 +86,9 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
|
|||||||
"--" + flags.AzureClientSecretFN, testdata.AzureClientSecret,
|
"--" + flags.AzureClientSecretFN, testdata.AzureClientSecret,
|
||||||
|
|
||||||
"--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase,
|
"--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase,
|
||||||
|
|
||||||
|
// bool flags
|
||||||
|
"--" + flags.RestorePermissionsFN,
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.SetOut(new(bytes.Buffer)) // drop output
|
cmd.SetOut(new(bytes.Buffer)) // drop output
|
||||||
@ -111,6 +115,7 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
|
|||||||
|
|
||||||
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions)
|
||||||
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination)
|
||||||
|
assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource)
|
||||||
|
|
||||||
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV)
|
||||||
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV)
|
||||||
@ -121,6 +126,9 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() {
|
|||||||
assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV)
|
assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV)
|
||||||
|
|
||||||
assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV)
|
assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV)
|
||||||
|
|
||||||
|
// bool flags
|
||||||
|
assert.True(t, flags.RestorePermissionsFV)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type RestoreCfgOpts struct {
|
|||||||
// to the default folder name. Defaults to
|
// to the default folder name. Defaults to
|
||||||
// dttm.HumanReadable.
|
// dttm.HumanReadable.
|
||||||
DTTMFormat dttm.TimeFormat
|
DTTMFormat dttm.TimeFormat
|
||||||
|
ProtectedResource string
|
||||||
RestorePermissions bool
|
RestorePermissions bool
|
||||||
|
|
||||||
Populated flags.PopulatedFlags
|
Populated flags.PopulatedFlags
|
||||||
@ -29,6 +30,7 @@ func makeRestoreCfgOpts(cmd *cobra.Command) RestoreCfgOpts {
|
|||||||
Collisions: flags.CollisionsFV,
|
Collisions: flags.CollisionsFV,
|
||||||
Destination: flags.DestinationFV,
|
Destination: flags.DestinationFV,
|
||||||
DTTMFormat: dttm.HumanReadable,
|
DTTMFormat: dttm.HumanReadable,
|
||||||
|
ProtectedResource: flags.ToResourceFV,
|
||||||
RestorePermissions: flags.RestorePermissionsFV,
|
RestorePermissions: flags.RestorePermissionsFV,
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// populated contains the list of flags that appear in the
|
||||||
@ -69,6 +71,7 @@ func MakeRestoreConfig(
|
|||||||
restoreCfg.Location = opts.Destination
|
restoreCfg.Location = opts.Destination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreCfg.ProtectedResource = opts.ProtectedResource
|
||||||
restoreCfg.IncludePermissions = opts.RestorePermissions
|
restoreCfg.IncludePermissions = opts.RestorePermissions
|
||||||
|
|
||||||
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
|
Infof(ctx, "Restoring to folder %s", restoreCfg.Location)
|
||||||
|
|||||||
1
src/cli/utils/testdata/flags.go
vendored
1
src/cli/utils/testdata/flags.go
vendored
@ -46,6 +46,7 @@ var (
|
|||||||
|
|
||||||
Collisions = "collisions"
|
Collisions = "collisions"
|
||||||
Destination = "destination"
|
Destination = "destination"
|
||||||
|
ToResource = "toResource"
|
||||||
RestorePermissions = true
|
RestorePermissions = true
|
||||||
|
|
||||||
AzureClientID = "testAzureClientId"
|
AzureClientID = "testAzureClientId"
|
||||||
|
|||||||
@ -51,16 +51,15 @@ func ConsumeRestoreCollections(
|
|||||||
ctr *count.Bus,
|
ctr *count.Bus,
|
||||||
) (*support.ControllerOperationStatus, error) {
|
) (*support.ControllerOperationStatus, error) {
|
||||||
var (
|
var (
|
||||||
restoreMetrics support.CollectionMetrics
|
restoreMetrics support.CollectionMetrics
|
||||||
el = errs.Local()
|
el = errs.Local()
|
||||||
caches = NewRestoreCaches(backupDriveIDNames)
|
caches = NewRestoreCaches(backupDriveIDNames)
|
||||||
protectedResourceID = rcc.ProtectedResource.ID()
|
fallbackDriveName = "" // onedrive cannot create drives
|
||||||
fallbackDriveName = "" // onedrive cannot create drives
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "backup_version", rcc.BackupVersion)
|
ctx = clues.Add(ctx, "backup_version", rcc.BackupVersion)
|
||||||
|
|
||||||
err := caches.Populate(ctx, rh, protectedResourceID)
|
err := caches.Populate(ctx, rh, rcc.ProtectedResource.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "initializing restore caches")
|
return nil, clues.Wrap(err, "initializing restore caches")
|
||||||
}
|
}
|
||||||
@ -132,15 +131,14 @@ func RestoreCollection(
|
|||||||
ctr *count.Bus,
|
ctr *count.Bus,
|
||||||
) (support.CollectionMetrics, error) {
|
) (support.CollectionMetrics, error) {
|
||||||
var (
|
var (
|
||||||
metrics = support.CollectionMetrics{}
|
metrics = support.CollectionMetrics{}
|
||||||
directory = dc.FullPath()
|
directory = dc.FullPath()
|
||||||
protectedResourceID = directory.ResourceOwner()
|
el = errs.Local()
|
||||||
el = errs.Local()
|
metricsObjects int64
|
||||||
metricsObjects int64
|
metricsBytes int64
|
||||||
metricsBytes int64
|
metricsSuccess int64
|
||||||
metricsSuccess int64
|
wg sync.WaitGroup
|
||||||
wg sync.WaitGroup
|
complete bool
|
||||||
complete bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, end := diagnostics.Span(ctx, "gc:drive:restoreCollection", diagnostics.Label("path", directory))
|
ctx, end := diagnostics.Span(ctx, "gc:drive:restoreCollection", diagnostics.Label("path", directory))
|
||||||
@ -156,7 +154,7 @@ func RestoreCollection(
|
|||||||
rh,
|
rh,
|
||||||
caches,
|
caches,
|
||||||
drivePath,
|
drivePath,
|
||||||
protectedResourceID,
|
rcc.ProtectedResource.ID(),
|
||||||
fallbackDriveName)
|
fallbackDriveName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return metrics, clues.Wrap(err, "ensuring drive exists")
|
return metrics, clues.Wrap(err, "ensuring drive exists")
|
||||||
|
|||||||
@ -41,14 +41,13 @@ func ConsumeRestoreCollections(
|
|||||||
ctr *count.Bus,
|
ctr *count.Bus,
|
||||||
) (*support.ControllerOperationStatus, error) {
|
) (*support.ControllerOperationStatus, error) {
|
||||||
var (
|
var (
|
||||||
lrh = libraryRestoreHandler{ac}
|
lrh = libraryRestoreHandler{ac}
|
||||||
protectedResourceID = dcs[0].FullPath().ResourceOwner()
|
restoreMetrics support.CollectionMetrics
|
||||||
restoreMetrics support.CollectionMetrics
|
caches = onedrive.NewRestoreCaches(backupDriveIDNames)
|
||||||
caches = onedrive.NewRestoreCaches(backupDriveIDNames)
|
el = errs.Local()
|
||||||
el = errs.Local()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
err := caches.Populate(ctx, lrh, protectedResourceID)
|
err := caches.Populate(ctx, lrh, rcc.ProtectedResource.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "initializing restore caches")
|
return nil, clues.Wrap(err, "initializing restore caches")
|
||||||
}
|
}
|
||||||
@ -69,7 +68,7 @@ func ConsumeRestoreCollections(
|
|||||||
metrics support.CollectionMetrics
|
metrics support.CollectionMetrics
|
||||||
ictx = clues.Add(ctx,
|
ictx = clues.Add(ctx,
|
||||||
"category", category,
|
"category", category,
|
||||||
"restore_location", rcc.RestoreConfig.Location,
|
"restore_location", clues.Hide(rcc.RestoreConfig.Location),
|
||||||
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()),
|
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()),
|
||||||
"full_path", dc.FullPath())
|
"full_path", dc.FullPath())
|
||||||
)
|
)
|
||||||
|
|||||||
@ -108,3 +108,18 @@ the copy of`reports.txt` is named `reports 1.txt`.
|
|||||||
Collisions will entirely replace the current version of the item with the backup
|
Collisions will entirely replace the current version of the item with the backup
|
||||||
version. If multiple existing items collide with the backup item, only one of the
|
version. If multiple existing items collide with the backup item, only one of the
|
||||||
existing items is replaced.
|
existing items is replaced.
|
||||||
|
|
||||||
|
## To resource
|
||||||
|
|
||||||
|
The `--to-resource` flag lets you select which resource will receive the restored data.
|
||||||
|
A resource can be a mailbox, user, or sharepoint site.
|
||||||
|
|
||||||
|
<CodeBlock language="bash">{
|
||||||
|
`corso restore onedrive --backup abcd --to-resource adelev@alcion.ai`
|
||||||
|
}</CodeBlock>
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
* The resource must exist. Corso will not create new mailboxes, users, or sites.
|
||||||
|
* The resource must have access to the service being restored. No restore will be
|
||||||
|
performed for an unlicensed resource.
|
||||||
Loading…
x
Reference in New Issue
Block a user