<!-- PR description--> Some events in have a empty repoID. This could be because of multiple config files used. And new config files might have repoID missing. Solution- Fetch repoID from Kopia is not found locally in the config file. Note: Since the Corso Start event happens before we connect to Kopia, it might have repoID missing if the config file is used for the first time. All the consecutive events will have correct values. Corso start event will start populating correct values if the config file is used once. #### Does this PR need a docs update or release note? - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [ ] 🐛 Bugfix #### 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/3388 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual
349 lines
8.9 KiB
Go
349 lines
8.9 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
|
|
. "github.com/alcionai/corso/src/cli/print"
|
|
"github.com/alcionai/corso/src/cli/utils"
|
|
"github.com/alcionai/corso/src/internal/common/idname"
|
|
"github.com/alcionai/corso/src/internal/data"
|
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
|
"github.com/alcionai/corso/src/pkg/backup"
|
|
"github.com/alcionai/corso/src/pkg/logger"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
"github.com/alcionai/corso/src/pkg/repository"
|
|
"github.com/alcionai/corso/src/pkg/selectors"
|
|
"github.com/alcionai/corso/src/pkg/store"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// adding commands to cobra
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var subCommandFuncs = []func() *cobra.Command{
|
|
createCmd,
|
|
listCmd,
|
|
detailsCmd,
|
|
deleteCmd,
|
|
}
|
|
|
|
var serviceCommands = []func(cmd *cobra.Command) *cobra.Command{
|
|
addExchangeCommands,
|
|
addOneDriveCommands,
|
|
addSharePointCommands,
|
|
}
|
|
|
|
// AddCommands attaches all `corso backup * *` commands to the parent.
|
|
func AddCommands(cmd *cobra.Command) {
|
|
backupC := backupCmd()
|
|
cmd.AddCommand(backupC)
|
|
|
|
for _, sc := range subCommandFuncs {
|
|
subCommand := sc()
|
|
backupC.AddCommand(subCommand)
|
|
|
|
for _, addBackupTo := range serviceCommands {
|
|
addBackupTo(subCommand)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// common flags and flag attachers for commands
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// list output filter flags
|
|
var (
|
|
failedItemsFN = "failed-items"
|
|
listFailedItems string
|
|
skippedItemsFN = "skipped-items"
|
|
listSkippedItems string
|
|
recoveredErrorsFN = "recovered-errors"
|
|
listRecoveredErrors string
|
|
)
|
|
|
|
func addFailedItemsFN(cmd *cobra.Command) {
|
|
cmd.Flags().StringVar(
|
|
&listFailedItems, failedItemsFN, "show",
|
|
"Toggles showing or hiding the list of items that failed.")
|
|
}
|
|
|
|
func addSkippedItemsFN(cmd *cobra.Command) {
|
|
cmd.Flags().StringVar(
|
|
&listSkippedItems, skippedItemsFN, "show",
|
|
"Toggles showing or hiding the list of items that were skipped.")
|
|
}
|
|
|
|
func addRecoveredErrorsFN(cmd *cobra.Command) {
|
|
cmd.Flags().StringVar(
|
|
&listRecoveredErrors, recoveredErrorsFN, "show",
|
|
"Toggles showing or hiding the list of errors which corso recovered from.")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// commands
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// The backup category of commands.
|
|
// `corso backup [<subcommand>] [<flag>...]`
|
|
func backupCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "backup",
|
|
Short: "Backup your service data",
|
|
Long: `Backup the data stored in one of your M365 services.`,
|
|
RunE: handleBackupCmd,
|
|
Args: cobra.NoArgs,
|
|
}
|
|
}
|
|
|
|
// Handler for flat calls to `corso backup`.
|
|
// Produces the same output as `corso backup --help`.
|
|
func handleBackupCmd(cmd *cobra.Command, args []string) error {
|
|
return cmd.Help()
|
|
}
|
|
|
|
// The backup create subcommand.
|
|
// `corso backup create <service> [<flag>...]`
|
|
var createCommand = "create"
|
|
|
|
func createCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: createCommand,
|
|
Short: "Backup an M365 Service",
|
|
RunE: handleCreateCmd,
|
|
Args: cobra.NoArgs,
|
|
}
|
|
}
|
|
|
|
// Handler for calls to `corso backup create`.
|
|
// Produces the same output as `corso backup create --help`.
|
|
func handleCreateCmd(cmd *cobra.Command, args []string) error {
|
|
return cmd.Help()
|
|
}
|
|
|
|
// The backup list subcommand.
|
|
// `corso backup list <service> [<flag>...]`
|
|
var listCommand = "list"
|
|
|
|
func listCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: listCommand,
|
|
Short: "List the history of backups",
|
|
RunE: handleListCmd,
|
|
Args: cobra.NoArgs,
|
|
}
|
|
}
|
|
|
|
// Handler for calls to `corso backup list`.
|
|
// Produces the same output as `corso backup list --help`.
|
|
func handleListCmd(cmd *cobra.Command, args []string) error {
|
|
return cmd.Help()
|
|
}
|
|
|
|
// The backup details subcommand.
|
|
// `corso backup details <service> [<flag>...]`
|
|
var detailsCommand = "details"
|
|
|
|
func detailsCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: detailsCommand,
|
|
Short: "Shows the details of a backup",
|
|
RunE: handleDetailsCmd,
|
|
Args: cobra.NoArgs,
|
|
}
|
|
}
|
|
|
|
// Handler for calls to `corso backup details`.
|
|
// Produces the same output as `corso backup details --help`.
|
|
func handleDetailsCmd(cmd *cobra.Command, args []string) error {
|
|
return cmd.Help()
|
|
}
|
|
|
|
// The backup delete subcommand.
|
|
// `corso backup delete <service> [<flag>...]`
|
|
var deleteCommand = "delete"
|
|
|
|
func deleteCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: deleteCommand,
|
|
Short: "Deletes a backup",
|
|
RunE: handleDeleteCmd,
|
|
Args: cobra.NoArgs,
|
|
}
|
|
}
|
|
|
|
// Handler for calls to `corso backup delete`.
|
|
// Produces the same output as `corso backup delete --help`.
|
|
func handleDeleteCmd(cmd *cobra.Command, args []string) error {
|
|
return cmd.Help()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// common handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// standard set of selector behavior that we want used in the cli
|
|
var defaultSelectorConfig = selectors.Config{OnlyMatchItemNames: true}
|
|
|
|
func runBackups(
|
|
ctx context.Context,
|
|
r repository.Repository,
|
|
serviceName, resourceOwnerType string,
|
|
selectorSet []selectors.Selector,
|
|
ins idname.Cacher,
|
|
) error {
|
|
var (
|
|
bIDs []string
|
|
errs = []error{}
|
|
)
|
|
|
|
for _, discSel := range selectorSet {
|
|
discSel.Configure(defaultSelectorConfig)
|
|
|
|
var (
|
|
owner = discSel.DiscreteOwner
|
|
ictx = clues.Add(ctx, "resource_owner_selected", owner)
|
|
)
|
|
|
|
bo, err := r.NewBackupWithLookup(ictx, discSel, ins)
|
|
if err != nil {
|
|
errs = append(errs, clues.Wrap(err, owner).WithClues(ictx))
|
|
Errf(ictx, "%v\n", err)
|
|
|
|
continue
|
|
}
|
|
|
|
ictx = clues.Add(
|
|
ctx,
|
|
"resource_owner_id", bo.ResourceOwner.ID(),
|
|
"resource_owner_name", bo.ResourceOwner.Name())
|
|
|
|
err = bo.Run(ictx)
|
|
if err != nil {
|
|
if errors.Is(err, graph.ErrServiceNotEnabled) {
|
|
logger.Ctx(ctx).Infow("service not enabled", "resource_owner_name", bo.ResourceOwner.Name())
|
|
|
|
continue
|
|
}
|
|
|
|
errs = append(errs, clues.Wrap(err, owner).WithClues(ictx))
|
|
Errf(ictx, "%v\n", err)
|
|
|
|
continue
|
|
}
|
|
|
|
bIDs = append(bIDs, string(bo.Results.BackupID))
|
|
|
|
if !DisplayJSONFormat() {
|
|
Infof(ctx, "Done\n")
|
|
printBackupStats(ctx, r, string(bo.Results.BackupID))
|
|
} else {
|
|
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
|
|
}
|
|
}
|
|
|
|
bups, berrs := r.Backups(ctx, bIDs)
|
|
if berrs.Failure() != nil {
|
|
return Only(ctx, clues.Wrap(berrs.Failure(), "Unable to retrieve backup results from storage"))
|
|
}
|
|
|
|
Info(ctx, "Completed Backups:")
|
|
backup.PrintAll(ctx, bups)
|
|
|
|
if len(errs) > 0 {
|
|
sb := fmt.Sprintf("%d of %d backups failed:\n", len(errs), len(selectorSet))
|
|
|
|
for i, e := range errs {
|
|
logger.CtxErr(ctx, e).Errorf("Backup %d of %d failed", i+1, len(selectorSet))
|
|
sb += "∙ " + e.Error() + "\n"
|
|
}
|
|
|
|
return Only(ctx, clues.New(sb))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// genericDeleteCommand is a helper function that all services can use
|
|
// for the removal of an entry from the repository
|
|
func genericDeleteCommand(cmd *cobra.Command, bID, designation string, args []string) error {
|
|
if utils.HasNoFlagsAndShownHelp(cmd) {
|
|
return nil
|
|
}
|
|
|
|
ctx := clues.Add(cmd.Context(), "delete_backup_id", bID)
|
|
|
|
r, _, _, err := utils.GetAccountAndConnect(ctx)
|
|
if err != nil {
|
|
return Only(ctx, err)
|
|
}
|
|
|
|
defer utils.CloseRepo(ctx, r)
|
|
|
|
if err := r.DeleteBackup(ctx, bID); err != nil {
|
|
return Only(ctx, clues.Wrap(err, "Deleting backup "+bID))
|
|
}
|
|
|
|
Infof(ctx, "Deleted %s backup %s", designation, bID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// genericListCommand is a helper function that all services can use
|
|
// to display the backup IDs saved within the repository
|
|
func genericListCommand(cmd *cobra.Command, bID string, service path.ServiceType, args []string) error {
|
|
ctx := cmd.Context()
|
|
|
|
r, _, _, err := utils.GetAccountAndConnect(ctx)
|
|
if err != nil {
|
|
return Only(ctx, err)
|
|
}
|
|
|
|
defer utils.CloseRepo(ctx, r)
|
|
|
|
if len(bID) > 0 {
|
|
fe, b, errs := r.GetBackupErrors(ctx, bID)
|
|
if errs.Failure() != nil {
|
|
if errors.Is(errs.Failure(), data.ErrNotFound) {
|
|
return Only(ctx, clues.New("No backup exists with the id "+bID))
|
|
}
|
|
|
|
return Only(ctx, clues.Wrap(errs.Failure(), "Failed to list backup id "+bID))
|
|
}
|
|
|
|
b.Print(ctx)
|
|
fe.PrintItems(ctx, !ifShow(listFailedItems), !ifShow(listSkippedItems), !ifShow(listRecoveredErrors))
|
|
|
|
return nil
|
|
}
|
|
|
|
bs, err := r.BackupsByTag(ctx, store.Service(service))
|
|
if err != nil {
|
|
return Only(ctx, clues.Wrap(err, "Failed to list backups in the repository"))
|
|
}
|
|
|
|
backup.PrintAll(ctx, bs)
|
|
|
|
return nil
|
|
}
|
|
|
|
func ifShow(flag string) bool {
|
|
return strings.ToLower(strings.TrimSpace(flag)) == "show"
|
|
}
|
|
|
|
func printBackupStats(ctx context.Context, r repository.Repository, bid string) {
|
|
b, err := r.Backup(ctx, bid)
|
|
if err != nil {
|
|
logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion")
|
|
}
|
|
|
|
b.ToPrintable().Stats.Print(ctx)
|
|
Info(ctx, " ")
|
|
}
|