diff --git a/.changeset/silver-flowers-trade.md b/.changeset/silver-flowers-trade.md new file mode 100644 index 0000000000..8701de375c --- /dev/null +++ b/.changeset/silver-flowers-trade.md @@ -0,0 +1,40 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor +--- + +BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support. + +`BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`. +With provided `events`, the event-based updates/refresh will be available. +However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`. + +The event subscription happens on `connect(connection)` if the `events` is available. + +**Migration:** + +```diff + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, + }, + ); +- env.eventBroker.subscribe(bitbucketCloudProvider); +``` + +**New Backend System:** + +Before this change, using this module with the new backend system was broken. +Now, you can add the catalog module for Bitbucket Cloud incl. event support backend. +Event support will always be enabled. +However, no updates/refresh will happen without receiving events. + +```ts +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), +); +``` diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index a7477a2a61..81770afbf8 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -24,7 +24,35 @@ package. yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` -### Installation without Events Support +### 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-cloud/alpha'), +); +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/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 And then add the entity provider to your catalog builder: @@ -49,7 +77,7 @@ export default async function createPlugin( } ``` -### Installation with Events Support +#### Installation with Events Support Please follow the installation instructions at @@ -83,6 +111,7 @@ export default async function createPlugin( env.config, { catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + events: env.events, logger: env.logger, scheduler: env.scheduler, tokenManager: env.tokenManager, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index 22eeddea3a..22845cf260 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -7,18 +7,15 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; -import { EventParams } from '@backstage/plugin-events-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; -import { EventSubscriber } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; // @public -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -26,7 +23,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -37,12 +35,8 @@ export class BitbucketCloudEntityProvider // (undocumented) getTaskId(): string; // (undocumented) - onEvent(params: EventParams): Promise; - // (undocumented) onRepoPush(event: Events.RepoPushEvent): Promise; // (undocumented) - refresh(logger: Logger): Promise; - // (undocumented) - supportsEventTopics(): string[]; + refresh(logger: LoggerService): Promise; } ``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 418e88a003..441781331e 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -56,13 +56,13 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "uuid": "^9.0.0", - "winston": "^3.2.1" + "uuid": "^9.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-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts index c771a40acc..9fff322eab 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts @@ -14,18 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { Duration } from 'luxon'; import { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; describe('catalogModuleBitbucketCloudEntityProvider', () => { 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 addedSubscribers: Array | undefined; let usedSchedule: TaskScheduleDefinition | undefined; const catalogExtensionPointImpl = { @@ -33,11 +43,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { addedProviders = providers; }, }; - const eventsExtensionPointImpl = { - addSubscribers: (subscribers: any) => { - addedSubscribers = subscribers; - }, - }; + const connection = jest.fn() as unknown as EntityProviderConnection; const runner = jest.fn(); const scheduler = mockServices.scheduler.mock({ createScheduledTaskRunner(schedule) { @@ -49,9 +55,9 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, catalogExtensionPointImpl], - [eventsExtensionPoint, eventsExtensionPointImpl], ], features: [ + eventsServiceFactory(), catalogModuleBitbucketCloudEntityProvider(), mockServices.rootConfig.factory({ data: { @@ -75,10 +81,14 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); expect(addedProviders?.length).toEqual(1); - expect(addedProviders?.pop()?.getProviderName()).toEqual( + expect(runner).not.toHaveBeenCalled(); + const provider = addedProviders!.pop()!; + expect(provider.getProviderName()).toEqual( 'bitbucketCloud-provider:default', ); - expect(addedSubscribers).toEqual(addedProviders); - expect(runner).not.toHaveBeenCalled(); + await provider.connect(connection); + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('bitbucketCloud-provider:default'); + expect(runner).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts index 39265dc1a5..21d86ce8e6 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, createBackendModule, @@ -23,7 +22,7 @@ import { catalogProcessingExtensionPoint, catalogServiceRef, } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; /** @@ -38,9 +37,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ catalog: catalogProcessingExtensionPoint, catalogApi: catalogServiceRef, config: coreServices.rootConfig, - // TODO(pjungermann): How to make this optional for those which only want the provider without event support? - // Do we even want to support this? - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, tokenManager: coreServices.tokenManager, @@ -54,16 +51,15 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ scheduler, tokenManager, }) { - const winstonLogger = loggerToWinstonLogger(logger); const providers = BitbucketCloudEntityProvider.fromConfig(config, { catalogApi, - logger: winstonLogger, + events, + logger, scheduler, tokenManager, }); catalog.addEntityProvider(providers); - events.addSubscribers(providers); }, }); }, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 573f21ce60..6a21d95afa 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -29,6 +29,7 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -436,6 +437,7 @@ describe('BitbucketCloudEntityProvider', () => { 'added-module/catalog-custom.yaml', ); + const events = DefaultEventsService.create({ logger }); const catalogApi = { getEntities: async ( request: { filter: Record }, @@ -457,6 +459,7 @@ describe('BitbucketCloudEntityProvider', () => { }; const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, @@ -537,7 +540,7 @@ describe('BitbucketCloudEntityProvider', () => { ); await provider.connect(entityProviderConnection); - await provider.onEvent(repoPushEventParams); + await events.publish(repoPushEventParams); const addedEntities = [ { @@ -566,31 +569,22 @@ describe('BitbucketCloudEntityProvider', () => { }); }); - it('onRepoPush fail on incomplete setup', async () => { - const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - logger, - schedule, - })[0]; - - await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow( - 'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.', - ); - }); - it('no onRepoPush update on non-matching workspace slug', async () => { const catalogApi = { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, @@ -613,15 +607,17 @@ describe('BitbucketCloudEntityProvider', () => { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index 14f561473b..9135e0717f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -15,6 +15,7 @@ */ import { TokenManager } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; import { LocationEntity } from '@backstage/catalog-model'; @@ -35,13 +36,12 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProviderConfig, readProviderConfigs, } from './BitbucketCloudEntityProviderConfig'; import * as uuid from 'uuid'; -import { Logger } from 'winston'; const DEFAULT_BRANCH = 'master'; const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push'; @@ -62,14 +62,13 @@ interface IngestionTarget { * * @public */ -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { private readonly client: BitbucketCloudClient; private readonly config: BitbucketCloudEntityProviderConfig; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private readonly catalogApi?: CatalogApi; + private readonly events?: EventsService; private readonly tokenManager?: TokenManager; private connection?: EntityProviderConnection; @@ -79,7 +78,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -114,6 +114,7 @@ export class BitbucketCloudEntityProvider options.logger, taskRunner, options.catalogApi, + options.events, options.tokenManager, ); }); @@ -122,9 +123,10 @@ export class BitbucketCloudEntityProvider private constructor( config: BitbucketCloudEntityProviderConfig, integration: BitbucketCloudIntegration, - logger: Logger, + logger: LoggerService, taskRunner: TaskRunner, catalogApi?: CatalogApi, + events?: EventsService, tokenManager?: TokenManager, ) { this.client = BitbucketCloudClient.fromConfig(integration.config); @@ -134,6 +136,7 @@ export class BitbucketCloudEntityProvider }); this.scheduleFn = this.createScheduleFn(taskRunner); this.catalogApi = catalogApi; + this.events = events; this.tokenManager = tokenManager; } @@ -176,9 +179,23 @@ export class BitbucketCloudEntityProvider async connect(connection: EntityProviderConnection): Promise { this.connection = connection; await this.scheduleFn(); + + if (this.events) { + await this.events.subscribe({ + id: this.getProviderName(), + topics: [TOPIC_REPO_PUSH], + onEvent: async params => { + if (params.topic !== TOPIC_REPO_PUSH) { + return; + } + + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); + }, + }); + } } - async refresh(logger: Logger) { + async refresh(logger: LoggerService) { if (!this.connection) { throw new Error('Not initialized'); } @@ -198,20 +215,6 @@ export class BitbucketCloudEntityProvider ); } - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ - supportsEventTopics(): string[] { - return [TOPIC_REPO_PUSH]; - } - - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ - async onEvent(params: EventParams): Promise { - if (params.topic !== TOPIC_REPO_PUSH) { - return; - } - - await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); - } - private canHandleEvents(): boolean { if (this.catalogApi && this.tokenManager) { return true; diff --git a/yarn.lock b/yarn.lock index a876413ec3..5439b536e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5303,11 +5303,11 @@ __metadata: "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 uuid: ^9.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft