add fault/clues to graph_conector.go (#2376)

## Description

Refactors error handling in graph_connector.
Also begins some error refactoring in support by
moving StackTraceErrror style funcs into a more
normalized handler in graph/errors.go.  And
removes the (Non)Recoverable error wraps which
we weren't using anyway.

## 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]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-02-08 19:50:07 -07:00 committed by GitHub
parent a440aa9a34
commit 667d2d8e29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 185 deletions

View File

@ -16,6 +16,8 @@ import (
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
D "github.com/alcionai/corso/src/internal/diagnostics" D "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -226,3 +228,46 @@ func (gc *GraphConnector) OneDriveDataCollections(
return collections, allExcludes, errs return collections, allExcludes, errs
} }
// RestoreDataCollections restores data from the specified collections
// into M365 using the GraphAPI.
// SideEffect: gc.status is updated at the completion of operation
func (gc *GraphConnector) RestoreDataCollections(
ctx context.Context,
backupVersion int,
acct account.Account,
selector selectors.Selector,
dest control.RestoreDestination,
opts control.Options,
dcs []data.RestoreCollection,
) (*details.Details, error) {
ctx, end := D.Span(ctx, "connector:restore")
defer end()
var (
status *support.ConnectorOperationStatus
err error
deets = &details.Builder{}
)
creds, err := acct.M365Config()
if err != nil {
return nil, errors.Wrap(err, "malformed azure credentials")
}
switch selector.Service {
case selectors.ServiceExchange:
status, err = exchange.RestoreExchangeDataCollections(ctx, creds, gc.Service, dest, dcs, deets)
case selectors.ServiceOneDrive:
status, err = onedrive.RestoreCollections(ctx, backupVersion, gc.Service, dest, opts, dcs, deets)
case selectors.ServiceSharePoint:
status, err = sharepoint.RestoreCollections(ctx, backupVersion, creds, gc.Service, dest, dcs, deets)
default:
err = errors.Errorf("restore data from service %s not supported", selector.Service.String())
}
gc.incrementAwaitingMessages()
gc.UpdateStatus(status)
return deets.Details(), err
}

View File

@ -291,6 +291,8 @@ func (c Contacts) Serialize(
return nil, fmt.Errorf("expected Contactable, got %T", item) return nil, fmt.Errorf("expected Contactable, got %T", item)
} }
ctx = clues.Add(ctx, "item_id", *contact.GetId())
var ( var (
err error err error
writer = kioser.NewJsonSerializationWriter() writer = kioser.NewJsonSerializationWriter()
@ -299,7 +301,7 @@ func (c Contacts) Serialize(
defer writer.Close() defer writer.Close()
if err = writer.WriteObjectValue("", contact); err != nil { if err = writer.WriteObjectValue("", contact); err != nil {
return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) return nil, clues.Stack(err).WithClues(ctx)
} }
bs, err := writer.GetSerializedContent() bs, err := writer.GetSerializedContent()

View File

@ -340,6 +340,8 @@ func (c Events) Serialize(
return nil, fmt.Errorf("expected Eventable, got %T", item) return nil, fmt.Errorf("expected Eventable, got %T", item)
} }
ctx = clues.Add(ctx, "item_id", *event.GetId())
var ( var (
err error err error
writer = kioser.NewJsonSerializationWriter() writer = kioser.NewJsonSerializationWriter()
@ -348,7 +350,7 @@ func (c Events) Serialize(
defer writer.Close() defer writer.Close()
if err = writer.WriteObjectValue("", event); err != nil { if err = writer.WriteObjectValue("", event); err != nil {
return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) return nil, clues.Stack(err).WithClues(ctx)
} }
bs, err := writer.GetSerializedContent() bs, err := writer.GetSerializedContent()

View File

@ -321,6 +321,8 @@ func (c Mail) Serialize(
return nil, fmt.Errorf("expected Messageable, got %T", item) return nil, fmt.Errorf("expected Messageable, got %T", item)
} }
ctx = clues.Add(ctx, "item_id", *msg.GetId())
var ( var (
err error err error
writer = kioser.NewJsonSerializationWriter() writer = kioser.NewJsonSerializationWriter()
@ -329,7 +331,7 @@ func (c Mail) Serialize(
defer writer.Close() defer writer.Close()
if err = writer.WriteObjectValue("", msg); err != nil { if err = writer.WriteObjectValue("", msg); err != nil {
return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) return nil, clues.Stack(err).WithClues(ctx)
} }
bs, err := writer.GetSerializedContent() bs, err := writer.GetSerializedContent()

View File

@ -2,6 +2,7 @@ package graph
import ( import (
"context" "context"
"fmt"
"net/url" "net/url"
"os" "os"
@ -176,3 +177,50 @@ func hasErrorCode(err error, codes ...string) bool {
return slices.Contains(codes, *oDataError.GetError().GetCode()) return slices.Contains(codes, *oDataError.GetError().GetCode())
} }
// ErrData is a helper function that extracts ODataError metadata from
// the error. If the error is not an ODataError type, returns an empty
// slice. The returned value is guaranteed to be an even-length pairing
// of key, value tuples.
func ErrData(e error) []any {
result := make([]any, 0)
if e == nil {
return result
}
odErr, ok := e.(odataerrors.ODataErrorable)
if !ok {
return result
}
// Get MainError
mainErr := odErr.GetError()
result = appendIf(result, "odataerror_code", mainErr.GetCode())
result = appendIf(result, "odataerror_message", mainErr.GetMessage())
result = appendIf(result, "odataerror_target", mainErr.GetTarget())
for i, d := range mainErr.GetDetails() {
pfx := fmt.Sprintf("odataerror_details_%d_", i)
result = appendIf(result, pfx+"code", d.GetCode())
result = appendIf(result, pfx+"message", d.GetMessage())
result = appendIf(result, pfx+"target", d.GetTarget())
}
inner := mainErr.GetInnererror()
if inner != nil {
result = appendIf(result, "odataerror_inner_cli_req_id", inner.GetClientRequestId())
result = appendIf(result, "odataerror_inner_req_id", inner.GetRequestId())
}
return result
}
func appendIf(a []any, k string, v *string) []any {
if v == nil {
return a
}
return append(a, k, *v)
}

View File

@ -4,11 +4,14 @@ package connector
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"runtime/trace" "runtime/trace"
"strings" "strings"
"sync" "sync"
"github.com/alcionai/clues"
"github.com/hashicorp/go-multierror"
"github.com/microsoft/kiota-abstractions-go/serialization" "github.com/microsoft/kiota-abstractions-go/serialization"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -17,18 +20,12 @@ import (
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/discovery/api" "github.com/alcionai/corso/src/internal/connector/discovery/api"
"github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/connector/sharepoint"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data"
D "github.com/alcionai/corso/src/internal/diagnostics" D "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/selectors"
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -74,7 +71,7 @@ func NewGraphConnector(
) (*GraphConnector, error) { ) (*GraphConnector, error) {
m365, err := acct.M365Config() m365, err := acct.M365Config()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "retrieving m365 account configuration") return nil, clues.Wrap(err, "retrieving m365 account configuration").WithClues(ctx)
} }
gc := GraphConnector{ gc := GraphConnector{
@ -87,12 +84,12 @@ func NewGraphConnector(
gc.Service, err = gc.createService() gc.Service, err = gc.createService()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "creating service connection") return nil, clues.Wrap(err, "creating service connection").WithClues(ctx)
} }
gc.Owners, err = api.NewClient(m365) gc.Owners, err = api.NewClient(m365)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "creating api client") return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
} }
// TODO(ashmrtn): When selectors only encapsulate a single resource owner that // TODO(ashmrtn): When selectors only encapsulate a single resource owner that
@ -174,8 +171,7 @@ func (gc *GraphConnector) setTenantSites(ctx context.Context) error {
gc.tenant, gc.tenant,
sharepoint.GetAllSitesForTenant, sharepoint.GetAllSitesForTenant,
models.CreateSiteCollectionResponseFromDiscriminatorValue, models.CreateSiteCollectionResponseFromDiscriminatorValue,
identifySite, identifySite)
)
if err != nil { if err != nil {
return err return err
} }
@ -194,22 +190,23 @@ const personalSitePath = "sharepoint.com/personal/"
func identifySite(item any) (string, string, error) { func identifySite(item any) (string, string, error) {
m, ok := item.(models.Siteable) m, ok := item.(models.Siteable)
if !ok { if !ok {
return "", "", errors.New("iteration retrieved non-Site item") return "", "", clues.New("iteration retrieved non-Site item").With("item_type", fmt.Sprintf("%T", item))
} }
if m.GetName() == nil { if m.GetName() == nil {
// the built-in site at "https://{tenant-domain}/search" never has a name. // the built-in site at "https://{tenant-domain}/search" never has a name.
if m.GetWebUrl() != nil && strings.HasSuffix(*m.GetWebUrl(), "/search") { if m.GetWebUrl() != nil && strings.HasSuffix(*m.GetWebUrl(), "/search") {
return "", "", errKnownSkippableCase // TODO: pii siteID, on this and all following cases
return "", "", clues.Stack(errKnownSkippableCase).With("site_id", *m.GetId())
} }
return "", "", errors.Errorf("no name for Site: %s", *m.GetId()) return "", "", clues.New("site has no name").With("site_id", *m.GetId())
} }
// personal (ie: oneDrive) sites have to be filtered out server-side. // personal (ie: oneDrive) sites have to be filtered out server-side.
url := m.GetWebUrl() url := m.GetWebUrl()
if url != nil && strings.Contains(*url, personalSitePath) { if url != nil && strings.Contains(*url, personalSitePath) {
return "", "", errKnownSkippableCase return "", "", clues.Stack(errKnownSkippableCase).With("site_id", *m.GetId())
} }
return *m.GetWebUrl(), *m.GetId(), nil return *m.GetWebUrl(), *m.GetId(), nil
@ -261,49 +258,6 @@ func (gc *GraphConnector) UnionSiteIDsAndWebURLs(ctx context.Context, ids, urls
return idsl, nil return idsl, nil
} }
// RestoreDataCollections restores data from the specified collections
// into M365 using the GraphAPI.
// SideEffect: gc.status is updated at the completion of operation
func (gc *GraphConnector) RestoreDataCollections(
ctx context.Context,
backupVersion int,
acct account.Account,
selector selectors.Selector,
dest control.RestoreDestination,
opts control.Options,
dcs []data.RestoreCollection,
) (*details.Details, error) {
ctx, end := D.Span(ctx, "connector:restore")
defer end()
var (
status *support.ConnectorOperationStatus
err error
deets = &details.Builder{}
)
creds, err := acct.M365Config()
if err != nil {
return nil, errors.Wrap(err, "malformed azure credentials")
}
switch selector.Service {
case selectors.ServiceExchange:
status, err = exchange.RestoreExchangeDataCollections(ctx, creds, gc.Service, dest, dcs, deets)
case selectors.ServiceOneDrive:
status, err = onedrive.RestoreCollections(ctx, backupVersion, gc.Service, dest, opts, dcs, deets)
case selectors.ServiceSharePoint:
status, err = sharepoint.RestoreCollections(ctx, backupVersion, creds, gc.Service, dest, dcs, deets)
default:
err = errors.Errorf("restore data from service %s not supported", selector.Service.String())
}
gc.incrementAwaitingMessages()
gc.UpdateStatus(status)
return deets.Details(), err
}
// AwaitStatus waits for all gc tasks to complete and then returns status // AwaitStatus waits for all gc tasks to complete and then returns status
func (gc *GraphConnector) AwaitStatus() *support.ConnectorOperationStatus { func (gc *GraphConnector) AwaitStatus() *support.ConnectorOperationStatus {
defer func() { defer func() {
@ -359,30 +313,27 @@ func getResources(
response, err := query(ctx, gs) response, err := query(ctx, gs)
if err != nil { if err != nil {
return nil, errors.Wrapf( return nil, clues.Wrap(err, "retrieving tenant's resources").
err, WithClues(ctx).
"retrieving resources for tenant %s: %s", WithAll(graph.ErrData(err)...)
tenantID,
support.ConnectorStackErrorTrace(err),
)
} }
errs := &multierror.Error{}
iter, err := msgraphgocore.NewPageIterator(response, gs.Adapter(), parser) iter, err := msgraphgocore.NewPageIterator(response, gs.Adapter(), parser)
if err != nil { if err != nil {
return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) return nil, clues.Stack(err).WithClues(ctx).WithAll(graph.ErrData(err)...)
} }
var iterErrs error
callbackFunc := func(item any) bool { callbackFunc := func(item any) bool {
k, v, err := identify(item) k, v, err := identify(item)
if err != nil { if err != nil {
if errors.Is(err, errKnownSkippableCase) { if !errors.Is(err, errKnownSkippableCase) {
return true errs = multierror.Append(errs, clues.Stack(err).
WithClues(ctx).
With("query_url", gs.Adapter().GetBaseUrl()))
} }
iterErrs = support.WrapAndAppend(gs.Adapter().GetBaseUrl(), err, iterErrs)
return true return true
} }
@ -392,20 +343,8 @@ func getResources(
} }
if err := iter.Iterate(ctx, callbackFunc); err != nil { if err := iter.Iterate(ctx, callbackFunc); err != nil {
return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) return nil, clues.Stack(err).WithClues(ctx).WithAll(graph.ErrData(err)...)
} }
return resources, iterErrs return resources, errs.ErrorOrNil()
}
// IsRecoverableError returns true iff error is a RecoverableGCEerror
func IsRecoverableError(e error) bool {
var recoverable support.RecoverableGCError
return errors.As(e, &recoverable)
}
// IsNonRecoverableError returns true iff error is a NonRecoverableGCEerror
func IsNonRecoverableError(e error) bool {
var nonRecoverable support.NonRecoverableGCError
return errors.As(e, &nonRecoverable)
} }

View File

@ -116,58 +116,6 @@ func (suite *DisconnectedGraphConnectorSuite) TestGraphConnector_Status() {
suite.Equal(2, gc.Status().FolderCount) suite.Equal(2, gc.Status().FolderCount)
} }
func (suite *DisconnectedGraphConnectorSuite) TestGraphConnector_ErrorChecking() {
tests := []struct {
name string
err error
returnRecoverable assert.BoolAssertionFunc
returnNonRecoverable assert.BoolAssertionFunc
}{
{
name: "Neither Option",
err: errors.New("regular error"),
returnRecoverable: assert.False,
returnNonRecoverable: assert.False,
},
{
name: "Validate Recoverable",
err: support.SetRecoverableError(errors.New("Recoverable")),
returnRecoverable: assert.True,
returnNonRecoverable: assert.False,
},
{
name: "Validate NonRecoverable",
err: support.SetNonRecoverableError(errors.New("Non-recoverable")),
returnRecoverable: assert.False,
returnNonRecoverable: assert.True,
},
{
name: "Wrapped Recoverable",
err: support.WrapAndAppend(
"Wrapped Recoverable",
support.SetRecoverableError(errors.New("Recoverable")),
nil),
returnRecoverable: assert.True,
returnNonRecoverable: assert.False,
},
{
name: "On Nil",
err: nil,
returnRecoverable: assert.False,
returnNonRecoverable: assert.False,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
recoverable := IsRecoverableError(test.err)
nonRecoverable := IsNonRecoverableError(test.err)
test.returnRecoverable(suite.T(), recoverable, "Test: %s Recoverable-received %v", test.name, recoverable)
test.returnNonRecoverable(suite.T(), nonRecoverable, "Test: %s non-recoverable: %v", test.name, nonRecoverable)
t.Logf("Is nil: %v", test.err == nil)
})
}
}
func (suite *DisconnectedGraphConnectorSuite) TestVerifyBackupInputs() { func (suite *DisconnectedGraphConnectorSuite) TestVerifyBackupInputs() {
users := []string{ users := []string{
"elliotReid@someHospital.org", "elliotReid@someHospital.org",

View File

@ -8,29 +8,8 @@ import (
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
msgraph_errors "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" msgraph_errors "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common"
) )
// GraphConnector has two types of errors that are exported
// RecoverableGCError is a query error that can be overcome with time
type RecoverableGCError struct {
common.Err
}
func SetRecoverableError(e error) error {
return RecoverableGCError{*common.EncapsulateError(e)}
}
// NonRecoverableGCError is a permanent query error
type NonRecoverableGCError struct {
common.Err
}
func SetNonRecoverableError(e error) error {
return NonRecoverableGCError{*common.EncapsulateError(e)}
}
// WrapErrorAndAppend helper function used to attach identifying information to an error // WrapErrorAndAppend helper function used to attach identifying information to an error
// and return it as a mulitierror // and return it as a mulitierror
func WrapAndAppend(identifier string, e, previous error) error { func WrapAndAppend(identifier string, e, previous error) error {
@ -101,7 +80,7 @@ func ConnectorStackErrorTraceWrap(e error, prefix string) error {
return errors.Wrap(e, prefix) return errors.Wrap(e, prefix)
} }
// ConnectorStackErrorTracew is a helper function that extracts // ConnectorStackErrorTrace is a helper function that extracts
// the stack trace for oDataErrors, if the error has one. // the stack trace for oDataErrors, if the error has one.
func ConnectorStackErrorTrace(e error) string { func ConnectorStackErrorTrace(e error) string {
eMessage := "" eMessage := ""

View File

@ -41,26 +41,6 @@ func (suite *GraphConnectorErrorSuite) TestWrapAndAppend_OnVar() {
suite.True(strings.Contains(received.Error(), id)) suite.True(strings.Contains(received.Error(), id))
} }
func (suite *GraphConnectorErrorSuite) TestAsRecoverableError() {
err := assert.AnError
rcv := RecoverableGCError{}
suite.False(errors.As(err, &rcv))
aRecoverable := SetRecoverableError(err)
suite.True(errors.As(aRecoverable, &rcv))
}
func (suite *GraphConnectorErrorSuite) TestAsNonRecoverableError() {
err := assert.AnError
noRecover := NonRecoverableGCError{}
suite.False(errors.As(err, &noRecover))
nonRecoverable := SetNonRecoverableError(err)
suite.True(errors.As(nonRecoverable, &noRecover))
}
func (suite *GraphConnectorErrorSuite) TestWrapAndAppend_Add3() { func (suite *GraphConnectorErrorSuite) TestWrapAndAppend_Add3() {
errOneTwo := WrapAndAppend("user1", assert.AnError, assert.AnError) errOneTwo := WrapAndAppend("user1", assert.AnError, assert.AnError)
combined := WrapAndAppend("unix36", assert.AnError, errOneTwo) combined := WrapAndAppend("unix36", assert.AnError, errOneTwo)

View File

@ -347,8 +347,7 @@ func generateContainerOfItems(
sel, sel,
dest, dest,
control.Options{RestorePermissions: true}, control.Options{RestorePermissions: true},
dataColls, dataColls)
)
require.NoError(t, err) require.NoError(t, err)
return deets return deets

View File

@ -84,6 +84,11 @@ func (e *Errors) Fail(err error) *Errors {
return e.setErr(err) return e.setErr(err)
} }
// Failed returns true if e.err != nil, signifying a catastrophic failure.
func (e *Errors) Failed() bool {
return e.err != nil
}
// setErr handles setting errors.err. Sync locking gets // setErr handles setting errors.err. Sync locking gets
// handled upstream of this call. // handled upstream of this call.
func (e *Errors) setErr(err error) *Errors { func (e *Errors) setErr(err error) *Errors {
@ -99,6 +104,7 @@ func (e *Errors) setErr(err error) *Errors {
type Adder interface { type Adder interface {
Add(err error) *Errors Add(err error) *Errors
Failed() bool
} }
// Add appends the error to the slice of recoverable and // Add appends the error to the slice of recoverable and

View File

@ -4,7 +4,8 @@ import "github.com/alcionai/corso/src/pkg/fault"
// Adder mocks an adder interface for testing. // Adder mocks an adder interface for testing.
type Adder struct { type Adder struct {
Errs []error FailFast bool
Errs []error
} }
func NewAdder() *Adder { func NewAdder() *Adder {
@ -15,3 +16,7 @@ func (ma *Adder) Add(err error) *fault.Errors {
ma.Errs = append(ma.Errs, err) ma.Errs = append(ma.Errs, err)
return fault.New(true) return fault.New(true)
} }
func (ma *Adder) Failed() bool {
return ma.FailFast && len(ma.Errs) > 0
}