diff --git a/.changeset/brown-grapes-battle.md b/.changeset/brown-grapes-battle.md new file mode 100644 index 0000000000..f94c938b02 --- /dev/null +++ b/.changeset/brown-grapes-battle.md @@ -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 diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 6609707868..c5a17bb2c9 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -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. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index b886cc224f..2f8b4186d1 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -7,6 +7,7 @@ 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 +19,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider { config: Config, options: { logger: Logger; - schedule: TaskRunner; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; }, ): BitbucketCloudEntityProvider[]; // (undocumented) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts index 29289bb70e..8fa58413c0 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/config.d.ts @@ -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; } >; }; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index d36d7c653e..6079f4fe14 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -44,6 +44,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "luxon": "^3.0.0", "msw": "^0.47.0" }, "files": [ diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts index 641150acbf..7d780cd064 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.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({ diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts index 74d642ed9b..f3334a6574 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts @@ -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 { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.test.ts index a42bd2cde7..5a2cc99437 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.test.ts @@ -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, + }, + }, + }); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.ts index 95e995f8c3..def88b0951 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProviderConfig.ts @@ -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, }; } diff --git a/yarn.lock b/yarn.lock index 1178b3d940..ad1fe8b20a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4462,6 +4462,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + luxon: ^3.0.0 msw: ^0.47.0 uuid: ^8.0.0 winston: ^3.2.1