diff --git a/.changeset/good-cats-tap.md b/.changeset/good-cats-tap.md new file mode 100644 index 0000000000..ab0ef23a42 --- /dev/null +++ b/.changeset/good-cats-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Retry msgraph API calls, due to frequent ETIMEDOUT errors. Also allow disabling fetching user photos. diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 7f5c4b804c..911564d160 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -113,6 +113,7 @@ export class MicrosoftGraphClient { requestRaw( url: string, headers?: Record, + retryCount?: number, ): Promise; } @@ -210,6 +211,7 @@ export type MicrosoftGraphProviderConfig = { groupSearch?: string; groupSelect?: string[]; queryMode?: 'basic' | 'advanced'; + loadUserPhotos?: boolean; schedule?: TaskScheduleDefinition; }; @@ -244,6 +246,7 @@ export function readMicrosoftGraphOrg( userExpand?: string; userFilter?: string; userSelect?: string[]; + loadUserPhotos?: boolean; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index 9d341215cd..5a1f56a03f 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -160,6 +160,11 @@ export interface Config { * E.g. "accountEnabled eq true and userType eq 'member'" */ filter?: string; + /** + * Set to false to not load user photos. + * This can be useful for huge organizations. + */ + loadPhotos?: boolean; }; group?: { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index f1ccc44a99..1233b1e2f9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -216,6 +216,7 @@ export class MicrosoftGraphClient { async requestRaw( url: string, headers?: Record, + retryCount = 2, ): Promise { // Make sure that we always have a valid access token (might be cached) const urlObj = new URL(url); @@ -227,12 +228,19 @@ export class MicrosoftGraphClient { throw new Error('Failed to obtain token from Azure Identity'); } - return await fetch(url, { - headers: { - ...headers, - Authorization: `Bearer ${token.token}`, - }, - }); + try { + return await fetch(url, { + headers: { + ...headers, + Authorization: `Bearer ${token.token}`, + }, + }); + } catch (e: any) { + if (e?.code === 'ETIMEDOUT' && retryCount > 0) { + return this.requestRaw(url, headers, retryCount - 1); + } + throw e; + } } /** diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 1d9592e96f..1545114e45 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -126,6 +126,12 @@ export type MicrosoftGraphProviderConfig = { */ queryMode?: 'basic' | 'advanced'; + /** + * Set to false to not load user photos. + * This can be useful for huge organizations. + */ + loadUserPhotos?: boolean; + /** * Schedule configuration for refresh tasks. */ @@ -280,6 +286,7 @@ export function readProviderConfig( const userExpand = config.getOptionalString('user.expand'); const userFilter = config.getOptionalString('user.filter'); const userSelect = config.getOptionalStringArray('user.select'); + const loadUserPhotos = config.getOptionalBoolean('user.loadPhotos'); const groupExpand = config.getOptionalString('group.expand'); const groupFilter = config.getOptionalString('group.filter'); @@ -335,6 +342,7 @@ export function readProviderConfig( userExpand, userFilter, userSelect, + loadUserPhotos, groupExpand, groupFilter, groupSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index ccf4c51556..68fe2280b9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -49,6 +49,7 @@ export async function readMicrosoftGraphUsers( userExpand?: string; userFilter?: string; userSelect?: string[]; + loadUserPhotos?: boolean; transformer?: UserTransformer; logger: Logger; }, @@ -70,6 +71,7 @@ export async function readMicrosoftGraphUsers( client, users, options.logger, + options.loadUserPhotos, options.transformer, ), }; @@ -82,6 +84,7 @@ export async function readMicrosoftGraphUsersInGroups( userExpand?: string; userFilter?: string; userSelect?: string[]; + loadUserPhotos?: boolean; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -145,6 +148,7 @@ export async function readMicrosoftGraphUsersInGroups( client, userGroupMembers.values(), options.logger, + options.loadUserPhotos, options.transformer, ), }; @@ -366,6 +370,7 @@ export async function readMicrosoftGraphOrg( userExpand?: string; userFilter?: string; userSelect?: string[]; + loadUserPhotos?: boolean; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -391,6 +396,7 @@ export async function readMicrosoftGraphOrg( userSelect: options.userSelect, userGroupMemberFilter: options.userGroupMemberFilter, userGroupMemberSearch: options.userGroupMemberSearch, + loadUserPhotos: options.loadUserPhotos, transformer: options.userTransformer, logger: options.logger, }, @@ -402,6 +408,7 @@ export async function readMicrosoftGraphOrg( userExpand: options.userExpand, userFilter: options.userFilter, userSelect: options.userSelect, + loadUserPhotos: options.loadUserPhotos, transformer: options.userTransformer, logger: options.logger, }); @@ -429,6 +436,7 @@ async function transformUsers( client: MicrosoftGraphClient, users: Iterable | AsyncIterable, logger: Logger, + loadUserPhotos = true, transformer?: UserTransformer, ) { const limiter = limiterFactory(10); @@ -443,12 +451,14 @@ async function transformUsers( limiter(async () => { let userPhoto; try { - userPhoto = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); + if (loadUserPhotos) { + userPhoto = await client.getUserPhotoWithSizeLimit( + user.id!, + // We are limiting the photo size, as users with full resolution photos + // can make the Backstage API slow + 120, + ); + } } catch (e) { logger.warn(`Unable to load user photo for`, { user: user.id,