corso/src/cli/backup/backup.go
neha_gupta 0a17a72800
fetch repoID from kopia is not found in config (#3578)
<!-- 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
2023-06-15 11:31:57 +05:30

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, " ")
}