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:^"