Add cli for exchange export (#4641)

<!-- PR description-->

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [x] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/3893

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2023-11-16 10:23:16 +05:30 committed by GitHub
parent 2dfe22dd0c
commit fd9c431bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 14 deletions

View File

@ -105,7 +105,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// Flags addition ordering should follow the order we want them to appear in help and docs: // Flags addition ordering should follow the order we want them to appear in help and docs:
// More generic (ex: --user) and more frequently used flags take precedence. // More generic (ex: --user) and more frequently used flags take precedence.
flags.AddBackupIDFlag(c, true) flags.AddBackupIDFlag(c, true)
flags.AddExchangeDetailsAndRestoreFlags(c) flags.AddExchangeDetailsAndRestoreFlags(c, false)
case deleteCommand: case deleteCommand:
c, fs = utils.AddCommand(cmd, exchangeDeleteCmd()) c, fs = utils.AddCommand(cmd, exchangeDeleteCmd())

109
src/cli/export/exchange.go Normal file
View File

@ -0,0 +1,109 @@
package export
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
)
// called by export.go to map subcommands to provider-specific handling.
func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
var (
c *cobra.Command
fs *pflag.FlagSet
)
switch cmd.Use {
case exportCommand:
c, fs = utils.AddCommand(cmd, exchangeExportCmd())
c.Use = c.Use + " " + exchangeServiceCommandUseSuffix
// Flags addition ordering should follow the order we want them to appear in help and docs:
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddExchangeDetailsAndRestoreFlags(c, true)
flags.AddExportConfigFlags(c)
flags.AddFailFastFlag(c)
}
return c
}
const (
exchangeServiceCommand = "exchange"
exchangeServiceCommandUseSuffix = "<destination> --backup <backupId>"
// TODO(meain): remove message about only supporting email exports once others are added
//nolint:lll
exchangeServiceCommandExportExamples = `> Only email exports are supported as of now.
# Export emails with ID 98765abcdef and 12345abcdef from Alice's last backup (1234abcd...) to my-folder
corso export exchange my-folder --backup 1234abcd-12ab-cd34-56de-1234abcd --email 98765abcdef,12345abcdef
# Export emails with subject containing "Hello world" in the "Inbox" to my-folder
corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd \
--email-subject "Hello world" --email-folder Inbox my-folder`
// TODO(meain): Uncomment once support for these are added
// `# Export an entire calendar to my-folder
// corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd \
// --event-calendar Calendar my-folder
// # Export the contact with ID abdef0101 to my-folder
// corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd --contact abdef0101 my-folder`
)
// `corso export exchange [<flag>...] <destination>`
func exchangeExportCmd() *cobra.Command {
return &cobra.Command{
Use: exchangeServiceCommand,
Short: "Export M365 Exchange service data",
RunE: exportExchangeCmd,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("missing export destination")
}
return nil
},
Example: exchangeServiceCommandExportExamples,
}
}
// processes an exchange service export.
func exportExchangeCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
opts := utils.MakeExchangeOpts(cmd)
if flags.RunModeFV == flags.RunModeFlagTest {
return nil
}
if err := utils.ValidateExchangeRestoreFlags(flags.BackupIDFV, opts); err != nil {
return err
}
sel := utils.IncludeExchangeRestoreDataSelectors(opts)
utils.FilterExchangeRestoreInfoSelectors(sel, opts)
return runExport(
ctx,
cmd,
args,
opts.ExportCfg,
sel.Selector,
flags.BackupIDFV,
"Exchange",
defaultAcceptedFormatTypes)
}

View File

@ -0,0 +1,78 @@
package export
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/cli/flags"
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
cliTD "github.com/alcionai/corso/src/cli/testdata"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/tester"
)
type ExchangeUnitSuite struct {
tester.Suite
}
func TestExchangeUnitSuite(t *testing.T) {
suite.Run(t, &ExchangeUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ExchangeUnitSuite) TestAddExchangeCommands() {
expectUse := exchangeServiceCommand + " " + exchangeServiceCommandUseSuffix
table := []struct {
name string
use string
expectUse string
expectShort string
expectRunE func(*cobra.Command, []string) error
}{
{"export exchange", exportCommand, expectUse, exchangeExportCmd().Short, exportExchangeCmd},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
parent := &cobra.Command{Use: exportCommand}
cmd := cliTD.SetUpCmdHasFlags(
t,
parent,
addExchangeCommands,
[]cliTD.UseCobraCommandFn{
flags.AddAllProviderFlags,
flags.AddAllStorageFlags,
},
flagsTD.WithFlags(
exchangeServiceCommand,
[]string{
flagsTD.RestoreDestination,
"--" + flags.RunModeFN, flags.RunModeFlagTest,
"--" + flags.BackupFN, flagsTD.BackupInput,
"--" + flags.FormatFN, flagsTD.FormatType,
"--" + flags.ArchiveFN,
},
flagsTD.PreparedProviderFlags(),
flagsTD.PreparedStorageFlags()))
cliTD.CheckCmdChild(
t,
parent,
3,
test.expectUse,
test.expectShort,
test.expectRunE)
opts := utils.MakeExchangeOpts(cmd)
assert.Equal(t, flagsTD.BackupInput, flags.BackupIDFV)
assert.Equal(t, flagsTD.Archive, opts.ExportCfg.Archive)
assert.Equal(t, flagsTD.FormatType, opts.ExportCfg.Format)
flagsTD.AssertStorageFlags(t, cmd)
})
}
}

View File

@ -23,8 +23,11 @@ var exportCommands = []func(cmd *cobra.Command) *cobra.Command{
addOneDriveCommands, addOneDriveCommands,
addSharePointCommands, addSharePointCommands,
addGroupsCommands, addGroupsCommands,
addExchangeCommands,
} }
var defaultAcceptedFormatTypes = []string{string(control.DefaultFormat)}
// AddCommands attaches all `corso export * *` commands to the parent. // AddCommands attaches all `corso export * *` commands to the parent.
func AddCommands(cmd *cobra.Command) { func AddCommands(cmd *cobra.Command) {
subCommand := exportCmd() subCommand := exportCmd()
@ -63,8 +66,9 @@ func runExport(
ueco utils.ExportCfgOpts, ueco utils.ExportCfgOpts,
sel selectors.Selector, sel selectors.Selector,
backupID, serviceName string, backupID, serviceName string,
acceptedFormatTypes []string,
) error { ) error {
if err := utils.ValidateExportConfigFlags(&ueco); err != nil { if err := utils.ValidateExportConfigFlags(&ueco, acceptedFormatTypes); err != nil {
return Only(ctx, err) return Only(ctx, err)
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags" "github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/control"
) )
// called by export.go to map subcommands to provider-specific handling. // called by export.go to map subcommands to provider-specific handling.
@ -99,5 +100,18 @@ func exportGroupsCmd(cmd *cobra.Command, args []string) error {
sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts) sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts)
utils.FilterGroupsRestoreInfoSelectors(sel, opts) utils.FilterGroupsRestoreInfoSelectors(sel, opts)
return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "Groups") acceptedGroupsFormatTypes := []string{
string(control.DefaultFormat),
string(control.JSONFormat),
}
return runExport(
ctx,
cmd,
args,
opts.ExportCfg,
sel.Selector,
flags.BackupIDFV,
"Groups",
acceptedGroupsFormatTypes)
} }

View File

@ -90,5 +90,13 @@ func exportOneDriveCmd(cmd *cobra.Command, args []string) error {
sel := utils.IncludeOneDriveRestoreDataSelectors(opts) sel := utils.IncludeOneDriveRestoreDataSelectors(opts)
utils.FilterOneDriveRestoreInfoSelectors(sel, opts) utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "OneDrive") return runExport(
ctx,
cmd,
args,
opts.ExportCfg,
sel.Selector,
flags.BackupIDFV,
"OneDrive",
defaultAcceptedFormatTypes)
} }

View File

@ -94,5 +94,13 @@ func exportSharePointCmd(cmd *cobra.Command, args []string) error {
sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts) sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts)
utils.FilterSharePointRestoreInfoSelectors(sel, opts) utils.FilterSharePointRestoreInfoSelectors(sel, opts)
return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "SharePoint") return runExport(
ctx,
cmd,
args,
opts.ExportCfg,
sel.Selector,
flags.BackupIDFV,
"SharePoint",
defaultAcceptedFormatTypes)
} }

View File

@ -49,7 +49,7 @@ var (
// AddExchangeDetailsAndRestoreFlags adds flags that are common to both the // AddExchangeDetailsAndRestoreFlags adds flags that are common to both the
// details and restore commands. // details and restore commands.
func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) { func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command, emailOnly bool) {
fs := cmd.Flags() fs := cmd.Flags()
// email flags // email flags
@ -78,6 +78,12 @@ func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) {
EmailReceivedBeforeFN, "", EmailReceivedBeforeFN, "",
"Select emails received before this datetime.") "Select emails received before this datetime.")
// NOTE: Only temporary until we add support for exporting the
// others as well in exchange.
if emailOnly {
return
}
// event flags // event flags
fs.StringSliceVar( fs.StringSliceVar(
&EventFV, &EventFV,

View File

@ -27,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
fs.SortFlags = false fs.SortFlags = false
flags.AddBackupIDFlag(c, true) flags.AddBackupIDFlag(c, true)
flags.AddExchangeDetailsAndRestoreFlags(c) flags.AddExchangeDetailsAndRestoreFlags(c, false)
flags.AddRestoreConfigFlags(c, true) flags.AddRestoreConfigFlags(c, true)
flags.AddFailFastFlag(c) flags.AddFailFastFlag(c)
} }

View File

@ -31,6 +31,7 @@ type ExchangeOpts struct {
EventSubject string EventSubject string
RestoreCfg RestoreCfgOpts RestoreCfg RestoreCfgOpts
ExportCfg ExportCfgOpts
Populated flags.PopulatedFlags Populated flags.PopulatedFlags
} }
@ -60,6 +61,7 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts {
EventSubject: flags.EventSubjectFV, EventSubject: flags.EventSubjectFV,
RestoreCfg: makeRestoreCfgOpts(cmd), RestoreCfg: makeRestoreCfgOpts(cmd),
ExportCfg: makeExportCfgOpts(cmd),
// populated contains the list of flags that appear in the // populated contains the list of flags that appear in the
// command, according to pflags. Use this to differentiate // command, according to pflags. Use this to differentiate

View File

@ -45,12 +45,7 @@ func MakeExportConfig(
// ValidateExportConfigFlags ensures all export config flags that utilize // ValidateExportConfigFlags ensures all export config flags that utilize
// enumerated values match a well-known value. // enumerated values match a well-known value.
func ValidateExportConfigFlags(opts *ExportCfgOpts) error { func ValidateExportConfigFlags(opts *ExportCfgOpts, acceptedFormatTypes []string) error {
acceptedFormatTypes := []string{
string(control.DefaultFormat),
string(control.JSONFormat),
}
if _, populated := opts.Populated[flags.FormatFN]; !populated { if _, populated := opts.Populated[flags.FormatFN]; !populated {
opts.Format = string(control.DefaultFormat) opts.Format = string(control.DefaultFormat)
} else if !filters.Equal(acceptedFormatTypes).Compare(opts.Format) { } else if !filters.Equal(acceptedFormatTypes).Compare(opts.Format) {

View File

@ -55,6 +55,11 @@ func (suite *ExportCfgUnitSuite) TestMakeExportConfig() {
} }
func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() { func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() {
acceptedFormatTypes := []string{
string(control.DefaultFormat),
string(control.JSONFormat),
}
table := []struct { table := []struct {
name string name string
input ExportCfgOpts input ExportCfgOpts
@ -100,7 +105,8 @@ func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
err := ValidateExportConfigFlags(&test.input)
err := ValidateExportConfigFlags(&test.input, acceptedFormatTypes)
test.expectErr(t, err, clues.ToCore(err)) test.expectErr(t, err, clues.ToCore(err))
assert.Equal(t, test.expectFormat, control.FormatType(test.input.Format)) assert.Equal(t, test.expectFormat, control.FormatType(test.input.Format))