feat(catalog/msgraph): Add option to configure schedule via app-config.yaml
Relates-to: PR #13859 Relates-to: PR #14034 Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
`MicrosoftGraphOrgEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code.
|
||||
|
||||
Please find how to configure the schedule at the config at
|
||||
https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-msgraph#readme
|
||||
@@ -85,6 +85,13 @@ catalog:
|
||||
# in order to add extra information to your groups that can be used on your custom groupTransformers
|
||||
# See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0
|
||||
select: ['id', 'displayName', 'description']
|
||||
schedule: # optional; same options as in TaskScheduleDefinition
|
||||
# supports cron, ISO duration, "human duration" as used in code
|
||||
frequency: { hours: 1 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
timeout: { minutes: 50 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
initialDelay: { seconds: 15},
|
||||
```
|
||||
|
||||
`user.filter` and `userGroupMember.filter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown.
|
||||
@@ -116,13 +123,21 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-msgraph
|
||||
+ builder.addEntityProvider(
|
||||
+ MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ scheduler,
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
|
||||
Instead of configuring the refresh schedule inside the config (per provider instance),
|
||||
you can define it in code (for all of them):
|
||||
|
||||
```diff
|
||||
- scheduler,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { hours: 1 },
|
||||
+ timeout: { minutes: 50 },
|
||||
+ initialDelay: { seconds: 15}
|
||||
+ initialDelay: { seconds: 15},
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
|
||||
## Customize the Processor or Entity Provider
|
||||
@@ -161,10 +176,7 @@ export async function myGroupTransformer(
|
||||
builder.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 5 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
scheduler,
|
||||
+ groupTransformer: myGroupTransformer,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,8 +12,10 @@ import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-backend';
|
||||
import { Logger } from 'winston';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { Response as Response_2 } from 'node-fetch';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
@@ -147,7 +149,8 @@ export type MicrosoftGraphOrgEntityProviderOptions =
|
||||
| MicrosoftGraphOrgEntityProviderLegacyOptions
|
||||
| {
|
||||
logger: Logger;
|
||||
schedule: 'manual' | TaskRunner;
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
organizationTransformer?:
|
||||
@@ -202,6 +205,7 @@ export type MicrosoftGraphProviderConfig = {
|
||||
groupSearch?: string;
|
||||
groupSelect?: string[];
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
+13
-9
@@ -14,14 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks';
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Configuration options for the catalog plugin.
|
||||
*/
|
||||
catalog?: {
|
||||
/**
|
||||
* List of processor-specific options and attributes
|
||||
*/
|
||||
processors?: {
|
||||
/**
|
||||
* MicrosoftGraphOrgReaderProcessor configuration
|
||||
@@ -109,9 +105,7 @@ export interface Config {
|
||||
}>;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List of provider-specific options and attributes
|
||||
*/
|
||||
|
||||
providers?: {
|
||||
/**
|
||||
* MicrosoftGraphOrgEntityProvider configuration.
|
||||
@@ -209,6 +203,11 @@ export interface Config {
|
||||
*/
|
||||
search?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the refresh.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
| Record<
|
||||
string,
|
||||
@@ -292,6 +291,11 @@ export interface Config {
|
||||
*/
|
||||
search?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the refresh.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"luxon": "^3.0.0",
|
||||
"msw": "^0.47.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Duration } from 'luxon';
|
||||
import { readMicrosoftGraphConfig, readProviderConfigs } from './config';
|
||||
|
||||
describe('readMicrosoftGraphConfig', () => {
|
||||
@@ -172,6 +173,12 @@ describe('readProviderConfigs', () => {
|
||||
filter: 'securityEnabled eq false',
|
||||
select: ['id', 'displayName', 'description'],
|
||||
},
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -192,6 +199,12 @@ describe('readProviderConfigs', () => {
|
||||
groupExpand: 'member',
|
||||
groupSelect: ['id', 'displayName', 'description'],
|
||||
groupFilter: 'securityEnabled eq false',
|
||||
schedule: {
|
||||
frequency: Duration.fromISO('PT30M'),
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(actual).toEqual(expected);
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
readTaskScheduleDefinitionFromConfig,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import { trimEnd } from 'lodash';
|
||||
|
||||
@@ -121,6 +125,11 @@ export type MicrosoftGraphProviderConfig = {
|
||||
* Some features like `$expand` are not available for advanced queries, though.
|
||||
*/
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
|
||||
/**
|
||||
* Schedule configuration for refresh tasks.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -296,6 +305,10 @@ export function readProviderConfig(
|
||||
throw new Error(`clientId must be provided when clientSecret is defined.`);
|
||||
}
|
||||
|
||||
const schedule = config.has('schedule')
|
||||
? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule'))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
target,
|
||||
@@ -312,5 +325,6 @@ export function readProviderConfig(
|
||||
queryMode,
|
||||
userGroupMemberFilter,
|
||||
userGroupMemberSearch,
|
||||
schedule,
|
||||
};
|
||||
}
|
||||
|
||||
+207
-58
@@ -14,6 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskInvocationDefinition,
|
||||
TaskRunner,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
@@ -43,10 +48,21 @@ const readMicrosoftGraphOrgMocked = readMicrosoftGraphOrg as jest.Mock<
|
||||
Promise<{ users: UserEntity[]; groups: GroupEntity[] }>
|
||||
>;
|
||||
|
||||
describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
class PersistingTaskRunner implements TaskRunner {
|
||||
private tasks: TaskInvocationDefinition[] = [];
|
||||
|
||||
it('should apply mutation', async () => {
|
||||
getTasks() {
|
||||
return this.tasks;
|
||||
}
|
||||
|
||||
run(task: TaskInvocationDefinition): Promise<void> {
|
||||
this.tasks.push(task);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(MicrosoftGraphClient, 'create')
|
||||
.mockReturnValue({} as unknown as MicrosoftGraphClient);
|
||||
@@ -78,8 +94,65 @@ describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const config = {
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
const logger = getVoidLogger();
|
||||
const taskRunner = new PersistingTaskRunner();
|
||||
const scheduler = {
|
||||
createScheduledTaskRunner: (_: any) => taskRunner,
|
||||
} as unknown as PluginTaskScheduler;
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const expectedMutation = {
|
||||
entities: [
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'msgraph:customProviderId/u1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
},
|
||||
name: 'u1',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'msgraph:customProviderId/g1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
},
|
||||
name: 'g1',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
};
|
||||
|
||||
it('should apply mutation - manual', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
@@ -92,67 +165,143 @@ describe('MicrosoftGraphOrgEntityProvider', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(
|
||||
new ConfigReader(config),
|
||||
{
|
||||
logger: getVoidLogger(),
|
||||
schedule: 'manual',
|
||||
},
|
||||
)[0];
|
||||
|
||||
provider.connect(entityProviderConnection);
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule: 'manual',
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await provider.read();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/u1',
|
||||
},
|
||||
name: 'u1',
|
||||
},
|
||||
spec: {
|
||||
memberOf: [],
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply mutation - schedule', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'msgraph:customProviderId/g1',
|
||||
},
|
||||
name: 'g1',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'msgraph-org-provider:customProviderId',
|
||||
},
|
||||
],
|
||||
type: 'full',
|
||||
},
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule: taskRunner,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = taskRunner.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId:refresh',
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply mutation - scheduler', async () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = taskRunner.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
'MicrosoftGraphOrgEntityProvider:customProviderId:refresh',
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith(
|
||||
expectedMutation,
|
||||
);
|
||||
});
|
||||
|
||||
it('fail without schedule and scheduler', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
}),
|
||||
).toThrow('Either schedule or scheduler must be provided');
|
||||
});
|
||||
|
||||
it('fail with scheduler but no schedule config', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
microsoftGraphOrg: {
|
||||
customProviderId: {
|
||||
target: 'target',
|
||||
tenantId: 'tenantId',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
}),
|
||||
).toThrow(
|
||||
'No schedule provided neither via code nor config for MicrosoftGraphOrgEntityProvider:customProviderId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+31
-12
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
@@ -67,7 +67,13 @@ export type MicrosoftGraphOrgEntityProviderOptions =
|
||||
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
|
||||
* to enable automatic scheduling of tasks.
|
||||
*/
|
||||
schedule: 'manual' | TaskRunner;
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
|
||||
/**
|
||||
* Scheduler used to schedule refreshes based on
|
||||
* the schedule config.
|
||||
*/
|
||||
scheduler?: PluginTaskScheduler;
|
||||
|
||||
/**
|
||||
* The function that transforms a user entry in msgraph to an entity.
|
||||
@@ -168,6 +174,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
];
|
||||
}
|
||||
|
||||
if (!options.schedule && !options.scheduler) {
|
||||
throw new Error('Either schedule or scheduler must be provided.');
|
||||
}
|
||||
|
||||
function getTransformer<T extends Function>(
|
||||
id: string,
|
||||
transformers?: T | Record<string, T>,
|
||||
@@ -180,6 +190,16 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
}
|
||||
|
||||
return readProviderConfigs(configRoot).map(providerConfig => {
|
||||
if (!options.schedule && !providerConfig.schedule) {
|
||||
throw new Error(
|
||||
`No schedule provided neither via code nor config for MicrosoftGraphOrgEntityProvider:${providerConfig.id}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const taskRunner =
|
||||
options.schedule ??
|
||||
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
|
||||
|
||||
const provider = new MicrosoftGraphOrgEntityProvider({
|
||||
id: providerConfig.id,
|
||||
provider: providerConfig,
|
||||
@@ -197,7 +217,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
options.organizationTransformer,
|
||||
),
|
||||
});
|
||||
provider.schedule(options.schedule);
|
||||
|
||||
if (taskRunner !== 'manual') {
|
||||
provider.schedule(taskRunner);
|
||||
}
|
||||
|
||||
return provider;
|
||||
});
|
||||
@@ -238,7 +261,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
provider,
|
||||
});
|
||||
|
||||
result.schedule(options.schedule);
|
||||
if (options.schedule !== 'manual') {
|
||||
result.schedule(options.schedule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -311,16 +336,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
markCommitComplete();
|
||||
}
|
||||
|
||||
private schedule(
|
||||
schedule: MicrosoftGraphOrgEntityProviderOptions['schedule'],
|
||||
) {
|
||||
if (schedule === 'manual') {
|
||||
return;
|
||||
}
|
||||
|
||||
private schedule(taskRunner: TaskRunner) {
|
||||
this.scheduleFn = async () => {
|
||||
const id = `${this.getProviderName()}:refresh`;
|
||||
await schedule.run({
|
||||
await taskRunner.run({
|
||||
id,
|
||||
fn: async () => {
|
||||
const logger = this.options.logger.child({
|
||||
|
||||
Reference in New Issue
Block a user