From 6343c8d9154e30193b5004d4325ff5b0a96ecd35 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 1 Oct 2024 21:18:23 +0200 Subject: [PATCH] fix!: fix event support for Bitbucket Cloud Fixes the event-based updates at `BitbucketCloudEntityProvider`. Previously, this entity provider had optional event support for legacy backends that could be enabled by passing `catalogApi`, `events`, and `tokenManager`. For the new/current backend system, the `catalogModuleBitbucketCloudEntityProvider` (`catalog.bitbucket-cloud-entity-provider`), event support was enabled by default. A recent change removed `tokenManager` as a dependency from the module as well as removed it as input. While this didn't break the instantiation of the module, it broke the event-based updates and led to a runtime misbehavior, accompanied by an info log message. This change will replace the use of `tokenManager` with the use of `auth` (`AuthService`). Additionally, it will make `catalogApi` and `events` required dependencies. For the current backend system, this change is transparent and doesn't require any action. For the legacy backend system, this change will require you to pass those dependencies if you didn't do it already. **BREAKING CHANGES:** > _(For legacy backend users only.)_ > > Previously optional `catalogApi`, and `events` are required now. > A new required dependency `auth` was added. Fixes: #26925 Relates-to: PR #26141 Signed-off-by: Patrick Jungermann --- .changeset/rare-rabbits-flow.md | 29 ++++++ docs/integrations/bitbucketCloud/discovery.md | 31 +------ .../package.json | 1 - .../report.api.md | 14 +-- ...talogModuleBitbucketCloudEntityProvider.ts | 12 ++- .../BitbucketCloudEntityProvider.test.ts | 60 ++++++++++-- .../providers/BitbucketCloudEntityProvider.ts | 92 ++++++++----------- yarn.lock | 1 - 8 files changed, 135 insertions(+), 105 deletions(-) create mode 100644 .changeset/rare-rabbits-flow.md diff --git a/.changeset/rare-rabbits-flow.md b/.changeset/rare-rabbits-flow.md new file mode 100644 index 0000000000..6b64957d59 --- /dev/null +++ b/.changeset/rare-rabbits-flow.md @@ -0,0 +1,29 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor +--- + +Fixes the event-based updates at `BitbucketCloudEntityProvider`. + +Previously, this entity provider had optional event support for legacy backends +that could be enabled by passing `catalogApi`, `events`, and `tokenManager`. + +For the new/current backend system, the `catalogModuleBitbucketCloudEntityProvider` +(`catalog.bitbucket-cloud-entity-provider`), event support was enabled by default. + +A recent change removed `tokenManager` as a dependency from the module as well as removed it as input. +While this didn't break the instantiation of the module, it broke the event-based updates, +and led to a runtime misbehavior, accompanied by an info log message. + +This change will replace the use of `tokenManager` with the use of `auth` (`AuthService`). + +Additionally, to simplify, it will make `catalogApi` and `events` required dependencies. +For the current backend system, this change is transparent and doesn't require any action. +For the legacy backend system, this change will require you to pass those dependencies +if you didn't do it already. + +BREAKING CHANGES: + +_(For legacy backend users only.)_ + +Previously optional `catalogApi`, and `events` are required now. +A new required dependency `auth` was added. diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 6ba9c0aa1f..102feb46d7 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -51,33 +51,6 @@ Further documentation: ### Installation with Legacy Backend System -#### Installation without Events Support - -And then add the entity provider to your catalog builder: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - builder.addEntityProvider( - BitbucketCloudEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - - // .. -} -``` - -#### Installation with Events Support - Please follow the installation instructions at - @@ -104,19 +77,17 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); /* highlight-add-start */ const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( env.config, { + auth: env.auth, catalogApi: new CatalogClient({ discoveryApi: env.discovery }), events: env.events, logger: env.logger, scheduler: env.scheduler, - tokenManager: env.tokenManager, }, ); - env.eventBroker.subscribe(bitbucketCloudProvider); builder.addEntityProvider(bitbucketCloudProvider); /* highlight-add-end */ const { processingEngine, router } = await builder.build(); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 008e7c5311..eecb4011d5 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -51,7 +51,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-common": "^0.25.0", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/report.api.md b/plugins/catalog-backend-module-bitbucket-cloud/report.api.md index d6c1977ce8..d0248b3973 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/report.api.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; @@ -12,7 +13,6 @@ import { EventsService } from '@backstage/plugin-events-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; -import { TokenManager } from '@backstage/backend-common'; // @public export class BitbucketCloudEntityProvider implements EntityProvider { @@ -21,12 +21,12 @@ export class BitbucketCloudEntityProvider implements EntityProvider { static fromConfig( config: Config, options: { - catalogApi?: CatalogApi; - events?: EventsService; + auth: AuthService; + catalogApi: CatalogApi; + events: EventsService; logger: LoggerService; schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; - tokenManager?: TokenManager; }, ): BitbucketCloudEntityProvider[]; getProviderName(): string; @@ -39,7 +39,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // Warnings were encountered during analysis: // -// src/providers/BitbucketCloudEntityProvider.d.ts:28:5 - (ae-undocumented) Missing documentation for "fromConfig". -// src/providers/BitbucketCloudEntityProvider.d.ts:44:5 - (ae-undocumented) Missing documentation for "refresh". -// src/providers/BitbucketCloudEntityProvider.d.ts:47:5 - (ae-undocumented) Missing documentation for "onRepoPush". +// src/providers/BitbucketCloudEntityProvider.d.ts:26:5 - (ae-undocumented) Missing documentation for "fromConfig". +// src/providers/BitbucketCloudEntityProvider.d.ts:42:5 - (ae-undocumented) Missing documentation for "refresh". +// src/providers/BitbucketCloudEntityProvider.d.ts:44:5 - (ae-undocumented) Missing documentation for "onRepoPush". ``` 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 179e74b8c0..8cc8f50d5b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts @@ -34,6 +34,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ register(env) { env.registerInit({ deps: { + auth: coreServices.auth, catalog: catalogProcessingExtensionPoint, catalogApi: catalogServiceRef, config: coreServices.rootConfig, @@ -41,8 +42,17 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ logger: coreServices.logger, scheduler: coreServices.scheduler, }, - async init({ catalog, catalogApi, config, events, logger, scheduler }) { + async init({ + auth, + catalog, + catalogApi, + config, + events, + logger, + scheduler, + }) { const providers = BitbucketCloudEntityProvider.fromConfig(config, { + auth, catalogApi, events, logger, 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 262582b99a..baea86715c 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import { TokenManager } from '@backstage/backend-common'; import { SchedulerServiceTaskInvocationDefinition, SchedulerServiceTaskRunner, @@ -92,11 +91,6 @@ describe('BitbucketCloudEntityProvider', () => { applyMutation: jest.fn(), refresh: jest.fn(), }; - const tokenManager = { - getToken: async () => { - return { token: 'fake-token' }; - }, - } as any as TokenManager; const repoPushEvent: Events.RepoPushEvent = { actor: { type: 'user', @@ -158,8 +152,14 @@ describe('BitbucketCloudEntityProvider', () => { }); it('no provider config', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); const config = new ConfigReader({}); + const events = DefaultEventsService.create({ logger }); const providers = BitbucketCloudEntityProvider.fromConfig(config, { + auth, + catalogApi, + events, logger, schedule, }); @@ -168,7 +168,13 @@ describe('BitbucketCloudEntityProvider', () => { }); it('single simple provider config', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); + const events = DefaultEventsService.create({ logger }); const providers = BitbucketCloudEntityProvider.fromConfig(simpleConfig, { + auth, + catalogApi, + events, logger, schedule, }); @@ -180,14 +186,24 @@ describe('BitbucketCloudEntityProvider', () => { }); it('fail without schedule and scheduler', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); + const events = DefaultEventsService.create({ logger }); + expect(() => BitbucketCloudEntityProvider.fromConfig(simpleConfig, { + auth, + catalogApi, + events, logger, }), ).toThrow('Either schedule or scheduler must be provided.'); }); it('fail with scheduler but no schedule config', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); + const events = DefaultEventsService.create({ logger }); const scheduler = mockServices.scheduler.mock(); const config = new ConfigReader({ catalog: { @@ -201,6 +217,9 @@ describe('BitbucketCloudEntityProvider', () => { expect(() => BitbucketCloudEntityProvider.fromConfig(config, { + auth, + catalogApi, + events, logger, scheduler, }), @@ -210,6 +229,9 @@ describe('BitbucketCloudEntityProvider', () => { }); it('single simple provider config with schedule in config', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); + const events = DefaultEventsService.create({ logger }); const scheduler = mockServices.scheduler.mock(); const config = new ConfigReader({ catalog: { @@ -226,6 +248,9 @@ describe('BitbucketCloudEntityProvider', () => { }); const providers = BitbucketCloudEntityProvider.fromConfig(config, { + auth, + catalogApi, + events, logger, scheduler, }); @@ -237,6 +262,8 @@ describe('BitbucketCloudEntityProvider', () => { }); it('multiple provider configs', () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); const config = new ConfigReader({ catalog: { providers: { @@ -251,7 +278,11 @@ describe('BitbucketCloudEntityProvider', () => { }, }, }); + const events = DefaultEventsService.create({ logger }); const providers = BitbucketCloudEntityProvider.fromConfig(config, { + auth, + catalogApi, + events, logger, schedule, }); @@ -266,7 +297,13 @@ describe('BitbucketCloudEntityProvider', () => { }); it('apply full update on scheduled execution', async () => { + const auth = mockServices.auth.mock(); + const catalogApi = catalogServiceMock.mock(); + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + auth, + catalogApi, + events, logger, schedule, })[0]; @@ -436,6 +473,9 @@ describe('BitbucketCloudEntityProvider', () => { 'added-module/catalog-custom.yaml', ); + const auth = mockServices.auth.mock({ + getPluginRequestToken: async () => ({ token: 'fake-token' }), + }); const events = DefaultEventsService.create({ logger }); const catalogApi = catalogServiceMock.mock({ getEntities: async ( @@ -457,11 +497,11 @@ describe('BitbucketCloudEntityProvider', () => { }, }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + auth, catalogApi, events, logger, schedule, - tokenManager, })[0]; server.use( @@ -569,14 +609,15 @@ describe('BitbucketCloudEntityProvider', () => { }); it('no onRepoPush update on non-matching workspace slug', async () => { + const auth = mockServices.auth.mock(); const catalogApi = catalogServiceMock.mock(); const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + auth, catalogApi, events, logger, schedule, - tokenManager, })[0]; await provider.connect(entityProviderConnection); @@ -599,14 +640,15 @@ describe('BitbucketCloudEntityProvider', () => { }); it('no onRepoPush update on non-matching repo slug', async () => { + const auth = mockServices.auth.mock(); const catalogApi = catalogServiceMock.mock(); const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + auth, catalogApi, events, logger, schedule, - tokenManager, })[0]; await provider.connect(entityProviderConnection); 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 6b19bea4f2..b439f391ca 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { TokenManager } from '@backstage/backend-common'; import { + AuthService, LoggerService, SchedulerService, SchedulerServiceTaskRunner, @@ -66,26 +66,25 @@ interface IngestionTarget { * @public */ export class BitbucketCloudEntityProvider implements EntityProvider { + private readonly auth: AuthService; + private readonly catalogApi: CatalogApi; private readonly client: BitbucketCloudClient; private readonly config: BitbucketCloudEntityProviderConfig; + private readonly events: EventsService; private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; - private readonly catalogApi?: CatalogApi; - private readonly events?: EventsService; - private readonly tokenManager?: TokenManager; - private connection?: EntityProviderConnection; - private eventConfigErrorThrown = false; + private connection?: EntityProviderConnection; static fromConfig( config: Config, options: { - catalogApi?: CatalogApi; - events?: EventsService; + auth: AuthService; + catalogApi: CatalogApi; + events: EventsService; logger: LoggerService; schedule?: SchedulerServiceTaskRunner; scheduler?: SchedulerService; - tokenManager?: TokenManager; }, ): BitbucketCloudEntityProvider[] { const integrations = ScmIntegrations.fromConfig(config); @@ -112,35 +111,35 @@ export class BitbucketCloudEntityProvider implements EntityProvider { options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!); return new BitbucketCloudEntityProvider( + options.auth, + options.catalogApi, providerConfig, + options.events, integration, options.logger, taskRunner, - options.catalogApi, - options.events, - options.tokenManager, ); }); } private constructor( + auth: AuthService, + catalogApi: CatalogApi, config: BitbucketCloudEntityProviderConfig, + events: EventsService, integration: BitbucketCloudIntegration, logger: LoggerService, taskRunner: SchedulerServiceTaskRunner, - catalogApi?: CatalogApi, - events?: EventsService, - tokenManager?: TokenManager, ) { + this.auth = auth; + this.catalogApi = catalogApi; this.client = BitbucketCloudClient.fromConfig(integration.config); this.config = config; + this.events = events; this.logger = logger.child({ target: this.getProviderName(), }); this.scheduleFn = this.createScheduleFn(taskRunner); - this.catalogApi = catalogApi; - this.events = events; - this.tokenManager = tokenManager; } private createScheduleFn( @@ -185,19 +184,17 @@ export class BitbucketCloudEntityProvider implements EntityProvider { 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.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); - }, - }); - } + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); + }, + }); } async refresh(logger: LoggerService) { @@ -220,32 +217,12 @@ export class BitbucketCloudEntityProvider implements EntityProvider { ); } - private canHandleEvents(): boolean { - if (this.catalogApi && this.tokenManager) { - return true; - } - - // throw only once - 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; - } - private enhanceEvent(event: Events.RepoPushEvent): void { // add missing slug event.repository.slug = event.repository.full_name!.split('/', 2)[1]; } async onRepoPush(event: Events.RepoPushEvent): Promise { - if (!this.canHandleEvents()) { - return; - } - if (!this.connection) { throw new Error('Not initialized'); } @@ -273,8 +250,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { const targets = await this.findCatalogFiles(repoSlug); - const { token } = await this.tokenManager!.getToken(); - const existing = await this.findExistingLocations(repoUrl, token); + const existing = await this.findExistingLocations(repoUrl); const added: DeferredEntity[] = this.toDeferredEntities( targets.filter( @@ -321,16 +297,20 @@ export class BitbucketCloudEntityProvider implements EntityProvider { private async findExistingLocations( repoUrl: string, - token: string, ): Promise { const filter: Record = {}; filter.kind = 'Location'; filter[`metadata.annotations.${ANNOTATION_BITBUCKET_CLOUD_REPO_URL}`] = repoUrl; - return this.catalogApi!.getEntities({ filter }, { token }).then( - result => result.items, - ) as Promise; + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + return this.catalogApi + .getEntities({ filter }, { token }) + .then(result => result.items) as Promise; } private async findCatalogFiles( diff --git a/yarn.lock b/yarn.lock index 93d7efa51d..6189cb8e21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5581,7 +5581,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-bitbucket-cloud@workspace:plugins/catalog-backend-module-bitbucket-cloud" dependencies: - "@backstage/backend-common": ^0.25.0 "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^"