update backup cli output (#2903)

changes a couple things in the backup cli output:
* appends the backup id after completion
* prints an error if the backup fails
* replaces multierr with a clues error
* logs each backup failure individually

---

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

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #1970

#### Test Plan

- [x] 💪 Manual
This commit is contained in:
Keepers 2023-03-22 10:01:02 -06:00 committed by GitHub
parent 8bebf579a4
commit 532c5c017b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 94 additions and 131 deletions

View File

@ -2,10 +2,10 @@ package backup
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"github.com/hashicorp/go-multierror" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/config"
"github.com/alcionai/corso/src/cli/options" "github.com/alcionai/corso/src/cli/options"
. "github.com/alcionai/corso/src/cli/print" . "github.com/alcionai/corso/src/cli/print"
@ -14,6 +14,7 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup" "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/path"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
@ -202,48 +203,53 @@ func runBackups(
selectorSet []selectors.Selector, selectorSet []selectors.Selector,
) error { ) error {
var ( var (
merrs *multierror.Error bIDs []model.StableID
bIDs []model.StableID errs = []error{}
) )
for _, discSel := range selectorSet { for _, discSel := range selectorSet {
bo, err := r.NewBackup(ctx, discSel) var (
owner = discSel.DiscreteOwner
bctx = clues.Add(ctx, "resource_owner", owner)
)
bo, err := r.NewBackup(bctx, discSel)
if err != nil { if err != nil {
merrs = multierror.Append(merrs, errors.Wrapf( errs = append(errs, clues.Wrap(err, owner).WithClues(bctx))
err, Errf(bctx, "%v\n", err)
"Failed to initialize %s backup for %s %s",
serviceName,
resourceOwnerType,
discSel.DiscreteOwner))
continue continue
} }
err = bo.Run(ctx) err = bo.Run(bctx)
if err != nil { if err != nil {
merrs = multierror.Append(merrs, errors.Wrapf( errs = append(errs, clues.Wrap(err, owner).WithClues(bctx))
err, Errf(bctx, "%v\n", err)
"Failed to run %s backup for %s %s",
serviceName,
resourceOwnerType,
discSel.DiscreteOwner))
continue continue
} }
bIDs = append(bIDs, bo.Results.BackupID) bIDs = append(bIDs, bo.Results.BackupID)
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
} }
bups, ferrs := r.Backups(ctx, bIDs) bups, berrs := r.Backups(ctx, bIDs)
// TODO: print/log recoverable errors if berrs.Failure() != nil {
if ferrs.Failure() != nil { return Only(ctx, errors.Wrap(berrs.Failure(), "Unable to retrieve backup results from storage"))
return Only(ctx, errors.Wrap(ferrs.Failure(), "Unable to retrieve backup results from storage"))
} }
Info(ctx, "Completed Backups:")
backup.PrintAll(ctx, bups) backup.PrintAll(ctx, bups)
if e := merrs.ErrorOrNil(); e != nil { if len(errs) > 0 {
return Only(ctx, e) 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 return nil

View File

@ -76,17 +76,14 @@ func Only(ctx context.Context, e error) error {
// if s is nil, prints nothing. // if s is nil, prints nothing.
// Prepends the message with "Error: " // Prepends the message with "Error: "
func Err(ctx context.Context, s ...any) { func Err(ctx context.Context, s ...any) {
err(getRootCmd(ctx).ErrOrStderr(), s...) out(getRootCmd(ctx).ErrOrStderr())
} }
// err is the testable core of Err() // Errf prints the params to cobra's error writer (stdErr by default)
func err(w io.Writer, s ...any) { // if s is nil, prints nothing.
if len(s) == 0 { // Prepends the message with "Error: "
return func Errf(ctx context.Context, tmpl string, s ...any) {
} outf(getRootCmd(ctx).ErrOrStderr(), "Error: "+tmpl, s...)
msg := append([]any{"Error: "}, s...)
fmt.Fprint(w, msg...)
} }
// Out prints the params to cobra's output writer (stdOut by default) // Out prints the params to cobra's output writer (stdOut by default)
@ -95,7 +92,25 @@ func Out(ctx context.Context, s ...any) {
out(getRootCmd(ctx).OutOrStdout(), s...) out(getRootCmd(ctx).OutOrStdout(), s...)
} }
// out is the testable core of Out() // Out prints the formatted strings to cobra's output writer (stdOut by default)
// if t is empty, prints nothing.
func Outf(ctx context.Context, t string, s ...any) {
outf(getRootCmd(ctx).OutOrStdout(), t, s...)
}
// Info prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing.
func Info(ctx context.Context, s ...any) {
out(getRootCmd(ctx).ErrOrStderr(), s...)
}
// Info prints the formatted strings to cobra's error writer (stdErr by default)
// if t is empty, prints nothing.
func Infof(ctx context.Context, t string, s ...any) {
outf(getRootCmd(ctx).ErrOrStderr(), t, s...)
}
// out is the testable core of exported print funcs
func out(w io.Writer, s ...any) { func out(w io.Writer, s ...any) {
if len(s) == 0 { if len(s) == 0 {
return return
@ -105,13 +120,7 @@ func out(w io.Writer, s ...any) {
fmt.Fprintf(w, "\n") fmt.Fprintf(w, "\n")
} }
// Out prints the formatted strings to cobra's output writer (stdOut by default) // outf is the testable core of exported print funcs
// if t is empty, prints nothing.
func Outf(ctx context.Context, t string, s ...any) {
outf(getRootCmd(ctx).OutOrStdout(), t, s...)
}
// outf is the testable core of Outf()
func outf(w io.Writer, t string, s ...any) { func outf(w io.Writer, t string, s ...any) {
if len(t) == 0 { if len(t) == 0 {
return return
@ -121,38 +130,6 @@ func outf(w io.Writer, t string, s ...any) {
fmt.Fprintf(w, "\n") fmt.Fprintf(w, "\n")
} }
// Info prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing.
func Info(ctx context.Context, s ...any) {
info(getRootCmd(ctx).ErrOrStderr(), s...)
}
// info is the testable core of Info()
func info(w io.Writer, s ...any) {
if len(s) == 0 {
return
}
fmt.Fprint(w, s...)
fmt.Fprintf(w, "\n")
}
// Info prints the formatted strings to cobra's error writer (stdErr by default)
// if t is empty, prints nothing.
func Infof(ctx context.Context, t string, s ...any) {
infof(getRootCmd(ctx).ErrOrStderr(), t, s...)
}
// infof is the testable core of Infof()
func infof(w io.Writer, t string, s ...any) {
if len(t) == 0 {
return
}
fmt.Fprintf(w, t, s...)
fmt.Fprintf(w, "\n")
}
// --------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------
// Output control for backup list/details // Output control for backup list/details
// --------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------

View File

@ -33,32 +33,22 @@ func (suite *PrintUnitSuite) TestOnly() {
assert.True(t, c.SilenceUsage) assert.True(t, c.SilenceUsage)
} }
func (suite *PrintUnitSuite) TestErr() { func (suite *PrintUnitSuite) TestOut() {
t := suite.T() t := suite.T()
b := bytes.Buffer{} b := bytes.Buffer{}
msg := "I have seen the fnords!" msg := "I have seen the fnords!"
err(&b, msg) out(&b, msg)
assert.Contains(t, b.String(), "Error: ")
assert.Contains(t, b.String(), msg) assert.Contains(t, b.String(), msg)
} }
func (suite *PrintUnitSuite) TestInfo() { func (suite *PrintUnitSuite) TestOutf() {
t := suite.T()
b := bytes.Buffer{}
msg := "I have seen the fnords!"
info(&b, msg)
assert.Contains(t, b.String(), msg)
}
func (suite *PrintUnitSuite) TestInfof() {
t := suite.T() t := suite.T()
b := bytes.Buffer{} b := bytes.Buffer{}
msg := "I have seen the fnords!" msg := "I have seen the fnords!"
msg2 := "smarf" msg2 := "smarf"
infof(&b, msg, msg2) outf(&b, msg, msg2)
bs := b.String() bs := b.String()
assert.Contains(t, bs, msg) assert.Contains(t, bs, msg)
assert.Contains(t, bs, msg2) assert.Contains(t, bs, msg2)

View File

@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -134,32 +133,22 @@ func runPurgeForEachUser(
boundary time.Time, boundary time.Time,
ps ...purger, ps ...purger,
) error { ) error {
var ( users, err := m365.Users(ctx, acct, fault.New(true))
errs error
ferrs = fault.New(false)
)
users, err := m365.Users(ctx, acct, ferrs)
if err != nil { if err != nil {
return clues.Wrap(err, "getting users") return clues.Wrap(err, "getting users")
} }
if len(ferrs.Errors().Recovered) > 0 {
// TODO(keepers): remove multierr
errs = multierror.Append(errs, ferrs.Recovered()...)
}
for _, u := range userOrUsers(user, users) { for _, u := range userOrUsers(user, users) {
Infof(ctx, "\nUser: %s - %s", u.PrincipalName, u.ID) Infof(ctx, "\nUser: %s - %s", u.PrincipalName, u.ID)
for _, p := range ps { for _, p := range ps {
if err := p(ctx, gc, boundary, u.PrincipalName); err != nil { if err := p(ctx, gc, boundary, u.PrincipalName); err != nil {
errs = multierror.Append(errs, err) return err
} }
} }
} }
return errs return nil
} }
// ----- OneDrive // ----- OneDrive
@ -240,10 +229,9 @@ func purgeFolders(
dnTime, err := common.ExtractTime(displayName) dnTime, err := common.ExtractTime(displayName)
if err != nil && !errors.Is(err, common.ErrNoTimeString) { if err != nil && !errors.Is(err, common.ErrNoTimeString) {
err = errors.Wrapf(err, "!! Error: parsing container named [%s]", displayName) err = errors.Wrapf(err, "!! Error: parsing container named [%s]", displayName)
errs = multierror.Append(errs, err)
Info(ctx, err) Info(ctx, err)
continue return err
} }
if !dnTime.Before(boundary) || dnTime == (time.Time{}) { if !dnTime.Before(boundary) || dnTime == (time.Time{}) {
@ -255,7 +243,6 @@ func purgeFolders(
err = deleter(gc.Service, uid, fld) err = deleter(gc.Service, uid, fld)
if err != nil { if err != nil {
err = errors.Wrapf(err, "!! Error") err = errors.Wrapf(err, "!! Error")
errs = multierror.Append(errs, err)
Info(ctx, err) Info(ctx, err)
} }
} }

View File

@ -9,7 +9,6 @@ require (
github.com/aws/aws-xray-sdk-go v1.8.1 github.com/aws/aws-xray-sdk-go v1.8.1
github.com/cenkalti/backoff/v4 v4.2.0 github.com/cenkalti/backoff/v4 v4.2.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1
github.com/kopia/kopia v0.12.2-0.20230123092305-e5387cec0acb github.com/kopia/kopia v0.12.2-0.20230123092305-e5387cec0acb
github.com/microsoft/kiota-abstractions-go v0.18.0 github.com/microsoft/kiota-abstractions-go v0.18.0
github.com/microsoft/kiota-authentication-azure-go v0.6.0 github.com/microsoft/kiota-authentication-azure-go v0.6.0
@ -69,7 +68,6 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect

View File

@ -197,10 +197,6 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ= github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=

View File

@ -60,7 +60,7 @@ func (cfc *contactFolderCache) Populate(
return errors.Wrap(err, "enumerating containers") return errors.Wrap(err, "enumerating containers")
} }
if err := cfc.populatePaths(ctx, false); err != nil { if err := cfc.populatePaths(ctx, false, errs); err != nil {
return errors.Wrap(err, "populating paths") return errors.Wrap(err, "populating paths")
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
@ -193,16 +192,29 @@ func (cr *containerResolver) DestinationNameToID(dest string) string {
return "" return ""
} }
func (cr *containerResolver) populatePaths(ctx context.Context, useIDInPath bool) error { func (cr *containerResolver) populatePaths(
var errs *multierror.Error ctx context.Context,
useIDInPath bool,
errs *fault.Bus,
) error {
var (
el = errs.Local()
lastErr error
)
// Populate all folder paths. // Populate all folder paths.
for _, f := range cr.Items() { for _, f := range cr.Items() {
if el.Failure() != nil {
return el.Failure()
}
_, _, err := cr.IDToPath(ctx, ptr.Val(f.GetId()), useIDInPath) _, _, err := cr.IDToPath(ctx, ptr.Val(f.GetId()), useIDInPath)
if err != nil { if err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "populating path")) err = clues.Wrap(err, "populating path")
el.AddRecoverable(err)
lastErr = err
} }
} }
return errs.ErrorOrNil() return lastErr
} }

View File

@ -370,7 +370,7 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestPopulatePaths() {
t := suite.T() t := suite.T()
err := suite.fc.populatePaths(ctx, false) err := suite.fc.populatePaths(ctx, false, fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
items := suite.fc.Items() items := suite.fc.Items()

View File

@ -81,7 +81,7 @@ func (ecc *eventCalendarCache) Populate(
return errors.Wrap(err, "enumerating containers") return errors.Wrap(err, "enumerating containers")
} }
if err := ecc.populatePaths(ctx, true); err != nil { if err := ecc.populatePaths(ctx, true, errs); err != nil {
return errors.Wrap(err, "establishing calendar paths") return errors.Wrap(err, "establishing calendar paths")
} }

View File

@ -85,7 +85,7 @@ func (mc *mailFolderCache) Populate(
return errors.Wrap(err, "enumerating containers") return errors.Wrap(err, "enumerating containers")
} }
if err := mc.populatePaths(ctx, false); err != nil { if err := mc.populatePaths(ctx, false, errs); err != nil {
return errors.Wrap(err, "populating paths") return errors.Wrap(err, "populating paths")
} }

View File

@ -121,8 +121,7 @@ func SeedWriter(ctx context.Context, w io.Writer, c *config) {
contxt, contxt,
mpb.WithWidth(progressBarWidth), mpb.WithWidth(progressBarWidth),
mpb.WithWaitGroup(&wg), mpb.WithWaitGroup(&wg),
mpb.WithOutput(writer), mpb.WithOutput(writer))
)
} }
// Complete blocks until the progress finishes writing out all data. // Complete blocks until the progress finishes writing out all data.
@ -172,8 +171,7 @@ func Message(ctx context.Context, msgs ...cleanable) {
decor.WC{ decor.WC{
W: len(message) + 1, W: len(message) + 1,
C: decor.DidentRight, C: decor.DidentRight,
})), })))
)
// Complete the bar immediately // Complete the bar immediately
bar.SetTotal(-1, true) bar.SetTotal(-1, true)
@ -210,8 +208,7 @@ func MessageWithCompletion(
mpb.PrependDecorators( mpb.PrependDecorators(
decor.Name(message+":"), decor.Name(message+":"),
decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8})), decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8})),
mpb.BarFillerOnComplete("done"), mpb.BarFillerOnComplete("done"))
)
go listen( go listen(
ctx, ctx,

View File

@ -159,11 +159,11 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
// No return here! We continue down to persistResults, even in case of failure. // No return here! We continue down to persistResults, even in case of failure.
logger.Ctx(ctx). logger.Ctx(ctx).
With("err", err). With("err", err).
Errorw("doing backup", clues.InErr(err).Slice()...) Errorw("running backup", clues.InErr(err).Slice()...)
op.Errors.Fail(errors.Wrap(err, "doing backup")) op.Errors.Fail(errors.Wrap(err, "running backup"))
} }
LogFaultErrors(ctx, op.Errors.Errors(), "doing backup") LogFaultErrors(ctx, op.Errors.Errors(), "running backup")
// ----- // -----
// Persistence // Persistence

View File

@ -149,11 +149,11 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De
// No return here! We continue down to persistResults, even in case of failure. // No return here! We continue down to persistResults, even in case of failure.
logger.Ctx(ctx). logger.Ctx(ctx).
With("err", err). With("err", err).
Errorw("doing restore", clues.InErr(err).Slice()...) Errorw("running restore", clues.InErr(err).Slice()...)
op.Errors.Fail(errors.Wrap(err, "doing restore")) op.Errors.Fail(errors.Wrap(err, "running restore"))
} }
LogFaultErrors(ctx, op.Errors.Errors(), "doing restore") LogFaultErrors(ctx, op.Errors.Errors(), "running restore")
// ----- // -----
// Persistence // Persistence