From 7b3ed9b928b5997aa5bb0a5e6976ee7a2a85280a Mon Sep 17 00:00:00 2001 From: David Lilienfeld Date: Sun, 27 Aug 2023 18:36:03 -0400 Subject: [PATCH] Add Bitbucket Server Event Integration Signed-off-by: David Lilienfeld --- .changeset/nine-falcons-repeat.md | 6 + .../integrations/bitbucketServer/discovery.md | 96 +++- .../package.json | 8 + .../report.api.md | 75 ++- .../src/index.ts | 9 +- .../src/lib/BitbucketServerClient.test.ts | 87 ++- .../src/lib/BitbucketServerClient.ts | 44 +- .../src/lib/index.ts | 5 +- .../src/lib/types.ts | 51 +- ...oduleBitbucketServerEntityProvider.test.ts | 32 +- ...alogModuleBitbucketServerEntityProvider.ts | 24 +- .../BitbucketServerEntityProvider.test.ts | 495 +++++++++++++++++- .../BitbucketServerEntityProvider.ts | 413 ++++++++++++++- .../BitbucketServerEntityProviderConfig.ts | 2 - .../BitbucketServerLocationParser.ts | 8 +- .../.eslintrc.js | 1 + .../README.md | 48 ++ .../api-report-alpha.md | 14 + .../api-report.md | 18 + .../catalog-info.yaml | 9 + .../package.json | 55 ++ .../src/alpha.ts | 18 + .../src/index.ts | 24 + .../router/BitbucketServerEventRouter.test.ts | 53 ++ .../src/router/BitbucketServerEventRouter.ts | 45 ++ .../eventsModuleBitbucketServerEventRouter.ts | 44 ++ .../eventsModuleBitbucketServerRouter.test.ts | 44 ++ .../src/setupTests.ts | 17 + yarn.lock | 15 + 29 files changed, 1715 insertions(+), 45 deletions(-) create mode 100644 .changeset/nine-falcons-repeat.md create mode 100644 plugins/events-backend-module-bitbucket-server/.eslintrc.js create mode 100644 plugins/events-backend-module-bitbucket-server/README.md create mode 100644 plugins/events-backend-module-bitbucket-server/api-report-alpha.md create mode 100644 plugins/events-backend-module-bitbucket-server/api-report.md create mode 100644 plugins/events-backend-module-bitbucket-server/catalog-info.yaml create mode 100644 plugins/events-backend-module-bitbucket-server/package.json create mode 100644 plugins/events-backend-module-bitbucket-server/src/alpha.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/index.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts create mode 100644 plugins/events-backend-module-bitbucket-server/src/setupTests.ts diff --git a/.changeset/nine-falcons-repeat.md b/.changeset/nine-falcons-repeat.md new file mode 100644 index 0000000000..a67e925343 --- /dev/null +++ b/.changeset/nine-falcons-repeat.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-server': minor +'@backstage/plugin-events-backend-module-bitbucket-server': minor +--- + +Added the ability for the plugin to receive events coming from Bitbucket Server push webhooks. It then performs a delta mutation on the catalog. diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index 9ba6c7a96d..a66db83806 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -26,7 +26,47 @@ dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to you yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` -And update your backend by adding the following line: +### Installation with New Backend System + +```ts +// optional if you want HTTP endpojnts to receive external events +// backend.add(import('@backstage/plugin-events-backend/alpha')); +// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events +// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); +backend.add( + import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), +); +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'), +); +``` + +You need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Further documentation: + +- +- +- + +### Installation with Legacy Backend System + +#### Installation without Events Support + +You will have to add the entity provider in the catalog initialization code of your +backend. The provider is not installed by default, therefore you have to add a +dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to your backend +package. + +```bash +# From your Backstage root directory +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server +``` + +And then add the entity provider to your catalog builder: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); @@ -37,6 +77,60 @@ backend.add( /* highlight-add-end */ ``` +#### Installation with Events Support + +Please follow the installation instructions at + +- +- + +Additionally, you need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) + - Bitbucket Server events webhook url should be set to `{backstageBaseUrl}/api/events/http/bitbucketServer` +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Set up your provider + +```ts title="packages/backend/src/plugins/catalog.ts" +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +/* highlight-add-start */ +import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server'; +/* highlight-add-end */ + +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); + /* highlight-add-start */ + const bitbucketServerProvider = BitbucketServerEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + logger: env.logger, + scheduler: env.scheduler, + events: env.events, + tokenManager: env.tokenManager, + }, + ); + env.eventBroker.subscribe(bitbucketServerProvider); + builder.addEntityProvider(bitbucketServerProvider); + /* highlight-add-end */ + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; +} +``` + +**Attention:** +`catalogApi` and `tokenManager` are required at this variant +compared to the one without events support. + ## Configuration To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md). diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 6113f4b41b..a7c403b534 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -51,17 +51,25 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "p-throttle": "^4.1.1", + "@backstage/plugin-events-node": "workspace:^", + "@types/node-fetch": "^2.5.12", + "node-fetch": "^2.7.0", "uuid": "^11.0.0" }, "devDependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-bitbucket-server/report.api.md b/plugins/catalog-backend-module-bitbucket-server/report.api.md index 0da9b78139..c72733806b 100644 --- a/plugins/catalog-backend-module-bitbucket-server/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-server/report.api.md @@ -5,14 +5,18 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; +import { TaskRunner } from '@backstage/backend-tasks'; +import { TokenManager } from '@backstage/backend-common'; // @public export class BitbucketServerClient { @@ -22,6 +26,11 @@ export class BitbucketServerClient { config: BitbucketServerIntegrationConfig; }): BitbucketServerClient; // (undocumented) + getDefaultBranch(options: { + projectKey: string; + repo: string; + }): Promise; + // (undocumented) getFile(options: { projectKey: string; repo: string; @@ -47,6 +56,16 @@ export class BitbucketServerClient { }; } +// @public (undocumented) +export type BitbucketServerDefaultBranch = { + id: string; + displayId: string; + type: string; + latestCommit: string; + latestChangeset: string; + isDefault: boolean; +}; + // @public export class BitbucketServerEntityProvider implements EntityProvider { connect(connection: EntityProviderConnection): Promise; @@ -55,9 +74,12 @@ export class BitbucketServerEntityProvider implements EntityProvider { config: Config, options: { logger: LoggerService; + events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: SchedulerServiceTaskRunner; - scheduler?: SchedulerService; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + catalogApi?: CatalogApi; + tokenManager?: TokenManager; }, ): BitbucketServerEntityProvider[]; getProviderName(): string; @@ -65,6 +87,52 @@ export class BitbucketServerEntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } +// @public (undocumented) +export namespace BitbucketServerEvents { + // (undocumented) + export type Actor = { + name?: string; + id: number; + }; + // (undocumented) + export type Change = { + ref: { + id: string; + displayId: string; + type: string; + }; + }; + // (undocumented) + export interface Event { + // (undocumented) + eventKey: string; + } + // (undocumented) + export interface RefsChangedEvent extends Event { + // (undocumented) + actor: Actor; + // (undocumented) + changes: Change[]; + // (undocumented) + commits: undefined; + // (undocumented) + date: string; + // (undocumented) + repository: Repository; + // (undocumented) + ToCommit: undefined; + } + // (undocumented) + export type Repository = { + slug: string; + id: number; + name: string; + project: BitbucketServerProject; + }; + { + } +} + // @public (undocumented) export type BitbucketServerListOptions = { [key: string]: number | undefined; @@ -108,6 +176,7 @@ export type BitbucketServerRepository = { }[] >; archived: boolean; + defaultBranch: string; }; // @public (undocumented) diff --git a/plugins/catalog-backend-module-bitbucket-server/src/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/index.ts index f05c9ca0e9..96e3a1a0a7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/index.ts @@ -21,12 +21,7 @@ */ export { default } from './module'; -export { BitbucketServerClient } from './lib'; -export type { - BitbucketServerProject, - BitbucketServerRepository, - BitbucketServerPagedResponse, - BitbucketServerListOptions, -} from './lib'; +export * from './lib'; + export { BitbucketServerEntityProvider } from './providers'; export type { BitbucketServerLocationParser } from './providers'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts index 6abcede9de..fbb4054390 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts @@ -23,7 +23,12 @@ import { BitbucketServerPagedResponse, paginated, } from './BitbucketServerClient'; -import { BitbucketServerProject, BitbucketServerRepository } from './types'; +import { + BitbucketServerDefaultBranch, + BitbucketServerProject, + BitbucketServerRepository, +} from './types'; +import { NotFoundError } from '@backstage/errors'; const server = setupServer(); @@ -122,6 +127,7 @@ describe('BitbucketServerClient', () => { ], }, archived: false, + defaultBranch: 'master', }, ], }; @@ -200,6 +206,7 @@ describe('BitbucketServerClient', () => { ], }, archived: false, + defaultBranch: 'master', }; return res(ctx.json(response)); @@ -217,4 +224,82 @@ describe('BitbucketServerClient', () => { 'https://bitbucket.mycompany.com/projects/test-project', ); }); + + it('getRepository no repo', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/wrong-repo`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const error = async () => { + await client.getRepository({ + projectKey: 'test-project', + repo: 'wrong-repo', + }); + }; + + await expect(error).rejects.toThrow( + "Repository 'wrong-repo' in project 'test-project' does not exist.", + ); + await expect(error).rejects.toThrow(NotFoundError); + }); + + it('getDefaultBranch success', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/test-repo/default-branch`, + (req, res, ctx) => { + if ( + req.headers.get('authorization') !== + 'Basic dGVzdC11c2VyOnRlc3QtcHc=' + ) { + return res(ctx.status(400)); + } + const response: BitbucketServerDefaultBranch = { + id: 'refs/heads/master', + displayId: 'master', + type: 'BRANCH', + latestCommit: '0f2f3ae484054696568bf4560ba4da280f7df82a', + latestChangeset: '0f2f3ae484054696568bf4560ba4da280f7df82a', + isDefault: true, + }; + + return res(ctx.json(response)); + }, + ), + ); + + const repo = await client.getDefaultBranch({ + projectKey: 'test-project', + repo: 'test-repo', + }); + expect(repo.displayId).toEqual('master'); + }); + + it('getDefaultBranch endpoint', async () => { + server.use( + rest.get( + `${config.apiBaseUrl}/projects/test-project/repos/wrong-repo/default-branch`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const error = async () => { + await client.getDefaultBranch({ + projectKey: 'test-project', + repo: 'wrong-repo', + }); + }; + + await expect(error).rejects.toThrow( + "Your Bitbucket Server version no longer supports the default branch endpoint or 'wrong-repo' in 'test-project' does not exist.", + ); + await expect(error).rejects.toThrow(NotFoundError); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index 3dd61d64e7..ba5f75268c 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -18,7 +18,6 @@ import { BitbucketServerIntegrationConfig, getBitbucketServerRequestOptions, } from '@backstage/integration'; -import { BitbucketServerProject, BitbucketServerRepository } from './types'; import pThrottle from 'p-throttle'; // 1 per second @@ -32,6 +31,15 @@ const throttledFetch = throttle( return await fetch(url, options); }, ); +import { + BitbucketServerDefaultBranch, + BitbucketServerRepository, +} from './index'; + +import { BitbucketServerProject } from './types'; +import { NotFoundError } from '@backstage/errors'; + +import { ResponseError } from '@backstage/errors'; /** * A client for interacting with a Bitbucket Server instance @@ -92,7 +100,35 @@ export class BitbucketServerClient { request, getBitbucketServerRequestOptions(this.config), ); - return response.json(); + if (response.ok) { + return response.json(); + } + if (response.status === 404) { + throw new NotFoundError( + `Repository '${options.repo}' in project '${options.projectKey}' does not exist.`, + ); + } + throw await ResponseError.fromResponse(response); + } + + async getDefaultBranch(options: { + projectKey: string; + repo: string; + }): Promise { + const request = `${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/default-branch`; + const response = await fetch( + request, + getBitbucketServerRequestOptions(this.config), + ); + if (response.ok) { + return response.json(); + } + if (response.status === 404) { + throw new NotFoundError( + `Your Bitbucket Server version no longer supports the default branch endpoint or '${options.repo}' in '${options.projectKey}' does not exist.`, + ); + } + throw await ResponseError.fromResponse(response); } resolvePath(options: { projectKey: string; repo: string; path: string }): { @@ -163,7 +199,9 @@ export type BitbucketServerPagedResponse = { values: T[]; nextPageStart: number; }; - +/** + * @public + */ export async function* paginated( request: ( options: BitbucketServerListOptions, diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts index 8b72c9335b..14fb01629e 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/index.ts @@ -19,7 +19,4 @@ export type { BitbucketServerPagedResponse, BitbucketServerListOptions, } from './BitbucketServerClient'; -export type { - BitbucketServerRepository, - BitbucketServerProject, -} from './types'; +export * from './types'; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts index 381eecb493..e014528791 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/types.ts @@ -15,7 +15,7 @@ */ /** @public */ -export type BitbucketServerRepository = { +type BitbucketServerRepository = { project: { key: string; }; @@ -28,9 +28,56 @@ export type BitbucketServerRepository = { }[] >; archived: boolean; + defaultBranch: string; }; /** @public */ -export type BitbucketServerProject = { +type BitbucketServerProject = { key: string; }; + +/** @public */ +type BitbucketServerDefaultBranch = { + id: string; + displayId: string; + type: string; + latestCommit: string; + latestChangeset: string; + isDefault: boolean; +}; + +/** @public */ +namespace BitbucketServerEvents { + interface Event { + eventKey: string; + } + + export interface RefsChangedEvent extends Event { + date: string; + actor: Actor; + repository: Repository; + changes: Change[]; + commits: undefined; + ToCommit: undefined; + } + type Actor = { + name?: string; + id: number; + }; + type Change = { + ref: { id: string; displayId: string; type: string }; + }; + type Repository = { + slug: string; + id: number; + name: string; + project: BitbucketServerProject; + }; +} + +export type { + BitbucketServerDefaultBranch, + BitbucketServerProject, + BitbucketServerEvents, + BitbucketServerRepository, +}; diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index fbd4fa7a36..27420e9300 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -16,20 +16,35 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { Duration } from 'luxon'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; -import { BitbucketServerEntityProvider } from '../providers'; +import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; describe('catalogModuleBitbucketServerEntityProvider', () => { it('should register provider at the catalog extension point', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); let addedProviders: Array | undefined; let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined; - const extensionPoint = { + const catalogExtensionPointImpl = { addEntityProvider: (providers: any) => { addedProviders = providers; }, }; + const connection = jest.fn() as unknown as EntityProviderConnection; const runner = jest.fn(); const scheduler = mockServices.scheduler.mock({ createScheduledTaskRunner(schedule) { @@ -60,7 +75,9 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }; await startTestBackend({ - extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + extensionPoints: [ + [catalogProcessingExtensionPoint, catalogExtensionPointImpl], + ], features: [ catalogModuleBitbucketServerEntityProvider, mockServices.rootConfig.factory({ data: config }), @@ -72,9 +89,14 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { expect(usedSchedule?.frequency).toEqual({ months: 1 }); expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); - expect(addedProviders?.pop()?.getProviderName()).toEqual( + expect(runner).not.toHaveBeenCalled(); + const provider = addedProviders!.pop()!; + expect(provider.getProviderName()).toEqual( 'bitbucketServer-provider:default', ); - expect(runner).not.toHaveBeenCalled(); + await provider.connect(connection); + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('bitbucketServer-provider:default'); + expect(runner).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts index 2c78d219d9..615bbb3477 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.ts @@ -18,8 +18,12 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { BitbucketServerEntityProvider } from '../providers'; +import { + catalogProcessingExtensionPoint, + catalogServiceRef, +} from '@backstage/plugin-catalog-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider'; /** * @public @@ -31,14 +35,28 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, + catalogApi: catalogServiceRef, config: coreServices.rootConfig, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, }, - async init({ catalog, config, logger, scheduler }) { + async init({ + catalog, + catalogApi, + config, + events, + logger, + scheduler, + tokenManager, + }) { const providers = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi, + events, logger, scheduler, + tokenManager, }); catalog.addEntityProvider(providers); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts index 05e0ea6a01..dd2735177b 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { TokenManager } from '@backstage/backend-common'; import { SchedulerService, SchedulerServiceTaskRunner, @@ -24,11 +25,22 @@ import { registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { + DeferredEntity, + EntityProviderConnection, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { BitbucketServerEntityProvider } from './BitbucketServerEntityProvider'; +import { + BitbucketServerEntityProvider, + toDeferredEntities, +} from './BitbucketServerEntityProvider'; import { BitbucketServerPagedResponse } from '../lib'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; +import { BitbucketServerEvents } from '../lib/index'; +import { CatalogApi } from '@backstage/catalog-client'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; class PersistingTaskRunner implements SchedulerServiceTaskRunner { private tasks: SchedulerServiceTaskInvocationDefinition[] = []; @@ -57,9 +69,15 @@ function pagedResponse(values: any): BitbucketServerPagedResponse { const logger = mockServices.logger.mock(); +const events = DefaultEventsService.create({ logger }); + const server = setupServer(); -function setupStubs(projects: Project[], baseUrl: string) { +function setupStubs( + projects: Project[], + baseUrl: string, + defaultBranch: string, +) { // Stub projects server.use( rest.get(`${baseUrl}/rest/api/1.0/projects`, (_, res, ctx) => { @@ -93,6 +111,7 @@ function setupStubs(projects: Project[], baseUrl: string) { ], }, archived: repo.archived ?? false, + defaultBranch: defaultBranch, }); } return res(ctx.json(pagedResponse(response))); @@ -102,6 +121,107 @@ function setupStubs(projects: Project[], baseUrl: string) { } } +const host = 'bitbucket.mycompany.com'; +const targetPath = `/catalog-info.yaml`; +const test1RepoUrl = `https://${host}/projects/TEST/repos/test1/browse`; + +function setupRepositoryReqHandler(defaultBranch: string) { + server.use( + rest.get( + `https://${host}/rest/api/1.0/projects/TEST/repos/test1`, + (_, res, ctx) => { + const response = { + slug: 'test1', + id: 1, + name: 'test1', + project: { + key: 'TEST', + id: 1, + name: 'TEST', + links: { + self: [ + { + href: `https://${host}/projects/TEST`, + }, + ], + }, + }, + links: { + self: [ + { + href: `${test1RepoUrl}`, + }, + ], + }, + defaultBranch: defaultBranch, + }; + return res(ctx.json(response)); + }, + ), + ); +} + +const tokenManager = { + getToken: async () => { + return { token: 'fake-token' }; + }, +} as any as TokenManager; +const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = { + eventKey: 'repo:refs_changed', + date: '2017-09-19T09:45:32+1000', + actor: { + name: 'admin', + id: 1, + }, + repository: { + slug: 'test1', + id: 84, + name: 'test1', + project: { + key: 'TEST', + }, + }, + changes: [ + { + ref: { + id: 'refs/heads/master', + displayId: 'master', + type: 'BRANCH', + }, + }, + ], + commits: undefined, + ToCommit: undefined, +}; +const repoPushEventParams = { + topic: 'bitbucketServer.repo:refs_changed', + eventPayload: repoPushEvent, + metadata: { 'x-event-key': 'repo:refs_changed' }, +}; + +const createLocationEntity = ( + repoUrl: string, + path: string, + defaultBranch: string, +): LocationEntity => { + const target = `${repoUrl}${path}`; + + const entity = locationSpecToLocationEntity({ + location: { + type: 'url', + target: target, + presence: 'optional', + }, + }); + entity.metadata.annotations = { + ...entity.metadata.annotations, + [`${host}/repo-url`]: target, + ['bitbucket.org/default-branch']: defaultBranch, + }; + + return entity; +}; + describe('BitbucketServerEntityProvider', () => { registerMswTestHooks(server); afterEach(() => { @@ -157,6 +277,7 @@ describe('BitbucketServerEntityProvider', () => { }); const providers = BitbucketServerEntityProvider.fromConfig(config, { logger, + events, schedule, }); @@ -191,6 +312,7 @@ describe('BitbucketServerEntityProvider', () => { }); const providers = BitbucketServerEntityProvider.fromConfig(config, { logger, + events, schedule, }); @@ -204,7 +326,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update on scheduled execution with filters', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -253,6 +374,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -270,6 +392,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:${url}`, 'backstage.io/managed-by-origin-location': `url:${url}`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -291,7 +414,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update on scheduled execution without filters', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -329,6 +451,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -345,6 +468,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -364,6 +488,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', }, @@ -441,7 +566,6 @@ describe('BitbucketServerEntityProvider', () => { }); it('apply full update with schedule in config', async () => { - const host = 'bitbucket.mycompany.com'; const config = new ConfigReader({ integrations: { bitbucketServer: [ @@ -488,6 +612,7 @@ describe('BitbucketServerEntityProvider', () => { { key: 'other-project', repos: [{ name: 'other-repo' }] }, ], `https://${host}`, + 'master', ); await provider.connect(entityProviderConnection); @@ -504,6 +629,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', }, @@ -523,6 +649,7 @@ describe('BitbucketServerEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', }, name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', }, @@ -542,4 +669,360 @@ describe('BitbucketServerEntityProvider', () => { entities: expectedEntities, }); }); + + it('Multiple location entities to deferred entities', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + bitbucketServer: { + host: host, + }, + }, + }, + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + }); + const providers = BitbucketServerEntityProvider.fromConfig(config, { + logger, + events, + schedule, + }); + + expect(providers).toHaveLength(1); + expect(providers[0].getProviderName()).toEqual( + 'bitbucketServer-provider:default', + ); + + const locationEntities = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + [`${host}/repo-url`]: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', + }, + name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6', + }, + spec: { + presence: 'optional', + target: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`, + type: 'url', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + [`${host}/repo-url`]: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + 'bitbucket.org/default-branch': 'master', + }, + name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f', + }, + spec: { + presence: 'optional', + target: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`, + type: 'url', + }, + }, + ]; + + const deferredEntities = toDeferredEntities( + locationEntities, + providers[0].getProviderName(), + ); + + expect(deferredEntities).toEqual([ + { + locationKey: providers[0].getProviderName(), + entity: locationEntities[0], + }, + { + locationKey: providers[0].getProviderName(), + entity: locationEntities[1], + }, + ]); + }); + + it('refresh onRepoPush', async () => { + const schedule = new PersistingTaskRunner(); + const keptModule = createLocationEntity( + test1RepoUrl, + `/kept-module:${targetPath}`, + 'master', + ); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + setupRepositoryReqHandler('master'); + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/kept-module:/catalog-info.yaml`, + defaultBranch: 'master', + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter[`metadata.annotations.${host}/repo-url`] !== + `${test1RepoUrl}/kept-module:${targetPath}` + ) { + return { items: [] }; + } + return { + items: [keptModule], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [`url:${test1RepoUrl}/kept-module:${targetPath}`], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('no refresh onRepoPush due to different default branch', async () => { + const schedule = new PersistingTaskRunner(); + const keptModule = createLocationEntity( + test1RepoUrl, + `/kept-module:${targetPath}`, + 'main', + ); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + setupRepositoryReqHandler('main'); + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/kept-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter[`metadata.annotations.${host}/repo-url`] !== + `${test1RepoUrl}/kept-module:${targetPath}` + ) { + return { items: [] }; + } + return { + items: [keptModule], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('add onRepoPush', async () => { + const schedule = new PersistingTaskRunner(); + setupRepositoryReqHandler('master'); + const addedModule = createLocationEntity( + test1RepoUrl, + `/added-module:${targetPath}`, + 'master', + ); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/added-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + _request: { filter: Record }, + _options: { token: string }, + ): Promise<{ items: Entity[] }> => { + return { + items: [], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + const addedEntities = [ + { + entity: addedModule, + locationKey: 'bitbucketServer-provider:mainProvider', + }, + ]; + const removedEntities: DeferredEntity[] = []; + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: addedEntities, + removed: removedEntities, + }); + }); + + it('fail add onRepoPush from wrong default branch', async () => { + const schedule = new PersistingTaskRunner(); + setupRepositoryReqHandler('main'); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: host, + }, + ], + }, + catalog: { + providers: { + bitbucketServer: { + mainProvider: { + host: host, + apiBaseUrl: `https://${host}/rest/api/1.0`, + catalogPath: `/added-module:/catalog-info.yaml`, + }, + }, + }, + }, + }); + + const catalogApi = { + getEntities: async ( + _request: { filter: Record }, + _options: { token: string }, + ): Promise<{ items: Entity[] }> => { + return { + items: [], + }; + }, + }; + const provider = BitbucketServerEntityProvider.fromConfig(config, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + events, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await events.publish(repoPushEventParams); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index 46f492d64c..4afc02b8b5 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { @@ -24,6 +25,7 @@ import { import { EntityProvider, EntityProviderConnection, + DeferredEntity, } from '@backstage/plugin-catalog-node'; import * as uuid from 'uuid'; import { BitbucketServerClient, paginated } from '../lib'; @@ -40,6 +42,12 @@ import { SchedulerService, SchedulerServiceTaskRunner, } from '@backstage/backend-plugin-api'; +import { BitbucketServerEvents } from '../lib'; +import { EventsService } from '@backstage/plugin-events-node'; +import { CatalogApi } from '@backstage/catalog-client'; +import { TokenManager } from '@backstage/backend-common'; + +const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed'; /** * Discovers catalog files located in Bitbucket Server. @@ -56,14 +64,23 @@ export class BitbucketServerEntityProvider implements EntityProvider { private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private connection?: EntityProviderConnection; + private readonly catalogApi?: CatalogApi; + private readonly events?: EventsService; + private readonly tokenManager?: TokenManager; + private eventConfigErrorThrown = false; + private readonly targetAnnotation: string; + private readonly defaultBranchAnnotation: string; static fromConfig( config: Config, options: { logger: LoggerService; + events?: EventsService; parser?: BitbucketServerLocationParser; - schedule?: SchedulerServiceTaskRunner; - scheduler?: SchedulerService; + schedule?: TaskRunner; + scheduler?: PluginTaskScheduler; + catalogApi?: CatalogApi; + tokenManager?: TokenManager; }, ): BitbucketServerEntityProvider[] { const integrations = ScmIntegrations.fromConfig(config); @@ -98,6 +115,9 @@ export class BitbucketServerEntityProvider implements EntityProvider { options.logger, taskRunner, options.parser, + options.catalogApi, + options.events, + options.tokenManager, ); }); } @@ -108,6 +128,9 @@ export class BitbucketServerEntityProvider implements EntityProvider { logger: LoggerService, taskRunner: SchedulerServiceTaskRunner, parser?: BitbucketServerLocationParser, + catalogApi?: CatalogApi, + events?: EventsService, + tokenManager?: TokenManager, ) { this.integration = integration; this.config = config; @@ -116,6 +139,11 @@ export class BitbucketServerEntityProvider implements EntityProvider { target: this.getProviderName(), }); this.scheduleFn = this.createScheduleFn(taskRunner); + this.catalogApi = catalogApi; + this.tokenManager = tokenManager; + this.targetAnnotation = `${this.config.host.split(':')[0]}/repo-url`; + this.defaultBranchAnnotation = 'bitbucket.org/default-branch'; + this.events = events; } private createScheduleFn( @@ -154,6 +182,22 @@ export class BitbucketServerEntityProvider implements EntityProvider { async connect(connection: EntityProviderConnection): Promise { this.connection = connection; await this.scheduleFn(); + + if (this.events) { + await this.events.subscribe({ + id: this.getProviderName(), + topics: [TOPIC_REPO_REFS_CHANGED], + onEvent: async params => { + if (params.topic !== TOPIC_REPO_REFS_CHANGED) { + return; + } + + await this.onRepoPush( + params.eventPayload as BitbucketServerEvents.RefsChangedEvent, + ); + }, + }); + } } async refresh(logger: LoggerService) { @@ -218,10 +262,373 @@ export class BitbucketServerEntityProvider implements EntityProvider { presence: 'optional', }, })) { + if (entity.metadata.annotations === undefined) { + entity.metadata.annotations = {}; + } + if (repository.defaultBranch === undefined) { + const defaultBranchResponse = await client.getDefaultBranch({ + repo: repository.slug, + projectKey: project.key, + }); + entity.metadata.annotations[this.defaultBranchAnnotation] = + defaultBranchResponse.displayId; + } else { + entity.metadata.annotations[this.defaultBranchAnnotation] = + repository.defaultBranch; + } result.push(entity); } } } return result; } + + /** + * Checks if the webhook was triggered on a commit to the head branch of a repository + * @param event Bitbucket Server webhook repo:refs_changed event + */ + private isDefaultBranchPush( + defaultBranch: String, + event: BitbucketServerEvents.RefsChangedEvent, + ): boolean { + return event.changes.some(c => defaultBranch === c.ref.displayId); + } + + /** + * Checks if the provider is able to handle events + * @returns Boolean + */ + private canHandleEvents(): boolean { + if ( + this.catalogApi !== undefined && + this.catalogApi !== null && + this.tokenManager !== undefined && + this.tokenManager !== null + ) { + return true; + } + + if (!this.eventConfigErrorThrown) { + this.eventConfigErrorThrown = true; + throw new Error( + `${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.`, + ); + } + + return false; + } + + /** + * Creates a Bitbucket Server location entity for the repository that is referenced in the push event passed in. + * + * @param event A Bitbucket Server push event with repository information. + * + * @returns An array of location entities for the repository. + * + * @example + * + * const RefsChangedEvent = { + * "eventKey": "repo:refs_changed", + * "date": "2022-01-01T00:00:00Z", + * "actor": { + * "name": "johndoe", + * "emailAddress": "johndoe@example.com", + * "id": 123, + * "displayName": "John Doe", + * "active": true, + * "slug": "johndoe", + * "type": "NORMAL" + * }, + * "repository": { + * "slug": "my-repo", + * "id": 123, + * "name": "My Repository", + * "project": { + * "key": "my-project", + * "id": 456, + * "name": "My Project", + * "description": "My project description", + * "public": true, + * "type": "NORMAL" + * } + * }, + * "changes": [ + * { + * "ref": { + * "id": "refs/heads/master", + * "displayId": "master", + * "type": "BRANCH" + * }, + * "refId": "refs/heads/master", + * "fromHash": "0123456789abcdef0123456789abcdef0123456", + * "toHash": "fedcba9876543210fedcba9876543210fedcba9", + * "type": "UPDATE" + * } + * ] + * }; + * + * const locationEntities = await getLocationEntity(RefsChangedEvent); + * + * // locationEntities: + * // [ + * // { + * // kind: 'Location', + * // metadata: { + * // name: 'my-repo', + * // namespace: 'my-project', + * // annotations: { + * // 'backstage.io/managed-by-location': 'url/catalog-info-path', + * // 'backstage.io/managed-by-origin-location': 'url/catalog-info-path', + * // 'host/repo-url': 'url', + * // }, + * // }, + * // spec: { + * // type: 'bitbucket', + * // target: 'url/catalog-info-path', + * // presence: 'optional', + * // }, + * // }, + * // ] + */ + private async getLocationEntity( + event: BitbucketServerEvents.RefsChangedEvent, + ): Promise { + const client = BitbucketServerClient.fromConfig({ + config: this.integration.config, + }); + const result: Entity[] = []; + try { + const repository = await client.getRepository({ + projectKey: event.repository.project.key, + repo: event.repository.slug, + }); + + for await (const entity of this.parser({ + client, + logger: this.logger, + location: { + type: 'url', + target: `${repository.links.self[0].href}${this.config.catalogPath}`, + presence: 'optional', + }, + })) { + entity.metadata.annotations![ + this.targetAnnotation + ] = `${repository.links.self[0].href}${this.config.catalogPath}`; + + if (entity.metadata.annotations === undefined) { + entity.metadata.annotations = {}; + } + + if (repository.defaultBranch === undefined) { + const defaultBranchResponse = await client.getDefaultBranch({ + repo: repository.slug, + projectKey: event.repository.project.key, + }); + entity.metadata.annotations[this.defaultBranchAnnotation] = + defaultBranchResponse.displayId; + } else { + entity.metadata.annotations[this.defaultBranchAnnotation] = + repository.defaultBranch; + } + result.push(entity); + } + } catch (error: any) { + if (error.name === 'NotFoundError') { + this.logger.error(error.message); + } + } + + return result; + } + + /** + * Finds if there are existing location entities for the repository that was pushed. If there are, it simply refreshes those entities, + * if not, it discovers any entity that was added and removed in the list of entities + * @param event - A Bitbucket Server webhook event for repo:refs_change + */ + private async onRepoPush( + event: BitbucketServerEvents.RefsChangedEvent, + ): Promise { + if (!this.canHandleEvents()) { + this.logger.error( + 'Bitbucket Server catalog entity provider is not set up to handle events. Missing tokenManager or catalogApi.', + ); + return; + } + + if (!this.connection) { + throw new Error('Not initialized'); + } + + const repoSlug = event.repository.slug; + const catalogRepoUrl: string = `https://${this.config.host}/projects/${event.repository.project.key}/repos/${repoSlug}/browse${this.config.catalogPath}`; + this.logger.info(`handle repo:push event for ${catalogRepoUrl}`); + const targets = await this.getLocationEntity(event); + if (targets.length === 0) { + this.logger.error('Failed to create location entity.'); + return; + } + const { token } = await this.tokenManager!.getToken(); + const existing = await this.findExistingLocations(catalogRepoUrl, token); + const stillExisting: LocationEntity[] = []; + const removed: DeferredEntity[] = []; + existing.forEach(item => { + if ( + targets.find( + value => + value.metadata.annotations![this.targetAnnotation] === + item.spec.target, + ) + ) { + stillExisting.push(item); + } else { + removed.push({ + locationKey: this.getProviderName(), + entity: item, + }); + } + }); + + const added = await this.getAddedEntities(targets, existing); + + if ( + stillExisting.length > 0 && + stillExisting[0].metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + stillExisting[0].metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } else if ( + added.length > 0 && + added[0].entity.metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + added[0].entity.metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } else if ( + removed.length > 0 && + removed[0].entity.metadata.annotations![this.defaultBranchAnnotation] !== + undefined && + !this.isDefaultBranchPush( + removed[0].entity.metadata.annotations![this.defaultBranchAnnotation], + event, + ) + ) { + return; + } + + const promises: Promise[] = [ + this.connection.refresh({ + keys: stillExisting.map(entity => `url:${entity.spec.target}`), + }), + ]; + + if (added.length > 0 || removed.length > 0) { + promises.push( + this.connection.applyMutation({ + type: 'delta', + added: added, + removed: removed, + }), + ); + } + + await Promise.all(promises); + + return; + } + + /** + * Gets the location entities that are to be newly added to the catalog. + * @param targets Location entities for catalog files in the repository that was pushed + * @param existing The location entities in the repository that was pushed that already exist + * @returns Returns all deferred entities that represent location entities that don't exist in the catalog yet + */ + private async getAddedEntities( + targets: Entity[], + existing: LocationEntity[], + ): Promise { + const added: DeferredEntity[] = toDeferredEntities( + targets.filter( + target => + !existing.find( + item => + item.spec.target === + target.metadata.annotations![this.targetAnnotation], + ), + ), + this.getProviderName(), + ); + return added; + } + + /** + * Finds all location entities in the catalog that already have the annotation `metadata.annotations.${this.config.host}/repo-url` + * that is equivalent to @param repoURL\. + * @param repoURL URL for the reposity that the method finds the existing location entities for + * @param token Token from class token manager + */ + private async findExistingLocations( + catalogRepoUrl: string, + token: string, + ): Promise { + const filter: Record = {}; + filter.kind = 'Location'; + filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl; + + return this.catalogApi!.getEntities({ filter }, { token }).then( + result => result.items, + ) as Promise; + } + + // private static toLocationSpec(target: string): LocationSpec { + // return { + // type: 'url', + // target: target, + // presence: 'required', + // }; + // } +} + +/** + * Converts an array of entities into an array of deferred entities with the provider's name as the location key. + * + * @param targets An array of entities to convert. + * + * @returns An array of deferred entities with the provider's name as the location key. + * + * @example + * + * const entities = [ + * { kind: 'Component', namespace: 'default', name: 'my-component' }, + * { kind: 'System', namespace: 'default', name: 'my-system' }, + * { kind: 'API', namespace: 'default', name: 'my-api' }, + * ]; + * + * const deferredEntities = toDeferredEntities(entities); + * + * // deferredEntities: + * // [ + * // { locationKey: 'my-provider', entity: { kind: 'Component', namespace: 'default', name: 'my-component' } }, + * // { locationKey: 'my-provider', entity: { kind: 'System', namespace: 'default', name: 'my-system' } }, + * // { locationKey: 'my-provider', entity: { kind: 'API', namespace: 'default', name: 'my-api' } }, + * // ] + */ +export function toDeferredEntities( + targets: Entity[], + locationKey: string, +): DeferredEntity[] { + return targets.map(entity => { + return { + locationKey, + entity, + }; + }); } diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts index 018ff23069..3b921b6bab 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.ts @@ -48,10 +48,8 @@ export function readProviderConfigs( // simple/single config variant return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)]; } - return providersConfig.keys().map(id => { const providerConfig = providersConfig.getConfig(id); - return readProviderConfig(id, providerConfig); }); } diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts index 6aea23387e..1c48d5e7bd 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerLocationParser.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { - LocationSpec, - locationSpecToLocationEntity, -} from '@backstage/plugin-catalog-node'; +import { locationSpecToLocationEntity } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; -import { BitbucketServerClient } from '../lib'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { BitbucketServerClient } from '../lib'; /** * A custom callback that reacts to finding a location by yielding entities. diff --git a/plugins/events-backend-module-bitbucket-server/.eslintrc.js b/plugins/events-backend-module-bitbucket-server/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-bitbucket-server/README.md b/plugins/events-backend-module-bitbucket-server/README.md new file mode 100644 index 0000000000..2c8fdff092 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/README.md @@ -0,0 +1,48 @@ +# events-backend-module-bitbucket-server + +Welcome to the `events-backend-module-bitbucket-server` backend plugin! + +This package is a module for the `events-backend` backend plugin +and extends the event system with an `BitbucketServerEventRouter`. + +The event router will subscribe to the topic `bitbucketServer` +and route the events to more concrete topics based on the value +of the provided `x-event-key` metadata field. + +Examples: + +| x-event-key | topic | +| ------------------- | ----------------------------------- | +| `repo:refs_changed` | `bitbucketServer.repo:refs_changed` | +| `repo:modified` | `bitbucketServer.repo:modified` | + +Please find all possible webhook event types at the +[official documentation](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-server +``` + +```ts +// packages/backend/src/index.ts +backend.add( + import('@backstage/plugin-events-backend-module-bitbucket-server/alpha'), +); +``` + +### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new BitbucketCloudEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); +``` diff --git a/plugins/events-backend-module-bitbucket-server/api-report-alpha.md b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md new file mode 100644 index 0000000000..62fb3e2a6b --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/api-report-alpha.md @@ -0,0 +1,14 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server" + +> 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'; + +// @alpha +const eventsModuleBitbucketServerEventRouter: () => BackendFeature; +export default eventsModuleBitbucketServerEventRouter; +export { eventsModuleBitbucketServerEventRouter }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/events-backend-module-bitbucket-server/api-report.md b/plugins/events-backend-module-bitbucket-server/api-report.md new file mode 100644 index 0000000000..18292a0d11 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/api-report.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export class BitbucketServerEventRouter extends SubTopicEventRouter { + constructor(options: { events: EventsService }); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; +} +``` diff --git a/plugins/events-backend-module-bitbucket-server/catalog-info.yaml b/plugins/events-backend-module-bitbucket-server/catalog-info.yaml new file mode 100644 index 0000000000..d2a04ca2a8 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-events-backend-module-bitbucket-server + title: '@backstage/plugin-events-backend-module-bitbucket-server' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json new file mode 100644 index 0000000000..025f559b44 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-events-backend-module-bitbucket-server", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/events-backend-module-bitbucket-server" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-events-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/events-backend-module-bitbucket-server/src/alpha.ts b/plugins/events-backend-module-bitbucket-server/src/alpha.ts new file mode 100644 index 0000000000..c741a37f24 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 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. + */ + +export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter'; +export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/index.ts b/plugins/events-backend-module-bitbucket-server/src/index.ts new file mode 100644 index 0000000000..78c97808a4 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 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. + */ + +/** + * The module "bitbucket-server" for the Backstage backend plugin "events-backend" + * adding an event router for Bitbucket Server. + * + * @packageDocumentation + */ + +export { BitbucketServerEventRouter } from './router/BitbucketServerEventRouter'; diff --git a/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts new file mode 100644 index 0000000000..6fdb4020f3 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { BitbucketServerEventRouter } from './BitbucketServerEventRouter'; + +describe('BitbucketServerEventRouter', () => { + const events = new TestEventsService(); + const eventRouter = new BitbucketServerEventRouter({ events }); + const topic = 'bitbucketServer'; + const eventPayload = { test: 'payload' }; + const metadata = { 'x-event-key': 'test:type' }; + + beforeEach(() => { + events.reset(); + }); + + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { + eventRouter.onEvent({ topic, eventPayload }); + + expect(events.published).toEqual([]); + }); + + it('with x-event-key', () => { + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('bitbucketServer.test:type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts new file mode 100644 index 0000000000..45031cc5f3 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/router/BitbucketServerEventRouter.ts @@ -0,0 +1,45 @@ +/* + * 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 { + EventParams, + EventsService, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `bitbucketServer` topic + * and publishes the events under the more concrete sub-topic + * depending on the `x-event-key` provided. + * + * @public + */ +export class BitbucketServerEventRouter extends SubTopicEventRouter { + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'bitbucketServer', + }); + } + + protected getSubscriberId(): string { + return 'BitbucketServerEventRouter'; + } + + protected determineSubTopic(params: EventParams): string | undefined { + return params.metadata?.['x-event-key'] as string | undefined; + } +} diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts new file mode 100644 index 0000000000..60fc67eb4f --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerEventRouter.ts @@ -0,0 +1,44 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { BitbucketServerEventRouter } from '../router/BitbucketServerEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for Bitbucket Server. + * + * Registers the `BitbucketServerEventRouter`. + * + * @alpha + */ +export const eventsModuleBitbucketServerEventRouter = createBackendModule({ + pluginId: 'events', + moduleId: 'bitbucket-server-event-router', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + }, + async init({ events }) { + const eventRouter = new BitbucketServerEventRouter({ + events, + }); + await eventRouter.subscribe(); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts new file mode 100644 index 0000000000..56e158b8ab --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/service/eventsModuleBitbucketServerRouter.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { createServiceFactory } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { eventsModuleBitbucketServerEventRouter } from './eventsModuleBitbucketServerEventRouter'; + +describe('eventsModuleBitbucketServerEventRouter', () => { + it('should be correctly wired and set up', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + await startTestBackend({ + features: [ + eventsServiceFactory(), + eventsModuleBitbucketServerEventRouter(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter'); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-server/src/setupTests.ts b/plugins/events-backend-module-bitbucket-server/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-server/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ + +export {}; diff --git a/yarn.lock b/yarn.lock index b337860101..42754c71b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5739,11 +5739,13 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 @@ -6557,6 +6559,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + languageName: unknown + linkType: soft + "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit": version: 0.0.0-use.local resolution: "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit"