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
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor
---
Bitbucket Cloud provider: 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://backstage.io/docs/integrations/bitbucketCloud/discovery
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-tasks': patch
---
Added new function `readTaskScheduleDefinitionFromConfig` to read `TaskScheduleDefinition` (aka. schedule) from the `Config`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
---
Add `bitbucketCloudCatalogModule` (new backend-plugin-api, alpha).
+20 -5
View File
@@ -37,6 +37,7 @@ And then add the entity provider to your catalog builder:
+ builder.addEntityProvider(
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
+ logger: env.logger,
+ // optional: alternatively, configure via app-config.yaml
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 30 },
+ timeout: { minutes: 3 },
@@ -67,24 +68,38 @@ catalog:
filters: # optional
projectKey: '^apis-.*$' # optional; RegExp
repoSlug: '^service-.*$' # optional; RegExp
schedule: # optional; same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
timeout: { minutes: 3 }
workspace: workspace-name
```
> **Note:** It is possible but certainly not recommended to skip the provider ID level.
> If you do so, `default` will be used as provider ID.
- **catalogPath** _(optional)_:
- **`catalogPath`** _(optional)_:
Default: `/catalog-info.yaml`.
Path where to look for `catalog-info.yaml` files.
When started with `/`, it is an absolute path from the repo root.
It supports values as allowed by the `path` filter/modifier
[at Bitbucket Cloud's code search](https://confluence.atlassian.com/bitbucket/code-search-in-bitbucket-873876782.html#Search-Pathmodifier).
- **filters** _(optional)_:
- **projectKey** _(optional)_:
- **`filters`** _(optional)_:
- **`projectKey`** _(optional)_:
Regular expression used to filter results based on the project key.
- **repoSlug** _(optional)_:
- **`repoSlug`** _(optional)_:
Regular expression used to filter results based on the repo slug.
- **workspace**:
- **`schedule`** _(optional)_:
- **`frequency`**:
How often you want the task to run. The system does its best to avoid overlapping invocations.
- **`timeout`**:
The maximum amount of time that a single task invocation can take.
- **`initialDelay`** _(optional)_:
The amount of time that should pass before the first invocation happens.
- **`scope`** _(optional)_:
`'global'` or `'local'`. Sets the scope of concurrency control.
- **`workspace`**:
Name of your organization account/workspace.
If you want to add multiple workspaces, you need to add one provider config each.
+18
View File
@@ -30,6 +30,11 @@ export interface PluginTaskScheduler {
triggerTask(id: string): Promise<void>;
}
// @public
export function readTaskScheduleDefinitionFromConfig(
config: Config,
): TaskScheduleDefinition;
// @public
export type TaskFunction =
| ((abortSignal: AbortSignal_2) => void | Promise<void>)
@@ -60,6 +65,19 @@ export interface TaskScheduleDefinition {
timeout: Duration | HumanDuration;
}
// @public
export interface TaskScheduleDefinitionConfig {
frequency:
| {
cron: string;
}
| string
| HumanDuration;
initialDelay?: string | HumanDuration;
scope?: 'global' | 'local';
timeout: string | HumanDuration;
}
// @public
export class TaskScheduler {
constructor(databaseManager: DatabaseManager, logger: Logger);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
export { TaskScheduler } from './TaskScheduler';
export type {
PluginTaskScheduler,
@@ -21,5 +22,6 @@ export type {
TaskInvocationDefinition,
TaskRunner,
TaskScheduleDefinition,
TaskScheduleDefinitionConfig,
HumanDuration,
} from './types';
@@ -0,0 +1,119 @@
/*
* 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 { Duration } from 'luxon';
import { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
import { HumanDuration } from './types';
describe('readTaskScheduleDefinitionFromConfig', () => {
it('all valid values', () => {
const config = new ConfigReader({
frequency: {
cron: '0 30 * * * *',
},
timeout: 'PT3M',
initialDelay: {
minutes: 20,
},
scope: 'global',
});
const result = readTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect((result.initialDelay as HumanDuration).minutes).toEqual(20);
expect(result.scope).toBe('global');
});
it('all valid required values', () => {
const config = new ConfigReader({
frequency: {
cron: '0 30 * * * *',
},
timeout: 'PT3M',
});
const result = readTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect(result.initialDelay).toBeUndefined();
expect(result.scope).toBeUndefined();
});
it('fail without required frequency', () => {
const config = new ConfigReader({
timeout: 'PT3M',
});
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
"Missing required config value at 'frequency'",
);
});
it('fail without required timeout', () => {
const config = new ConfigReader({
frequency: 'PT30M',
});
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
"Missing required config value at 'timeout'",
);
});
it('invalid frequency value', () => {
const config = new ConfigReader({
frequency: {
invalid: 'value',
},
timeout: 'PT3M',
});
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
'HumanDuration needs at least one of',
);
});
it('frequency value with additional invalid prop', () => {
const config = new ConfigReader({
frequency: {
minutes: 20,
invalid: 'value',
},
timeout: 'PT3M',
});
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
'HumanDuration does not contain properties: invalid',
);
});
it('invalid scope value', () => {
const config = new ConfigReader({
frequency: {
years: 2,
},
timeout: 'PT3M',
scope: 'invalid',
});
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
'Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: invalid',
);
});
});
@@ -0,0 +1,102 @@
/*
* 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 { Config } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { HumanDuration, TaskScheduleDefinition } from './types';
import { Duration } from 'luxon';
const propsOfHumanDuration = [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
];
function convertToHumanDuration(config: Config, key: string): HumanDuration {
const props = config.getConfig(key).keys();
if (!props.find(prop => propsOfHumanDuration.includes(prop))) {
throw new Error(
`HumanDuration needs at least one of: ${propsOfHumanDuration}`,
);
}
const invalidProps = props.filter(
prop => !propsOfHumanDuration.includes(prop),
);
if (invalidProps.length > 0) {
throw new Error(
`HumanDuration does not contain properties: ${invalidProps}`,
);
}
return config.get<JsonObject>(key) as HumanDuration;
}
function readDuration(config: Config, key: string): Duration | HumanDuration {
return typeof config.get(key) === 'string'
? Duration.fromISO(config.getString(key))
: convertToHumanDuration(config, key);
}
function readCronOrDuration(
config: Config,
key: string,
): { cron: string } | Duration | HumanDuration {
const value = config.get(key);
if (typeof value === 'object' && (value as { cron?: string }).cron) {
return value as { cron: string };
}
return readDuration(config, key);
}
/**
* Reads a TaskScheduleDefinition from a Config.
* Expects the config not to be the root config,
* but the config for the definition.
*
* @param config - config for a TaskScheduleDefinition.
* @public
*/
export function readTaskScheduleDefinitionFromConfig(
config: Config,
): TaskScheduleDefinition {
const frequency = readCronOrDuration(config, 'frequency');
const timeout = readDuration(config, 'timeout');
const initialDelay = config.has('initialDelay')
? readDuration(config, 'initialDelay')
: undefined;
const scope = config.getOptionalString('scope');
if (scope && !['global', 'local'].includes(scope)) {
throw new Error(
`Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: ${scope}`,
);
}
return {
frequency,
timeout,
initialDelay,
scope: scope as 'global' | 'local' | undefined,
};
}
+100 -3
View File
@@ -58,7 +58,7 @@ export interface TaskScheduleDefinition {
*
* @remarks
*
* This is a best effort value; under some circumstances there can be
* This is the best effort value; under some circumstances there can be
* deviations. For example, if the task runtime is longer than the frequency
* and the timeout has not been given or not been exceeded yet, the next
* invocation of this task will be delayed until after the previous one
@@ -112,7 +112,7 @@ export interface TaskScheduleDefinition {
* collaborating on a task that has its `scope` field set to `'global'`, then
* you may still see the task being processed by other long-lived workers,
* while any given single worker is in its initial sleep delay time e.g. after
* a deployment. Therefore this parameter is not useful for "globally" pausing
* a deployment. Therefore, this parameter is not useful for "globally" pausing
* work; its main intended use is for individual machines to get a chance to
* reach some equilibrium at startup before triggering heavy batch workloads.
*/
@@ -128,7 +128,104 @@ export interface TaskScheduleDefinition {
* attempt to ensure that only one worker machine runs the task at a time,
* according to the given cadence. This means that as the number of worker
* hosts increases, the invocation frequency of this task will not go up.
* Instead the load is spread randomly across hosts. This setting is useful
* Instead, the load is spread randomly across hosts. This setting is useful
* for tasks that access shared resources, for example catalog ingestion tasks
* where you do not want many machines to repeatedly import the same data and
* trample over each other.
*
* When the scope is set to `'local'`, there is no concurrency control across
* hosts. Each host runs the task according to the given cadence similarly to
* `setInterval`, but the runtime ensures that there are no overlapping runs.
*
* @defaultValue 'global'
*/
scope?: 'global' | 'local';
}
/**
* Config options for {@link TaskScheduleDefinition}
* that control the scheduling of a task.
*
* @public
*/
export interface TaskScheduleDefinitionConfig {
/**
* How often you want the task to run. The system does its best to avoid
* overlapping invocations.
*
* @remarks
*
* This is the best effort value; under some circumstances there can be
* deviations. For example, if the task runtime is longer than the frequency
* and the timeout has not been given or not been exceeded yet, the next
* invocation of this task will be delayed until after the previous one
* finishes.
*
* This is a required field.
*/
frequency:
| {
/**
* A crontab style string.
*
* @remarks
*
* Overview:
*
* ```
* ┌────────────── second (optional)
* │ ┌──────────── minute
* │ │ ┌────────── hour
* │ │ │ ┌──────── day of month
* │ │ │ │ ┌────── month
* │ │ │ │ │ ┌──── day of week
* │ │ │ │ │ │
* │ │ │ │ │ │
* * * * * * *
* ```
*/
cron: string;
}
| string
| HumanDuration;
/**
* The maximum amount of time that a single task invocation can take, before
* it's considered timed out and gets "released" such that a new invocation
* is permitted to take place (possibly, then, on a different worker).
*/
timeout: string | HumanDuration;
/**
* The amount of time that should pass before the first invocation happens.
*
* @remarks
*
* This can be useful in cold start scenarios to stagger or delay some heavy
* compute jobs. If no value is given for this field then the first invocation
* will happen as soon as possible according to the cadence.
*
* NOTE: This is a per-worker delay. If you have a cluster of workers all
* collaborating on a task that has its `scope` field set to `'global'`, then
* you may still see the task being processed by other long-lived workers,
* while any given single worker is in its initial sleep delay time e.g. after
* a deployment. Therefore, this parameter is not useful for "globally" pausing
* work; its main intended use is for individual machines to get a chance to
* reach some equilibrium at startup before triggering heavy batch workloads.
*/
initialDelay?: string | HumanDuration;
/**
* Sets the scope of concurrency control / locking to apply for invocations of
* this task.
*
* @remarks
*
* When the scope is set to the default value `'global'`, the scheduler will
* attempt to ensure that only one worker machine runs the task at a time,
* according to the given cadence. This means that as the number of worker
* hosts increases, the invocation frequency of this task will not go up.
* Instead, the load is spread randomly across hosts. This setting is useful
* for tasks that access shared resources, for example catalog ingestion tasks
* where you do not want many machines to repeatedly import the same data and
* trample over each other.
@@ -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);
},
});
},
});
+3
View File
@@ -4484,6 +4484,7 @@ __metadata:
resolution: "@backstage/plugin-catalog-backend-module-bitbucket-cloud@workspace:plugins/catalog-backend-module-bitbucket-cloud"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
@@ -4491,6 +4492,8 @@ __metadata:
"@backstage/integration": "workspace:^"
"@backstage/plugin-bitbucket-cloud-common": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
luxon: ^3.0.0
msw: ^0.47.0
uuid: ^8.0.0
winston: ^3.2.1