Merge pull request #13859 from Bonial-International-GmbH/pjungermann/catalog-modules/new-backend-api

feat(catalog/bitbucketCloud): schedule via config + backend-plugin-api support
This commit is contained in:
Johan Haals
2022-10-05 12:52:18 +02:00
committed by GitHub
20 changed files with 685 additions and 29 deletions
@@ -3,10 +3,12 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { TaskRunner } from '@backstage/backend-tasks';
// @public
@@ -18,7 +20,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
config: Config,
options: {
logger: Logger;
schedule: TaskRunner;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
},
): BitbucketCloudEntityProvider[];
// (undocumented)
@@ -28,4 +31,9 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
// (undocumented)
refresh(logger: Logger): Promise<void>;
}
// @alpha (undocumented)
export const bitbucketCloudEntityProviderCatalogModule: (
options?: undefined,
) => BackendFeature;
```
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks';
export interface Config {
catalog?: {
/**
@@ -53,6 +55,10 @@ export interface Config {
*/
projectKey?: RegExp;
};
/**
* (Optional) TaskScheduleDefinition for the discovery.
*/
schedule?: TaskScheduleDefinitionConfig;
}
| Record<
string,
@@ -83,6 +89,10 @@ export interface Config {
*/
projectKey?: RegExp;
};
/**
* (Optional) TaskScheduleDefinition for the discovery.
*/
schedule?: TaskScheduleDefinitionConfig;
}
>;
};
@@ -7,6 +7,7 @@
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
@@ -23,20 +24,22 @@
"backstage"
],
"scripts": {
"build": "backstage-cli package build",
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"clean": "backstage-cli package clean",
"start": "backstage-cli package start"
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-bitbucket-cloud-common": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
@@ -44,11 +47,13 @@
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"luxon": "^3.0.0",
"msw": "^0.47.0"
},
"files": [
"dist",
"config.d.ts"
"alpha",
"config.d.ts",
"dist"
],
"configSchema": "config.d.ts"
}
@@ -15,7 +15,11 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
import {
PluginTaskScheduler,
TaskInvocationDefinition,
TaskRunner,
} from '@backstage/backend-tasks';
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
@@ -77,6 +81,75 @@ describe('BitbucketCloudEntityProvider', () => {
);
});
it('fail without schedule and scheduler', () => {
const config = new ConfigReader({
catalog: {
providers: {
bitbucketCloud: {
workspace: 'test-ws',
},
},
},
});
expect(() =>
BitbucketCloudEntityProvider.fromConfig(config, {
logger,
}),
).toThrow('Either schedule or scheduler must be provided.');
});
it('fail with scheduler but no schedule config', () => {
const scheduler = jest.fn() as unknown as PluginTaskScheduler;
const config = new ConfigReader({
catalog: {
providers: {
bitbucketCloud: {
workspace: 'test-ws',
},
},
},
});
expect(() =>
BitbucketCloudEntityProvider.fromConfig(config, {
logger,
scheduler,
}),
).toThrow(
'No schedule provided neither via code nor config for bitbucketCloud-provider:default.',
);
});
it('single simple provider config with schedule in config', () => {
const scheduler = {
createScheduledTaskRunner: (_: any) => jest.fn(),
} as unknown as PluginTaskScheduler;
const config = new ConfigReader({
catalog: {
providers: {
bitbucketCloud: {
workspace: 'test-ws',
schedule: {
frequency: 'PT30M',
timeout: 'PT3M',
},
},
},
},
});
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
logger,
scheduler,
});
expect(providers).toHaveLength(1);
expect(providers[0].getProviderName()).toEqual(
'bitbucketCloud-provider:default',
);
});
it('multiple provider configs', () => {
const schedule = new PersistingTaskRunner();
const config = new ConfigReader({
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { TaskRunner } from '@backstage/backend-tasks';
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import {
BitbucketCloudIntegration,
@@ -58,7 +58,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
config: Config,
options: {
logger: Logger;
schedule: TaskRunner;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
},
): BitbucketCloudEntityProvider[] {
const integrations = ScmIntegrations.fromConfig(config);
@@ -69,29 +70,42 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
throw new Error('No integration for bitbucket.org available');
}
return readProviderConfigs(config).map(
providerConfig =>
new BitbucketCloudEntityProvider(
providerConfig,
integration,
options.logger,
options.schedule,
),
);
if (!options.schedule && !options.scheduler) {
throw new Error('Either schedule or scheduler must be provided.');
}
return readProviderConfigs(config).map(providerConfig => {
if (!options.schedule && !providerConfig.schedule) {
throw new Error(
`No schedule provided neither via code nor config for bitbucketCloud-provider:${providerConfig.id}.`,
);
}
const taskRunner =
options.schedule ??
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
return new BitbucketCloudEntityProvider(
providerConfig,
integration,
options.logger,
taskRunner,
);
});
}
private constructor(
config: BitbucketCloudEntityProviderConfig,
integration: BitbucketCloudIntegration,
logger: Logger,
schedule: TaskRunner,
taskRunner: TaskRunner,
) {
this.client = BitbucketCloudClient.fromConfig(integration.config);
this.config = config;
this.logger = logger.child({
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(schedule);
this.scheduleFn = this.createScheduleFn(taskRunner);
}
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
@@ -15,6 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readProviderConfigs } from './BitbucketCloudEntityProviderConfig';
describe('readProviderConfigs', () => {
@@ -68,13 +69,22 @@ describe('readProviderConfigs', () => {
repoSlug: 'repoSlug.*filter',
},
},
providerWithSchedule: {
workspace: 'test-ws5',
schedule: {
frequency: 'PT30M',
timeout: {
minutes: 3,
},
},
},
},
},
},
});
const providerConfigs = readProviderConfigs(config);
expect(providerConfigs).toHaveLength(4);
expect(providerConfigs).toHaveLength(5);
expect(providerConfigs[0]).toEqual({
id: 'providerWorkspaceOnly',
workspace: 'test-ws1',
@@ -111,5 +121,20 @@ describe('readProviderConfigs', () => {
repoSlug: /^repoSlug.*filter$/,
},
});
expect(providerConfigs[4]).toEqual({
id: 'providerWithSchedule',
workspace: 'test-ws5',
catalogPath: '/catalog-info.yaml',
filters: {
projectKey: undefined,
repoSlug: undefined,
},
schedule: {
frequency: Duration.fromISO('PT30M'),
timeout: {
minutes: 3,
},
},
});
});
});
@@ -14,6 +14,10 @@
* limitations under the License.
*/
import {
readTaskScheduleDefinitionFromConfig,
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
const DEFAULT_CATALOG_PATH = '/catalog-info.yaml';
@@ -27,6 +31,7 @@ export type BitbucketCloudEntityProviderConfig = {
projectKey?: RegExp;
repoSlug?: RegExp;
};
schedule?: TaskScheduleDefinition;
};
export function readProviderConfigs(
@@ -61,6 +66,10 @@ function readProviderConfig(
const projectKeyPattern = config.getOptionalString('filters.projectKey');
const repoSlugPattern = config.getOptionalString('filters.repoSlug');
const schedule = config.has('schedule')
? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule'))
: undefined;
return {
id,
catalogPath,
@@ -71,6 +80,7 @@ function readProviderConfig(
: undefined,
repoSlug: repoSlugPattern ? compileRegExp(repoSlugPattern) : undefined,
},
schedule,
};
}
@@ -21,3 +21,4 @@
*/
export { BitbucketCloudEntityProvider } from './BitbucketCloudEntityProvider';
export { bitbucketCloudEntityProviderCatalogModule } from './service/BitbucketCloudEntityProviderCatalogModule';
@@ -0,0 +1,84 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import {
configServiceRef,
loggerServiceRef,
schedulerServiceRef,
} from '@backstage/backend-plugin-api';
import {
PluginTaskScheduler,
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
import { startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule';
import { Duration } from 'luxon';
import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider';
describe('bitbucketCloudEntityProviderCatalogModule', () => {
it('should register provider at the catalog extension point', async () => {
let addedProviders: Array<BitbucketCloudEntityProvider> | undefined;
let usedSchedule: TaskScheduleDefinition | undefined;
const extensionPoint = {
addEntityProvider: (providers: any) => {
addedProviders = providers;
},
};
const runner = jest.fn();
const scheduler = {
createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => {
usedSchedule = schedule;
return runner;
},
} as unknown as PluginTaskScheduler;
const config = new ConfigReader({
catalog: {
providers: {
bitbucketCloud: {
schedule: {
frequency: 'P1M',
timeout: 'PT3M',
},
workspace: 'test-ws',
},
},
},
});
await startTestBackend({
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
services: [
[configServiceRef, config],
[loggerServiceRef, getVoidLogger()],
[schedulerServiceRef, scheduler],
],
features: [bitbucketCloudEntityProviderCatalogModule()],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'bitbucketCloud-provider:default',
);
expect(runner).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,52 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
configServiceRef,
createBackendModule,
loggerServiceRef,
loggerToWinstonLogger,
schedulerServiceRef,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider';
/**
* @alpha
*/
export const bitbucketCloudEntityProviderCatalogModule = createBackendModule({
pluginId: 'catalog',
moduleId: 'bitbucketCloudEntityProvider',
register(env) {
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
config: configServiceRef,
logger: loggerServiceRef,
scheduler: schedulerServiceRef,
},
async init({ catalog, config, logger, scheduler }) {
const winstonLogger = loggerToWinstonLogger(logger);
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
logger: winstonLogger,
scheduler,
});
catalog.addEntityProvider(providers);
},
});
},
});