first step towards having a centralized set of error sentinels that can be passed around corso instead of re-using low level error sentinels like those found in the graph/errors.go file. This PR works as a standalone, and only handles the lowest hanging fruit. SDK consumers will need to change their error enum references, but all api behavior should remain the same. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🤖 Supportability/Tests #### Issue(s) * #4685 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
302 lines
8.5 KiB
Go
302 lines
8.5 KiB
Go
package m365
|
|
|
|
import (
|
|
"context"
|
|
"runtime/trace"
|
|
"sync"
|
|
|
|
"github.com/alcionai/clues"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/idname"
|
|
"github.com/alcionai/corso/src/internal/data"
|
|
"github.com/alcionai/corso/src/internal/m365/resource"
|
|
"github.com/alcionai/corso/src/internal/m365/support"
|
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
|
"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/count"
|
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
)
|
|
|
|
var ErrNoResourceLookup = clues.New("missing resource lookup client")
|
|
|
|
// must comply with BackupProducer and RestoreConsumer
|
|
var (
|
|
_ inject.BackupProducer = &Controller{}
|
|
_ inject.ToServiceHandler = &Controller{}
|
|
)
|
|
|
|
// Controller is a struct used to wrap the GraphServiceClient and
|
|
// GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for
|
|
// bookkeeping and interfacing with other component.
|
|
type Controller struct {
|
|
AC api.Client
|
|
|
|
tenant string
|
|
credentials account.M365Config
|
|
|
|
resourceHandler idname.GetResourceIDAndNamer
|
|
// maps of resource owner ids to names, and names to ids.
|
|
// not guaranteed to be populated, only here as a post-population
|
|
// reference for processes that choose to populate the values.
|
|
IDNameLookup idname.Cacher
|
|
|
|
// wg is used to track completion of tasks
|
|
wg *sync.WaitGroup
|
|
region *trace.Region
|
|
|
|
// mutex used to synchronize updates to `status`
|
|
mu sync.Mutex
|
|
status support.ControllerOperationStatus // contains the status of the last run status
|
|
|
|
// backupDriveIDNames is populated on restore and export. It maps
|
|
// the backup's drive names to their id. Primarily for use when
|
|
// creating or looking up a new drive.
|
|
backupDriveIDNames idname.CacheBuilder
|
|
|
|
// backupSiteIDWebURL is populated on restore and export. It maps
|
|
// the backup's site names to their id. Primarily for use in
|
|
// exports for groups.
|
|
backupSiteIDWebURL idname.CacheBuilder
|
|
}
|
|
|
|
func NewController(
|
|
ctx context.Context,
|
|
acct account.Account,
|
|
pst path.ServiceType,
|
|
co control.Options,
|
|
counter *count.Bus,
|
|
) (*Controller, error) {
|
|
graph.InitializeConcurrencyLimiter(ctx, pst == path.ExchangeService, co.Parallelism.ItemFetch)
|
|
|
|
creds, err := acct.M365Config()
|
|
if err != nil {
|
|
return nil, clues.WrapWC(ctx, err, "retrieving m365 account configuration")
|
|
}
|
|
|
|
ac, err := api.NewClient(creds, co, counter)
|
|
if err != nil {
|
|
return nil, clues.WrapWC(ctx, err, "creating api client")
|
|
}
|
|
|
|
ctrl := Controller{
|
|
AC: ac,
|
|
IDNameLookup: idname.NewCache(nil),
|
|
|
|
credentials: creds,
|
|
tenant: acct.ID(),
|
|
wg: &sync.WaitGroup{},
|
|
backupDriveIDNames: idname.NewCache(nil),
|
|
backupSiteIDWebURL: idname.NewCache(nil),
|
|
}
|
|
|
|
// TODO: remove in favor of calling setResourceHandler in the newServiceHandler once
|
|
// all operations types are populated. When we do that, we can also remove pst from
|
|
// the func params in this call.
|
|
ctrl.setResourceHandler(pst)
|
|
|
|
return &ctrl, nil
|
|
}
|
|
|
|
func (ctrl *Controller) VerifyAccess(ctx context.Context) error {
|
|
return ctrl.AC.Access().GetToken(ctx)
|
|
}
|
|
|
|
func (ctrl *Controller) setResourceHandler(
|
|
serviceInOperation path.ServiceType,
|
|
) {
|
|
var rh *resourceGetter
|
|
|
|
switch serviceInOperation {
|
|
case path.ExchangeService, path.OneDriveService:
|
|
rh = &resourceGetter{
|
|
enum: resource.Users,
|
|
getter: ctrl.AC.Users(),
|
|
}
|
|
case path.GroupsService:
|
|
rh = &resourceGetter{
|
|
enum: resource.Groups,
|
|
getter: ctrl.AC.Groups(),
|
|
}
|
|
case path.SharePointService:
|
|
rh = &resourceGetter{
|
|
enum: resource.Sites,
|
|
getter: ctrl.AC.Sites(),
|
|
}
|
|
}
|
|
|
|
ctrl.resourceHandler = rh
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Processing Status
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// AwaitStatus waits for all tasks to complete and then returns status
|
|
func (ctrl *Controller) Wait() *data.CollectionStats {
|
|
defer func() {
|
|
if ctrl.region != nil {
|
|
ctrl.region.End()
|
|
ctrl.region = nil
|
|
}
|
|
}()
|
|
ctrl.wg.Wait()
|
|
|
|
// clean up and reset statefulness
|
|
dcs := data.CollectionStats{
|
|
Folders: ctrl.status.Folders,
|
|
Objects: ctrl.status.Metrics.Objects,
|
|
Successes: ctrl.status.Metrics.Successes,
|
|
Bytes: ctrl.status.Metrics.Bytes,
|
|
Details: ctrl.status.String(),
|
|
}
|
|
|
|
ctrl.wg = &sync.WaitGroup{}
|
|
ctrl.status = support.ControllerOperationStatus{}
|
|
|
|
return &dcs
|
|
}
|
|
|
|
// UpdateStatus is used by initiated tasks to indicate completion
|
|
func (ctrl *Controller) UpdateStatus(status *support.ControllerOperationStatus) {
|
|
defer ctrl.wg.Done()
|
|
|
|
if status == nil {
|
|
return
|
|
}
|
|
|
|
ctrl.mu.Lock()
|
|
defer ctrl.mu.Unlock()
|
|
ctrl.status = support.MergeStatus(ctrl.status, *status)
|
|
}
|
|
|
|
// Status returns the current status of the controller process.
|
|
func (ctrl *Controller) Status() support.ControllerOperationStatus {
|
|
return ctrl.status
|
|
}
|
|
|
|
// PrintableStatus returns a string formatted version of the status.
|
|
func (ctrl *Controller) PrintableStatus() string {
|
|
return ctrl.status.String()
|
|
}
|
|
|
|
func (ctrl *Controller) incrementAwaitingMessages() {
|
|
ctrl.wg.Add(1)
|
|
}
|
|
|
|
func (ctrl *Controller) CacheItemInfo(dii details.ItemInfo) {
|
|
if dii.Groups != nil {
|
|
ctrl.backupDriveIDNames.Add(dii.Groups.DriveID, dii.Groups.DriveName)
|
|
ctrl.backupSiteIDWebURL.Add(dii.Groups.SiteID, dii.Groups.WebURL)
|
|
}
|
|
|
|
if dii.SharePoint != nil {
|
|
ctrl.backupDriveIDNames.Add(dii.SharePoint.DriveID, dii.SharePoint.DriveName)
|
|
ctrl.backupSiteIDWebURL.Add(dii.SharePoint.SiteID, dii.SharePoint.WebURL)
|
|
}
|
|
|
|
if dii.OneDrive != nil {
|
|
ctrl.backupDriveIDNames.Add(dii.OneDrive.DriveID, dii.OneDrive.DriveName)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resource Lookup Handling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ idname.GetResourceIDAndNamer = &resourceGetter{}
|
|
|
|
type resourceGetter struct {
|
|
enum resource.Category
|
|
getter getIDAndNamer
|
|
}
|
|
|
|
type getIDAndNamer interface {
|
|
GetIDAndName(
|
|
ctx context.Context,
|
|
owner string,
|
|
cc api.CallConfig,
|
|
) (
|
|
ownerID string,
|
|
ownerName string,
|
|
err error,
|
|
)
|
|
}
|
|
|
|
// GetResourceIDAndNameFrom looks up the resource's canonical id and display name.
|
|
// If the resource is present in the idNameSwapper, then that interface's id and
|
|
// name values are returned. As a fallback, the resource calls the discovery
|
|
// api to fetch the user or site using the resource value. This fallback assumes
|
|
// that the resource is a well formed ID or display name of appropriate design
|
|
// (PrincipalName for users, WebURL for sites).
|
|
func (r resourceGetter) GetResourceIDAndNameFrom(
|
|
ctx context.Context,
|
|
owner string,
|
|
ins idname.Cacher,
|
|
) (idname.Provider, error) {
|
|
if ins != nil {
|
|
if n, ok := ins.NameOf(owner); ok {
|
|
return idname.NewProvider(owner, n), nil
|
|
} else if i, ok := ins.IDOf(owner); ok {
|
|
return idname.NewProvider(i, owner), nil
|
|
}
|
|
}
|
|
|
|
ctx = clues.Add(ctx, "owner_identifier", owner)
|
|
|
|
var (
|
|
id, name string
|
|
err error
|
|
)
|
|
|
|
id, name, err = r.getter.GetIDAndName(ctx, owner, api.CallConfig{})
|
|
if err != nil {
|
|
if graph.IsErrUserNotFound(err) {
|
|
return nil, clues.Stack(core.ErrResourceOwnerNotFound, err)
|
|
}
|
|
|
|
if graph.IsErrResourceLocked(err) {
|
|
return nil, clues.Stack(graph.ErrResourceLocked, err)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if len(id) == 0 || len(name) == 0 {
|
|
return nil, clues.Stack(core.ErrResourceOwnerNotFound)
|
|
}
|
|
|
|
return idname.NewProvider(id, name), nil
|
|
}
|
|
|
|
// PopulateProtectedResourceIDAndName takes the provided owner identifier and produces
|
|
// the owner's name and ID from that value. Returns an error if the owner is
|
|
// not recognized by the current tenant.
|
|
//
|
|
// The id-name cacher is optional. Some processes will look up all owners in
|
|
// the tenant before reaching this step. In that case, the data gets handed
|
|
// down for this func to consume instead of performing further queries. The
|
|
// data gets stored inside the controller instance for later re-use.
|
|
func (ctrl *Controller) PopulateProtectedResourceIDAndName(
|
|
ctx context.Context,
|
|
resourceID string, // input value, can be either id or name
|
|
ins idname.Cacher,
|
|
) (idname.Provider, error) {
|
|
if ctrl.resourceHandler == nil {
|
|
return nil, clues.StackWC(ctx, ErrNoResourceLookup)
|
|
}
|
|
|
|
pr, err := ctrl.resourceHandler.GetResourceIDAndNameFrom(ctx, resourceID, ins)
|
|
if err != nil {
|
|
return nil, clues.Wrap(err, "identifying resource owner")
|
|
}
|
|
|
|
ctrl.IDNameLookup = idname.NewCache(map[string]string{pr.ID(): pr.Name()})
|
|
|
|
return pr, nil
|
|
}
|