Fetch calendar by name on 409 ErrorFolderExist (#3318)

Handle 409, `ErrorFolderExists` error in restore path while creating destination calendar. We need to fix this for all m365 services. This PR is focused on calendar only.

Context:
`CreateCalendar()` may fail with ErrorFolderExists under certain error conditions. For e.g. consider below scenario.

1. `CreateCalendar()` does a POST to graph to create restore destination calendar but this fails with 5xx.
2. It's possible that step 1 may have left some dirty state in graph. For e.g. it's possible that the destination folder in step 1 was actually created, but 5xx was returned due to other reasons.
3. So when we reattempt POST in such a scenario, we sometimes observe ErrorFolderExists error .
4. Corso should be resilient to such errors. To fix this, when we encounter such an error, we will do a GET to fetch the restore destination folder and add it to folder cache.


---

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

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
Integration tests and mock tests will be added in a follow up PR. 
- [x] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abhishek Pandey 2023-05-05 12:43:07 -07:00 committed by GitHub
parent ef2083bc20
commit 5b9cd69e29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 1 deletions

View File

@ -99,6 +99,45 @@ func (c Events) GetContainerByID(
return graph.CalendarDisplayable{Calendarable: cal}, nil
}
// GetContainerByName fetches a calendar by name
func (c Events) GetContainerByName(
ctx context.Context,
userID, name string,
) (models.Calendarable, error) {
filter := fmt.Sprintf("name eq '%s'", name)
options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
ctx = clues.Add(ctx, "calendar_name", name)
resp, err := c.Stable.Client().UsersById(userID).Calendars().Get(ctx, options)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
// We only allow the api to match one calendar with provided name.
// Return an error if multiple calendars exist (unlikely) or if no calendar
// is found.
if len(resp.GetValue()) != 1 {
err = clues.New("unexpected number of calendars returned").
With("returned_calendar_count", len(resp.GetValue()))
return nil, err
}
// Sanity check ID and name
cal := resp.GetValue()[0]
cd := CalendarDisplayable{Calendarable: cal}
if err := checkIDAndName(cd); err != nil {
return nil, err
}
return cal, nil
}
// GetItem retrieves an Eventable item.
func (c Events) GetItem(
ctx context.Context,

View File

@ -689,10 +689,20 @@ func establishEventsRestoreLocation(
ctx = clues.Add(ctx, "is_new_cache", isNewCache)
temp, err := ac.Events().CreateCalendar(ctx, user, folders[0])
if err != nil {
if err != nil && !graph.IsErrFolderExists(err) {
return "", err
}
// 409 handling: Fetch folder if it exists and add to cache.
// This is rare, but may happen if CreateCalendar() POST fails with 5xx,
// potentially leaving dirty state in graph.
if graph.IsErrFolderExists(err) {
temp, err = ac.Events().GetContainerByName(ctx, user, folders[0])
if err != nil {
return "", err
}
}
folderID := ptr.Val(temp.GetId())
if isNewCache {

View File

@ -41,6 +41,10 @@ const (
syncFolderNotFound errorCode = "ErrorSyncFolderNotFound"
syncStateInvalid errorCode = "SyncStateInvalid"
syncStateNotFound errorCode = "SyncStateNotFound"
// This error occurs when an attempt is made to create a folder that has
// the same name as another folder in the same parent. Such duplicate folder
// names are not allowed by graph.
folderExists errorCode = "ErrorFolderExists"
)
type errorMessage string
@ -178,6 +182,10 @@ func IsMalwareResp(ctx context.Context, resp *http.Response) bool {
return false
}
func IsErrFolderExists(err error) bool {
return hasErrorCode(err, folderExists)
}
// ---------------------------------------------------------------------------
// error parsers
// ---------------------------------------------------------------------------

View File

@ -300,3 +300,48 @@ func (suite *GraphErrorsUnitSuite) TestMalwareInfo() {
assert.Equal(suite.T(), expect, ItemInfo(&i))
}
func (suite *GraphErrorsUnitSuite) TestIsErrFolderExists() {
table := []struct {
name string
err error
expect assert.BoolAssertionFunc
}{
{
name: "nil",
err: nil,
expect: assert.False,
},
{
name: "non-matching",
err: assert.AnError,
expect: assert.False,
},
{
name: "non-matching oDataErr",
err: odErr("folder doesn't exist"),
expect: assert.False,
},
{
name: "matching oDataErr",
err: odErr(string(folderExists)),
expect: assert.True,
},
// next two tests are to make sure the checks are case insensitive
{
name: "oDataErr camelcase",
err: odErr("ErrorFolderExists"),
expect: assert.True,
},
{
name: "oDataErr lowercase",
err: odErr("errorfolderexists"),
expect: assert.True,
},
}
for _, test := range table {
suite.Run(test.name, func() {
test.expect(suite.T(), IsErrFolderExists(test.err))
})
}
}