From 56969b6e550ff48c5c9cefab40958b50f8dbb99e Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 18:30:14 +0100 Subject: [PATCH 1/9] feat(events): add new events service Signed-off-by: Patrick Jungermann --- .changeset/breezy-cycles-count.md | 39 ++++++ .../events-backend-test-utils/api-report.md | 20 +++- .../src/deprecated.ts | 19 +++ .../events-backend-test-utils/src/index.ts | 1 + .../src/testUtils/TestEventBroker.ts | 5 +- .../src/testUtils/TestEventPublisher.ts | 5 +- .../src/testUtils/TestEventSubscriber.ts | 5 +- .../src/testUtils/TestEventsService.ts | 48 ++++++++ .../src/testUtils/index.ts | 4 +- plugins/events-backend/api-report.md | 9 +- plugins/events-backend/src/deprecated.ts | 18 +++ plugins/events-backend/src/index.ts | 3 +- .../src/service/DefaultEventBroker.test.ts | 18 +-- .../src/service/DefaultEventBroker.ts | 50 ++++---- .../src/service/EventsBackend.ts | 1 + plugins/events-node/api-report-alpha.md | 6 +- plugins/events-node/api-report.md | 39 +++++- plugins/events-node/package.json | 1 + .../src/api/DefaultEventsService.test.ts | 111 ++++++++++++++++++ .../src/api/DefaultEventsService.ts | 104 ++++++++++++++++ plugins/events-node/src/api/EventBroker.ts | 1 + plugins/events-node/src/api/EventPublisher.ts | 4 + .../events-node/src/api/EventSubscriber.ts | 4 + plugins/events-node/src/api/EventsService.ts | 57 +++++++++ plugins/events-node/src/api/index.ts | 9 +- plugins/events-node/src/deprecated.ts | 19 +++ plugins/events-node/src/extensions.ts | 9 ++ plugins/events-node/src/index.ts | 2 + plugins/events-node/src/service.ts | 47 ++++++++ yarn.lock | 1 + 30 files changed, 600 insertions(+), 59 deletions(-) create mode 100644 .changeset/breezy-cycles-count.md create mode 100644 plugins/events-backend-test-utils/src/deprecated.ts create mode 100644 plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts create mode 100644 plugins/events-backend/src/deprecated.ts create mode 100644 plugins/events-node/src/api/DefaultEventsService.test.ts create mode 100644 plugins/events-node/src/api/DefaultEventsService.ts create mode 100644 plugins/events-node/src/api/EventsService.ts create mode 100644 plugins/events-node/src/deprecated.ts create mode 100644 plugins/events-node/src/service.ts diff --git a/.changeset/breezy-cycles-count.md b/.changeset/breezy-cycles-count.md new file mode 100644 index 0000000000..81998d9e8b --- /dev/null +++ b/.changeset/breezy-cycles-count.md @@ -0,0 +1,39 @@ +--- +'@backstage/plugin-events-backend-test-utils': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-node': patch +--- + +Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + +**Summary:** + +- new: + `EventsService`, `eventsServiceRef`, `TestEventsService` +- deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + +Add the `eventsServiceRef` as dependency to your backend plugins +or backend plugin modules. + +**Details:** + +The previous implementation using the `EventsExtensionPoint` was added in the early stages +of the new backend system and does not respect the plugin isolation. +This made it not compatible anymore with the new backend system. + +Additionally, the previous interfaces had some room for simplification, +supporting less exposure of internal concerns as well. + +Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. +The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. +Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, +and used internally. There is no need to expose anything of that at their own interfaces. + +Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable +(by other plugins or their modules) anyway. + +The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. +Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. diff --git a/plugins/events-backend-test-utils/api-report.md b/plugins/events-backend-test-utils/api-report.md index 46131c4244..9630c3d4e2 100644 --- a/plugins/events-backend-test-utils/api-report.md +++ b/plugins/events-backend-test-utils/api-report.md @@ -6,9 +6,11 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { EventsServiceSubscribeOptions } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventBroker implements EventBroker { // (undocumented) publish(params: EventParams): Promise; @@ -22,7 +24,7 @@ export class TestEventBroker implements EventBroker { readonly subscribed: EventSubscriber[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventPublisher implements EventPublisher { // (undocumented) get eventBroker(): EventBroker | undefined; @@ -31,6 +33,20 @@ export class TestEventPublisher implements EventPublisher { } // @public (undocumented) +export class TestEventsService implements EventsService { + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + get published(): EventParams[]; + // (undocumented) + reset(): void; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; + // (undocumented) + get subscribed(): EventsServiceSubscribeOptions[]; +} + +// @public @deprecated (undocumented) export class TestEventSubscriber implements EventSubscriber { constructor(name: string, topics: string[]); // (undocumented) diff --git a/plugins/events-backend-test-utils/src/deprecated.ts b/plugins/events-backend-test-utils/src/deprecated.ts new file mode 100644 index 0000000000..15072dcfb4 --- /dev/null +++ b/plugins/events-backend-test-utils/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2024 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 { TestEventBroker } from './testUtils/TestEventBroker'; +export { TestEventPublisher } from './testUtils/TestEventPublisher'; +export { TestEventSubscriber } from './testUtils/TestEventSubscriber'; diff --git a/plugins/events-backend-test-utils/src/index.ts b/plugins/events-backend-test-utils/src/index.ts index efba5be0d0..da090488fc 100644 --- a/plugins/events-backend-test-utils/src/index.ts +++ b/plugins/events-backend-test-utils/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export * from './deprecated'; export * from './testUtils'; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts index c697a6506f..78556cfc63 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts @@ -20,7 +20,10 @@ import { EventSubscriber, } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated use `TestEventsService` instead + */ export class TestEventBroker implements EventBroker { readonly published: EventParams[] = []; readonly subscribed: EventSubscriber[] = []; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts index c1b2038afb..51bad11278 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts @@ -16,7 +16,10 @@ import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventPublisher` was replaced by `EventsService.publish` + */ export class TestEventPublisher implements EventPublisher { #eventBroker?: EventBroker; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts index ef5758b804..3db9023a9b 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts @@ -16,7 +16,10 @@ import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventSubscriber` was replaced by `EventsService.subscribe`. + */ export class TestEventSubscriber implements EventSubscriber { readonly name: string; readonly topics: string[]; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts new file mode 100644 index 0000000000..c87072711c --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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, + EventsServiceSubscribeOptions, +} from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventsService implements EventsService { + #published: EventParams[] = []; + #subscribed: EventsServiceSubscribeOptions[] = []; + + async publish(params: EventParams): Promise { + this.#published.push(params); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + this.#subscribed.push(options); + } + + get published(): EventParams[] { + return this.#published; + } + + get subscribed(): EventsServiceSubscribeOptions[] { + return this.#subscribed; + } + + reset(): void { + this.#published = []; + this.#subscribed = []; + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/index.ts b/plugins/events-backend-test-utils/src/testUtils/index.ts index a571ba3075..d9eb544628 100644 --- a/plugins/events-backend-test-utils/src/testUtils/index.ts +++ b/plugins/events-backend-test-utils/src/testUtils/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { TestEventBroker } from './TestEventBroker'; -export { TestEventPublisher } from './TestEventPublisher'; -export { TestEventSubscriber } from './TestEventSubscriber'; +export { TestEventsService } from './TestEventsService'; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index aeb6f9363d..9fffe719ba 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -7,14 +7,17 @@ import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; -// @public +// @public @deprecated export class DefaultEventBroker implements EventBroker { - constructor(logger: Logger); + // @deprecated + constructor(logger: LoggerService, events?: EventsService); // (undocumented) publish(params: EventParams): Promise; // (undocumented) @@ -23,7 +26,7 @@ export class DefaultEventBroker implements EventBroker { ): void; } -// @public +// @public @deprecated export class EventsBackend { constructor(logger: Logger); // (undocumented) diff --git a/plugins/events-backend/src/deprecated.ts b/plugins/events-backend/src/deprecated.ts new file mode 100644 index 0000000000..cce853b2af --- /dev/null +++ b/plugins/events-backend/src/deprecated.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 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 { EventsBackend } from './service/EventsBackend'; +export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index be173b677c..63dfa5d252 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -20,6 +20,5 @@ * @packageDocumentation */ -export { EventsBackend } from './service/EventsBackend'; +export * from './deprecated'; export { HttpPostIngressEventPublisher } from './service/http'; -export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/service/DefaultEventBroker.test.ts b/plugins/events-backend/src/service/DefaultEventBroker.test.ts index 99e5f6f72d..5317251726 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.test.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.test.ts @@ -85,15 +85,15 @@ describe('DefaultEventBroker', () => { } })(); - const errorSpy = jest.spyOn(logger, 'error'); + const warnSpy = jest.spyOn(logger, 'warn'); const eventBroker = new DefaultEventBroker(logger); eventBroker.subscribe(subscriber1); await eventBroker.publish({ topic, eventPayload: '1' }); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 1'), ); @@ -101,13 +101,13 @@ describe('DefaultEventBroker', () => { await eventBroker.publish({ topic, eventPayload: '2' }); // With two subscribers we should not halt on the first error but call all subscribers - expect(errorSpy).toHaveBeenCalledTimes(3); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber2" failed to process event', + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber2" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); }); diff --git a/plugins/events-backend/src/service/DefaultEventBroker.ts b/plugins/events-backend/src/service/DefaultEventBroker.ts index c3824b3e7d..27523f3118 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { LoggerService } from '@backstage/backend-plugin-api'; import { + DefaultEventsService, EventBroker, EventParams, + EventsService, EventSubscriber, } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; /** * In process event broker which will pass the event to all registered subscribers @@ -27,44 +29,34 @@ import { Logger } from 'winston'; * Events will not be persisted in any form. * * @public + * @deprecated use `DefaultEventsService` from `@backstage/plugin-events-node` instead */ -// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) export class DefaultEventBroker implements EventBroker { - constructor(private readonly logger: Logger) {} + private readonly events: EventsService; - private readonly subscribers: { - [topic: string]: EventSubscriber[]; - } = {}; + /** + * + * @param logger - logger + * @param events - replacement that gets wrapped to support not yet migrated implementations. + * An instance can be passed (required for a mixed mode), otherwise a new instance gets created internally. + * @deprecated use `DefaultEventsService` directly instead + */ + constructor(logger: LoggerService, events?: EventsService) { + this.events = events ?? DefaultEventsService.create({ logger }); + } async publish(params: EventParams): Promise { - this.logger.debug( - `Event received: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - - const subscribed = this.subscribers[params.topic] ?? []; - await Promise.all( - subscribed.map(async subscriber => { - try { - await subscriber.onEvent(params); - } catch (error) { - this.logger.error( - `Subscriber "${subscriber.constructor.name}" failed to process event`, - error, - ); - } - }), - ); + return this.events.publish(params); } subscribe( ...subscribers: Array> ): void { - subscribers.flat().forEach(subscriber => { - subscriber.supportsEventTopics().forEach(topic => { - this.subscribers[topic] = this.subscribers[topic] ?? []; - this.subscribers[topic].push(subscriber); + subscribers.flat().forEach(async subscriber => { + await this.events.subscribe({ + id: subscriber.constructor.name, + topics: subscriber.supportsEventTopics(), + onEvent: subscriber.onEvent.bind(subscriber), }); }); } diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts index 4415b8703a..2c93663b46 100644 --- a/plugins/events-backend/src/service/EventsBackend.ts +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -26,6 +26,7 @@ import { DefaultEventBroker } from './DefaultEventBroker'; * A builder that helps wire up all component parts of the event management. * * @public + * @deprecated `EventBroker`, `EventPublisher`, and `EventSubscriber` got replaced by `EventsService` and its methods. */ export class EventsBackend { private eventBroker: EventBroker; diff --git a/plugins/events-node/api-report-alpha.md b/plugins/events-node/api-report-alpha.md index fd30f54d45..f61048ac94 100644 --- a/plugins/events-node/api-report-alpha.md +++ b/plugins/events-node/api-report-alpha.md @@ -13,15 +13,15 @@ import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; export interface EventsExtensionPoint { // (undocumented) addHttpPostIngress(options: HttpPostIngressOptions): void; - // (undocumented) + // @deprecated (undocumented) addPublishers( ...publishers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) addSubscribers( ...subscribers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): void; } diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index dfda48d97e..081c56549a 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -3,7 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + // @public +export class DefaultEventsService implements EventsService { + // (undocumented) + static create(options: { logger: LoggerService }): DefaultEventsService; + forPlugin(pluginId: string): EventsService; + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public @deprecated export interface EventBroker { publish(params: EventParams): Promise; subscribe( @@ -18,9 +32,9 @@ export interface EventParams { topic: string; } -// @public +// @public @deprecated export interface EventPublisher { - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } @@ -39,8 +53,29 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // @public +export interface EventsService { + publish(params: EventParams): Promise; + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public (undocumented) +export type EventsServiceEventHandler = (params: EventParams) => Promise; + +// @public +export const eventsServiceRef: ServiceRef; + +// @public (undocumented) +export type EventsServiceSubscribeOptions = { + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +// @public @deprecated export interface EventSubscriber { + // @deprecated onEvent(params: EventParams): Promise; + // @deprecated supportsEventTopics(): string[]; } diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 58ddd50413..de794dfa45 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -45,6 +45,7 @@ "@backstage/backend-plugin-api": "workspace:^" }, "devDependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ diff --git a/plugins/events-node/src/api/DefaultEventsService.test.ts b/plugins/events-node/src/api/DefaultEventsService.test.ts new file mode 100644 index 0000000000..33df923892 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { DefaultEventsService } from './DefaultEventsService'; +import { EventParams } from './EventParams'; + +const logger = getVoidLogger(); + +describe('DefaultEventsService', () => { + it('passes events to interested subscribers', async () => { + const events = DefaultEventsService.create({ logger }); + const eventsSubscriber1: EventParams[] = []; + const eventsSubscriber2: EventParams[] = []; + + await events.subscribe({ + id: 'subscriber1', + topics: ['topicA', 'topicB'], + onEvent: async event => { + eventsSubscriber1.push(event); + }, + }); + await events.subscribe({ + id: 'subscriber2', + topics: ['topicB', 'topicC'], + onEvent: async event => { + eventsSubscriber2.push(event); + }, + }); + await events.publish({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + await events.publish({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + await events.publish({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + await events.publish({ + topic: 'topicD', + eventPayload: { test: 'topicD' }, + }); + + expect(eventsSubscriber1).toEqual([ + { topic: 'topicA', eventPayload: { test: 'topicA' } }, + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + ]); + expect(eventsSubscriber2).toEqual([ + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + { topic: 'topicC', eventPayload: { test: 'topicC' } }, + ]); + }); + + it('logs errors from subscribers', async () => { + const topic = 'testTopic'; + + const warnSpy = jest.spyOn(logger, 'warn'); + const events = DefaultEventsService.create({ logger }); + + await events.subscribe({ + id: 'subscriber1', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '1' }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 1'), + ); + + await events.subscribe({ + id: 'subscriber2', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '2' }); + + // With two subscribers we should not halt on the first error but call all subscribers + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber2" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + }); +}); diff --git a/plugins/events-node/src/api/DefaultEventsService.ts b/plugins/events-node/src/api/DefaultEventsService.ts new file mode 100644 index 0000000000..bb5c2a0ca8 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2024 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { EventParams } from './EventParams'; +import { EventsService, EventsServiceSubscribeOptions } from './EventsService'; + +/** + * In-process event broker which will pass the event to all registered subscribers + * interested in it. + * Events will not be persisted in any form. + * Events will not be passed to subscribers at other instances of the same cluster. + * + * @public + */ +// TODO(pjungermann): add opentelemetry? (see plugins/catalog-backend/src/util/opentelemetry.ts, etc.) +export class DefaultEventsService implements EventsService { + private readonly subscribers = new Map< + string, + Omit[] + >(); + + private constructor(private readonly logger: LoggerService) {} + + static create(options: { logger: LoggerService }): DefaultEventsService { + return new DefaultEventsService(options.logger); + } + + /** + * Returns a plugin-scoped context of the `EventService` + * that ensures to prefix subscriber IDs with the plugin ID. + * + * @param pluginId - The plugin that the `EventService` should be created for. + */ + forPlugin(pluginId: string): EventsService { + return { + publish: (params: EventParams): Promise => { + return this.publish(params); + }, + subscribe: (options: EventsServiceSubscribeOptions): Promise => { + return this.subscribe({ + ...options, + id: `${pluginId}.${options.id}`, + }); + }, + }; + } + + async publish(params: EventParams): Promise { + this.logger.debug( + `Event received: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + + if (!this.subscribers.has(params.topic)) { + return; + } + + const onEventPromises: Promise[] = []; + this.subscribers.get(params.topic)?.forEach(subscription => { + onEventPromises.push( + (async () => { + try { + await subscription.onEvent(params); + } catch (error) { + this.logger.warn( + `Subscriber "${subscription.id}" failed to process event for topic "${params.topic}"`, + error, + ); + } + })(), + ); + }); + + await Promise.all(onEventPromises); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + options.topics.forEach(topic => { + if (!this.subscribers.has(topic)) { + this.subscribers.set(topic, []); + } + + this.subscribers.get(topic)!.push({ + id: options.id, + onEvent: options.onEvent, + }); + }); + } +} diff --git a/plugins/events-node/src/api/EventBroker.ts b/plugins/events-node/src/api/EventBroker.ts index 736c2a2bf0..f6afdf2f14 100644 --- a/plugins/events-node/src/api/EventBroker.ts +++ b/plugins/events-node/src/api/EventBroker.ts @@ -23,6 +23,7 @@ import { EventSubscriber } from './EventSubscriber'; * others can subscribe for future events for topics they are interested in. * * @public + * @deprecated use `EventsService` instead */ export interface EventBroker { /** diff --git a/plugins/events-node/src/api/EventPublisher.ts b/plugins/events-node/src/api/EventPublisher.ts index 285f427804..9089eb33fa 100644 --- a/plugins/events-node/src/api/EventPublisher.ts +++ b/plugins/events-node/src/api/EventPublisher.ts @@ -23,7 +23,11 @@ import { EventBroker } from './EventBroker'; * or from event brokers, queues, etc. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventPublisher { + /** + * @deprecated use the `EventsService` via the constructor, setter, or other means instead + */ setEventBroker(eventBroker: EventBroker): Promise; } diff --git a/plugins/events-node/src/api/EventSubscriber.ts b/plugins/events-node/src/api/EventSubscriber.ts index 439f49b890..3686a8db3f 100644 --- a/plugins/events-node/src/api/EventSubscriber.ts +++ b/plugins/events-node/src/api/EventSubscriber.ts @@ -22,10 +22,13 @@ import { EventParams } from './EventParams'; * or other actions to react on events. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventSubscriber { /** * Supported event topics like "github", "bitbucketCloud", etc. + * + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ supportsEventTopics(): string[]; @@ -33,6 +36,7 @@ export interface EventSubscriber { * React on a received event. * * @param params - parameters for the to be received event. + * @deprecated you are not required to expose this anymore when using `EventsService` */ onEvent(params: EventParams): Promise; } diff --git a/plugins/events-node/src/api/EventsService.ts b/plugins/events-node/src/api/EventsService.ts new file mode 100644 index 0000000000..7af13f9b07 --- /dev/null +++ b/plugins/events-node/src/api/EventsService.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 } from './EventParams'; + +/** + * Allows a decoupled and asynchronous communication between components. + * Components can publish events for a given topic and + * others can subscribe for future events for topics they are interested in. + * + * @public + */ +export interface EventsService { + /** + * Publishes an event for the topic. + * + * @param params - parameters for the to be published event. + */ + publish(params: EventParams): Promise; + + /** + * Subscribes to one or more topics, registering an event handler for them. + * + * @param options - event subscription options. + */ + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +/** + * @public + */ +export type EventsServiceSubscribeOptions = { + /** + * Identifier for the subscription. E.g., used as part of log messages. + */ + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +/** + * @public + */ +export type EventsServiceEventHandler = (params: EventParams) => Promise; diff --git a/plugins/events-node/src/api/index.ts b/plugins/events-node/src/api/index.ts index 91711c0e38..94d3014dff 100644 --- a/plugins/events-node/src/api/index.ts +++ b/plugins/events-node/src/api/index.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -export type { EventBroker } from './EventBroker'; export type { EventParams } from './EventParams'; -export type { EventPublisher } from './EventPublisher'; export { EventRouter } from './EventRouter'; -export type { EventSubscriber } from './EventSubscriber'; +export type { + EventsService, + EventsServiceSubscribeOptions, + EventsServiceEventHandler, +} from './EventsService'; +export { DefaultEventsService } from './DefaultEventsService'; export * from './http'; export { SubTopicEventRouter } from './SubTopicEventRouter'; diff --git a/plugins/events-node/src/deprecated.ts b/plugins/events-node/src/deprecated.ts new file mode 100644 index 0000000000..615e7b81ea --- /dev/null +++ b/plugins/events-node/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2024 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 type { EventBroker } from './api/EventBroker'; +export type { EventPublisher } from './api/EventPublisher'; +export type { EventSubscriber } from './api/EventSubscriber'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 2e44b381af..90add52563 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -26,12 +26,21 @@ import { * @alpha */ export interface EventsExtensionPoint { + /** + * @deprecated use `eventsServiceRef` and `eventsServiceFactory` instead + */ setEventBroker(eventBroker: EventBroker): void; + /** + * @deprecated use `EventsService.publish` instead + */ addPublishers( ...publishers: Array> ): void; + /** + * @deprecated use `EventsService.subscribe` instead + */ addSubscribers( ...subscribers: Array> ): void; diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts index 2bdf456f13..2bd93a8aea 100644 --- a/plugins/events-node/src/index.ts +++ b/plugins/events-node/src/index.ts @@ -21,3 +21,5 @@ */ export * from './api'; +export * from './deprecated'; +export { eventsServiceRef } from './service'; diff --git a/plugins/events-node/src/service.ts b/plugins/events-node/src/service.ts new file mode 100644 index 0000000000..e1d3047fca --- /dev/null +++ b/plugins/events-node/src/service.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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 { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { EventsService, DefaultEventsService } from './api'; + +/** + * The {@link EventsService} that allows to publish events, and subscribe to topics. + * Uses the `root` scope so that events can be shared across all plugins, modules, and more. + * + * @public + */ +export const eventsServiceRef = createServiceRef({ + id: 'events.service', + scope: 'plugin', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + pluginMetadata: coreServices.pluginMetadata, + rootLogger: coreServices.rootLogger, + }, + async createRootContext({ rootLogger }) { + return DefaultEventsService.create({ logger: rootLogger }); + }, + async factory({ pluginMetadata }, eventsService) { + return eventsService.forPlugin(pluginMetadata.getId()); + }, + }), +}); diff --git a/yarn.lock b/yarn.lock index 7271c5f6bb..f49f9f73c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6495,6 +6495,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-events-node@workspace:plugins/events-node" dependencies: + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown From eff3ca9ddd5da779e0fabf3e677f6a76e790a056 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 19:20:26 +0100 Subject: [PATCH 2/9] feat(events)!: migrate `EventRouter` implementations from `EventBroker` to `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/kind-students-cross.md | 76 +++++++++++++++++++ .../events-backend-module-azure/api-report.md | 5 +- .../events-backend-module-azure/package.json | 3 +- .../src/router/AzureDevOpsEventRouter.test.ts | 33 ++++---- .../src/router/AzureDevOpsEventRouter.ts | 12 ++- ...eventsModuleAzureDevOpsEventRouter.test.ts | 34 ++++----- .../eventsModuleAzureDevOpsEventRouter.ts | 12 +-- .../api-report.md | 5 +- .../package.json | 3 +- .../router/BitbucketCloudEventRouter.test.ts | 33 ++++---- .../src/router/BitbucketCloudEventRouter.ts | 12 ++- ...ntsModuleBitbucketCloudEventRouter.test.ts | 37 +++++---- .../eventsModuleBitbucketCloudEventRouter.ts | 12 +-- .../api-report.md | 5 +- .../events-backend-module-gerrit/package.json | 3 +- .../src/router/GerritEventRouter.test.ts | 33 ++++---- .../src/router/GerritEventRouter.ts | 12 ++- .../eventsModuleGerritEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGerritEventRouter.ts | 10 +-- .../api-report.md | 5 +- .../events-backend-module-github/package.json | 3 +- .../src/router/GithubEventRouter.test.ts | 33 ++++---- .../src/router/GithubEventRouter.ts | 12 ++- .../eventsModuleGithubEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGithubEventRouter.ts | 10 +-- .../api-report.md | 5 +- .../events-backend-module-gitlab/package.json | 3 +- .../src/router/GitlabEventRouter.test.ts | 33 ++++---- .../src/router/GitlabEventRouter.ts | 12 ++- .../eventsModuleGitlabEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGitlabEventRouter.ts | 10 +-- plugins/events-node/api-report.md | 14 ++-- .../events-node/src/api/EventRouter.test.ts | 39 +++++----- plugins/events-node/src/api/EventRouter.ts | 44 ++++++++--- .../src/api/SubTopicEventRouter.test.ts | 35 ++++----- .../src/api/SubTopicEventRouter.ts | 12 +-- yarn.lock | 5 -- 37 files changed, 429 insertions(+), 288 deletions(-) create mode 100644 .changeset/kind-students-cross.md diff --git a/.changeset/kind-students-cross.md b/.changeset/kind-students-cross.md new file mode 100644 index 0000000000..9f9d2d40a6 --- /dev/null +++ b/.changeset/kind-students-cross.md @@ -0,0 +1,76 @@ +--- +'@backstage/plugin-events-backend-module-bitbucket-cloud': minor +'@backstage/plugin-events-backend-module-gerrit': minor +'@backstage/plugin-events-backend-module-github': minor +'@backstage/plugin-events-backend-module-gitlab': minor +'@backstage/plugin-events-backend-module-azure': minor +'@backstage/plugin-events-node': minor +--- + +BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + +`EventRouter` uses the new `EventsService` instead of the `EventBroker` now, +causing a breaking change to its signature. + +All of its extensions and implementations got adjusted accordingly. +(`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, +`GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + +Required adjustments were made to all backend modules for the new backend system, +now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + +**Migration:** + +Example for implementations of `SubTopicEventRouter`: + +```diff + import { + EventParams, ++ EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { +- constructor() { +- super('github'); ++ constructor(options: { events: EventsService }) { ++ super({ ++ events: options.events, ++ topic: 'github', ++ }); + } + ++ protected getSubscriberId(): string { ++ return 'GithubEventRouter'; ++ } ++ + // ... + } +``` + +Example for a direct extension of `EventRouter`: + +```diff + class MyEventRouter extends EventRouter { +- constructor(/* ... */) { ++ constructor(options: { ++ events: EventsService; ++ // ... ++ }) { +- super(); + // ... ++ super({ ++ events: options.events, ++ topics: topics, ++ }); + } ++ ++ protected getSubscriberId(): string { ++ return 'MyEventRouter'; ++ } +- +- supportsEventTopics(): string[] { +- return this.topics; +- } + } +``` diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md index 4460aeb510..66529ef2d4 100644 --- a/plugins/events-backend-module-azure/api-report.md +++ b/plugins/events-backend-module-azure/api-report.md @@ -4,12 +4,15 @@ ```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 AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 9187f74024..0ee9f2dbfc 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts index 56e761a8fc..837888a05a 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter'; describe('AzureDevOpsEventRouter', () => { - const eventRouter = new AzureDevOpsEventRouter(); + const events = new TestEventsService(); + const eventRouter = new AzureDevOpsEventRouter({ events: events }); const topic = 'azureDevOps'; const eventPayload = { eventType: 'test.type', test: 'payload' }; const metadata = {}; - it('no $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.eventType', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('azureDevOps.test.type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published).toHaveLength(1); + expect(events.published[0].topic).toEqual('azureDevOps.test.type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts index 11dd7546dd..de05abd032 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor() { - super('azureDevOps'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'azureDevOps', + }); + } + + protected getSubscriberId(): string { + return 'AzureDevOpsEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts index d67b5eb071..d2afb81851 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -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 { eventsModuleAzureDevOpsEventRouter } from './eventsModuleAzureDevOpsEventRouter'; -import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; describe('eventsModuleAzureDevOpsEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: AzureDevOpsEventRouter | undefined; - let addedSubscriber: AzureDevOpsEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleAzureDevOpsEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleAzureDevOpsEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); }); }); diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts index 50bc384502..9741015477 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleAzureDevOpsEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new AzureDevOpsEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new AzureDevOpsEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md index 4795edd89a..ba4f61d739 100644 --- a/plugins/events-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -4,12 +4,15 @@ ```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 BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 609fac199c..5273652f44 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts index b7a47984e4..65a4f1bf21 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter'; describe('BitbucketCloudEventRouter', () => { - const eventRouter = new BitbucketCloudEventRouter(); + const events = new TestEventsService(); + const eventRouter = new BitbucketCloudEventRouter({ events }); const topic = 'bitbucketCloud'; const eventPayload = { test: 'payload' }; const metadata = { 'x-event-key': 'test:type' }; - it('no x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('bitbucketCloud.test:type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('bitbucketCloud.test:type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts index 8350511d65..0f3ce09abf 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor() { - super('bitbucketCloud'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'bitbucketCloud', + }); + } + + protected getSubscriberId(): string { + return 'BitbucketCloudEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts index 025d994b4b..337e2206e4 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts @@ -14,32 +14,31 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -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 { eventsModuleBitbucketCloudEventRouter } from './eventsModuleBitbucketCloudEventRouter'; -import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; describe('eventsModuleBitbucketCloudEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: BitbucketCloudEventRouter | undefined; - let addedSubscriber: BitbucketCloudEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleBitbucketCloudEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter); + await startTestBackend({ + features: [ + eventsServiceFactory(), + eventsModuleBitbucketCloudEventRouter(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts index 841a463001..648d6c67fb 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleBitbucketCloudEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new BitbucketCloudEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new BitbucketCloudEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md index ba3c4dd29f..c75857aa43 100644 --- a/plugins/events-backend-module-gerrit/api-report.md +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -4,12 +4,15 @@ ```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 GerritEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 889c60c685..436fbde891 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts index 7302a26012..6c635fcbdf 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GerritEventRouter } from './GerritEventRouter'; describe('GerritEventRouter', () => { - const eventRouter = new GerritEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GerritEventRouter({ events: events }); const topic = 'gerrit'; const eventPayload = { type: 'test-type', test: 'payload' }; const metadata = {}; - it('no $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.type', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gerrit.test-type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gerrit.test-type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts index 3d97508b62..dac5aa34d5 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GerritEventRouter extends SubTopicEventRouter { - constructor() { - super('gerrit'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gerrit', + }); + } + + protected getSubscriberId(): string { + return 'GerritEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts index c11f4c42db..4fc971fcce 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleGerritEventRouter } from './eventsModuleGerritEventRouter'; -import { GerritEventRouter } from '../router/GerritEventRouter'; describe('eventsModuleGerritEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GerritEventRouter | undefined; - let addedSubscriber: GerritEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGerritEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GerritEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GerritEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGerritEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); }); }); diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts index 780ff7f878..8d9792c4f4 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GerritEventRouter } from '../router/GerritEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGerritEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GerritEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GerritEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md index bcf9b8ed74..5341f86039 100644 --- a/plugins/events-backend-module-github/api-report.md +++ b/plugins/events-backend-module-github/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -15,8 +16,10 @@ export function createGithubSignatureValidator( // @public export class GithubEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index f78858d981..209167f64c 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -44,8 +44,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "@octokit/webhooks-methods": "^3.0.0", - "winston": "^3.2.1" + "@octokit/webhooks-methods": "^3.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts index 14cf6b9933..47f7eeb99f 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GithubEventRouter } from './GithubEventRouter'; describe('GithubEventRouter', () => { - const eventRouter = new GithubEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GithubEventRouter({ events: events }); const topic = 'github'; const eventPayload = { test: 'payload' }; const metadata = { 'x-github-event': 'test_type' }; - it('no x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-github-event', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('github.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('github.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts index 10dd1c55c6..767ed784f2 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GithubEventRouter extends SubTopicEventRouter { - constructor() { - super('github'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'github', + }); + } + + protected getSubscriberId(): string { + return 'GithubEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts index 02151d0dcf..f147bbcb69 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -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 { eventsModuleGithubEventRouter } from './eventsModuleGithubEventRouter'; -import { GithubEventRouter } from '../router/GithubEventRouter'; describe('eventsModuleGithubEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GithubEventRouter | undefined; - let addedSubscriber: GithubEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGithubEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GithubEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GithubEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGithubEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); }); }); diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts index 093307dfaf..694b4b162d 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GithubEventRouter } from '../router/GithubEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGithubEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GithubEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GithubEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md index 8a0c513857..f348436375 100644 --- a/plugins/events-backend-module-gitlab/api-report.md +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,8 +14,10 @@ export function createGitlabTokenValidator(config: Config): RequestValidator; // @public export class GitlabEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index f59b6e5faf..08abb12b8f 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -43,8 +43,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts index 6ced12d3cb..bc9da24cbe 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GitlabEventRouter } from './GitlabEventRouter'; describe('GitlabEventRouter', () => { - const eventRouter = new GitlabEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GitlabEventRouter({ events: events }); const topic = 'gitlab'; const eventPayload = { event_name: 'test_type', test: 'payload' }; const metadata = {}; - it('no $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.event_name', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gitlab.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gitlab.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts index 16324340ee..23b0389b55 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GitlabEventRouter extends SubTopicEventRouter { - constructor() { - super('gitlab'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gitlab', + }); + } + + protected getSubscriberId(): string { + return 'GitlabEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts index 34a68ccbe4..9195be7a73 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -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 { eventsModuleGitlabEventRouter } from './eventsModuleGitlabEventRouter'; -import { GitlabEventRouter } from '../router/GitlabEventRouter'; describe('eventsModuleGitlabEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GitlabEventRouter | undefined; - let addedSubscriber: GitlabEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGitlabEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GitlabEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGitlabEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); }); }); diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts index fc44e95057..66245efb58 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGitlabEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GitlabEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GitlabEventRouter({ events: events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 081c56549a..9574d6c098 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -39,17 +39,17 @@ export interface EventPublisher { } // @public -export abstract class EventRouter implements EventPublisher, EventSubscriber { +export abstract class EventRouter { + protected constructor(options: { events: EventsService; topics: string[] }); // (undocumented) protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; // (undocumented) + protected abstract getSubscriberId(): string; + // (undocumented) onEvent(params: EventParams): Promise; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; - // (undocumented) - abstract supportsEventTopics(): string[]; + subscribe(): Promise; } // @public @@ -114,12 +114,10 @@ export type RequestValidator = ( // @public export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(topic: string); + protected constructor(options: { events: EventsService; topic: string }); // (undocumented) protected determineDestinationTopic(params: EventParams): string | undefined; // (undocumented) protected abstract determineSubTopic(params: EventParams): string | undefined; - // (undocumented) - supportsEventTopics(): string[]; } ``` diff --git a/plugins/events-node/src/api/EventRouter.test.ts b/plugins/events-node/src/api/EventRouter.test.ts index 551c5ea67d..f7709ead64 100644 --- a/plugins/events-node/src/api/EventRouter.test.ts +++ b/plugins/events-node/src/api/EventRouter.test.ts @@ -14,11 +14,19 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; class TestEventRouter extends EventRouter { + constructor(events: EventsService) { + super({ events, topics: ['my-topic'] }); + } + + protected getSubscriberId(): string { + return 'TestEventRouter'; + } + protected determineDestinationTopic(params: EventParams): string | undefined { const payload = params.eventPayload as { value?: number }; if (payload.value === undefined) { @@ -27,26 +35,21 @@ class TestEventRouter extends EventRouter { return payload.value % 2 === 0 ? 'even' : 'odd'; } - - supportsEventTopics(): string[] { - return ['my-topic']; - } } describe('EventRouter', () => { - const eventRouter = new TestEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestEventRouter(events); const topic = 'my-topic'; const metadata = { random: 'metadata' }; it('no destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload: { discarded: 'event' }, @@ -57,14 +60,6 @@ describe('EventRouter', () => { }); it('with destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - const payloadEven = { value: 2 }; const payloadOdd = { value: 3 }; await eventRouter.onEvent({ topic, eventPayload: payloadEven, metadata }); diff --git a/plugins/events-node/src/api/EventRouter.ts b/plugins/events-node/src/api/EventRouter.ts index b435ef15f4..5e4492d25f 100644 --- a/plugins/events-node/src/api/EventRouter.ts +++ b/plugins/events-node/src/api/EventRouter.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; -import { EventPublisher } from './EventPublisher'; -import { EventSubscriber } from './EventSubscriber'; +import { EventsService } from './EventsService'; /** * Subscribes to a topic and - depending on a set of conditions - @@ -26,13 +24,41 @@ import { EventSubscriber } from './EventSubscriber'; * @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}. * @public */ -export abstract class EventRouter implements EventPublisher, EventSubscriber { - private eventBroker?: EventBroker; +export abstract class EventRouter { + private readonly events: EventsService; + private readonly topics: string[]; + private subscribed: boolean = false; + + protected constructor(options: { events: EventsService; topics: string[] }) { + this.events = options.events; + this.topics = options.topics; + } + + protected abstract getSubscriberId(): string; protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; + /** + * Subscribes itself to the topic(s), + * after which events potentially can be received + * and processed by {@link EventRouter.onEvent}. + */ + async subscribe(): Promise { + if (this.subscribed) { + return; + } + + this.subscribed = true; + + await this.events.subscribe({ + id: this.getSubscriberId(), + topics: this.topics, + onEvent: this.onEvent.bind(this), + }); + } + async onEvent(params: EventParams): Promise { const topic = this.determineDestinationTopic(params); @@ -41,15 +67,9 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // republish to different topic - this.eventBroker?.publish({ + await this.events.publish({ ...params, topic, }); } - - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - - abstract supportsEventTopics(): string[]; } diff --git a/plugins/events-node/src/api/SubTopicEventRouter.test.ts b/plugins/events-node/src/api/SubTopicEventRouter.test.ts index d5c79895a5..6298e1549d 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.test.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.test.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; +import { EventsService } from './EventsService'; import { SubTopicEventRouter } from './SubTopicEventRouter'; class TestSubTopicEventRouter extends SubTopicEventRouter { - constructor() { - super('my-topic'); + constructor(events: EventsService) { + super({ events, topic: 'my-topic' }); + } + + protected getSubscriberId(): string { + return 'TestSubTopicEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { @@ -29,34 +33,25 @@ class TestSubTopicEventRouter extends SubTopicEventRouter { } describe('SubTopicEventRouter', () => { - const eventRouter = new TestSubTopicEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestSubTopicEventRouter(events); const topic = 'my-topic'; const eventPayload = { test: 'payload' }; const metadata = { 'x-my-event': 'test.type' }; it('no x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload }); expect(published).toEqual([]); }); it('with x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload, metadata }); expect(published.length).toBe(1); diff --git a/plugins/events-node/src/api/SubTopicEventRouter.ts b/plugins/events-node/src/api/SubTopicEventRouter.ts index 04abe14009..5a96ad6788 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; /** * Subscribes to the provided (generic) topic @@ -27,8 +28,11 @@ import { EventRouter } from './EventRouter'; * @public */ export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(private readonly topic: string) { - super(); + protected constructor(options: { events: EventsService; topic: string }) { + super({ + events: options.events, + topics: [options.topic], + }); } protected abstract determineSubTopic(params: EventParams): string | undefined; @@ -37,8 +41,4 @@ export abstract class SubTopicEventRouter extends EventRouter { const subTopic = this.determineSubTopic(params); return subTopic ? `${params.topic}.${subTopic}` : undefined; } - - supportsEventTopics(): string[] { - return [this.topic]; - } } diff --git a/yarn.lock b/yarn.lock index f49f9f73c5..39bfa662d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6404,7 +6404,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6417,7 +6416,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6430,7 +6428,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6445,7 +6442,6 @@ __metadata: "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@octokit/webhooks-methods": ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6459,7 +6455,6 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft From c4bd79422ab24f92bf706a79ccc1fb11446ff932 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 20:28:35 +0100 Subject: [PATCH 3/9] feat(events)!: migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/young-flies-wash.md | 35 +++++++++++ packages/backend/src/index.ts | 8 ++- packages/backend/src/plugins/events.ts | 11 +--- packages/backend/src/types.ts | 6 +- plugins/events-backend/api-report.md | 7 +-- .../src/service/EventsPlugin.test.ts | 56 +++++++++++------- .../src/service/EventsPlugin.ts | 59 ++++++------------- .../HttpPostIngressEventPublisher.test.ts | 40 ++++++------- .../http/HttpPostIngressEventPublisher.ts | 23 +++----- 9 files changed, 132 insertions(+), 113 deletions(-) create mode 100644 .changeset/young-flies-wash.md diff --git a/.changeset/young-flies-wash.md b/.changeset/young-flies-wash.md new file mode 100644 index 0000000000..dcedc9bb7c --- /dev/null +++ b/.changeset/young-flies-wash.md @@ -0,0 +1,35 @@ +--- +'@backstage/plugin-events-backend': minor +--- + +BREAKING CHANGE: Migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `HttpPostIngressEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `HttpPostIngressEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + }); + http.bind(eventsRouter); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(http) +- .start(); + + // or for other kinds of setups +- await Promise.all(http.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsPlugin` uses the `eventsServiceRef` as dependency. +Unsupported (and deprecated) extension point methods will throw an error to prevent unintended behavior. + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +``` diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bade028597..8931e6832b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -70,6 +70,7 @@ import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { DefaultEventBroker } from '@backstage/plugin-events-backend'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import { metrics } from '@opentelemetry/api'; @@ -99,7 +100,11 @@ function makeCreateEnv(config: Config) { discovery, }); - const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); + const eventsService = DefaultEventsService.create({ logger: root }); + const eventBroker = new DefaultEventBroker( + root.child({ type: 'plugin' }), + eventsService, + ); const signalService = DefaultSignalService.create({ eventBroker, }); @@ -119,6 +124,7 @@ function makeCreateEnv(config: Config) { config, reader, eventBroker, + events: eventsService, discovery, tokenManager, permissions, diff --git a/packages/backend/src/plugins/events.ts b/packages/backend/src/plugins/events.ts index f3ff354240..fd60a9bb14 100644 --- a/packages/backend/src/plugins/events.ts +++ b/packages/backend/src/plugins/events.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - EventsBackend, - HttpPostIngressEventPublisher, -} from '@backstage/plugin-events-backend'; +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -28,14 +25,10 @@ export default async function createPlugin( const http = HttpPostIngressEventPublisher.fromConfig({ config: env.config, + events: env.events, logger: env.logger, }); http.bind(eventsRouter); - await new EventsBackend(env.logger) - .setEventBroker(env.eventBroker) - .addPublishers(http) - .start(); - return eventsRouter; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index d76e68c1c9..7d9cd19310 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,7 +26,7 @@ import { import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventBroker, EventsService } from '@backstage/plugin-events-node'; import { SignalService } from '@backstage/plugin-signals-node'; export type PluginEnvironment = { @@ -40,6 +40,10 @@ export type PluginEnvironment = { permissions: PermissionEvaluator; scheduler: PluginTaskScheduler; identity: IdentityApi; + /** + * @deprecated use `events` instead + */ eventBroker: EventBroker; + events: EventsService; signalService: SignalService; }; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index 9fffe719ba..8b5d4e8d31 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -43,18 +43,17 @@ export class EventsBackend { } // @public -export class HttpPostIngressEventPublisher implements EventPublisher { +export class HttpPostIngressEventPublisher { // (undocumented) bind(router: express.Router): void; // (undocumented) static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit; }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; } ``` diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 7f555a0b6f..e39951e8dc 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -14,22 +14,27 @@ * limitations under the License. */ -import { createBackendModule } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { - TestEventBroker, - TestEventPublisher, - TestEventSubscriber, -} from '@backstage/plugin-events-backend-test-utils'; + createBackendModule, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import request from 'supertest'; import { eventsPlugin } from './EventsPlugin'; -describe('eventPlugin', () => { +describe('eventsPlugin', () => { it('should be initialized properly', async () => { - const eventBroker = new TestEventBroker(); - const publisher = new TestEventPublisher(); - const subscriber = new TestEventSubscriber('sub', ['fake']); + const eventsService = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return eventsService; + }, + }); const testModule = createBackendModule({ pluginId: 'events', @@ -40,9 +45,9 @@ describe('eventPlugin', () => { events: eventsExtensionPoint, }, async init({ events }) { - events.setEventBroker(eventBroker); - events.addPublishers(publisher); - events.addSubscribers(subscriber); + events.addHttpPostIngress({ + topic: 'fake-ext', + }); }, }); }, @@ -51,6 +56,7 @@ describe('eventPlugin', () => { const { server } = await startTestBackend({ extensionPoints: [], features: [ + eventsServiceFactory(), eventsPlugin(), testModule(), mockServices.logger.factory(), @@ -66,18 +72,24 @@ describe('eventPlugin', () => { ], }); - expect(publisher.eventBroker).toBe(eventBroker); - expect(eventBroker.subscribed.length).toEqual(1); - expect(eventBroker.subscribed[0]).toBe(subscriber); - - const response = await request(server) + const response1 = await request(server) .post('/api/events/http/fake') .timeout(1000) .send({ test: 'fake' }); - expect(response.status).toBe(202); + expect(response1.status).toBe(202); - expect(eventBroker.published.length).toEqual(1); - expect(eventBroker.published[0].topic).toEqual('fake'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' }); + const response2 = await request(server) + .post('/api/events/http/fake-ext') + .timeout(1000) + .send({ test: 'fake-ext' }); + expect(response2.status).toBe(202); + + expect(eventsService.published).toHaveLength(2); + expect(eventsService.published[0].topic).toEqual('fake'); + expect(eventsService.published[0].eventPayload).toEqual({ test: 'fake' }); + expect(eventsService.published[1].topic).toEqual('fake-ext'); + expect(eventsService.published[1].eventPayload).toEqual({ + test: 'fake-ext', + }); }); }); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 27456b5b2a..5e1df975c5 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -18,59 +18,42 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { eventsExtensionPoint, EventsExtensionPoint, } from '@backstage/plugin-events-node/alpha'; import { - EventBroker, - EventPublisher, - EventSubscriber, + eventsServiceRef, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { DefaultEventBroker } from './DefaultEventBroker'; import Router from 'express-promise-router'; import { HttpPostIngressEventPublisher } from './http'; class EventsExtensionPointImpl implements EventsExtensionPoint { - #eventBroker: EventBroker | undefined; #httpPostIngresses: HttpPostIngressOptions[] = []; - #publishers: EventPublisher[] = []; - #subscribers: EventSubscriber[] = []; - setEventBroker(eventBroker: EventBroker): void { - this.#eventBroker = eventBroker; + setEventBroker(_: any): void { + throw new Error( + 'setEventBroker is not supported anymore; use eventsServiceRef instead', + ); } - addPublishers( - ...publishers: Array> - ): void { - this.#publishers.push(...publishers.flat()); + addPublishers(_: any): void { + throw new Error( + 'addPublishers is not supported anymore; use EventsService instead', + ); } - addSubscribers( - ...subscribers: Array> - ): void { - this.#subscribers.push(...subscribers.flat()); + addSubscribers(_: any): void { + throw new Error( + 'addSubscribers is not supported anymore; use EventsService instead', + ); } addHttpPostIngress(options: HttpPostIngressOptions) { this.#httpPostIngresses.push(options); } - get eventBroker() { - return this.#eventBroker; - } - - get publishers() { - return this.#publishers; - } - - get subscribers() { - return this.#subscribers; - } - get httpPostIngresses() { return this.#httpPostIngresses; } @@ -90,12 +73,11 @@ export const eventsPlugin = createBackendPlugin({ env.registerInit({ deps: { config: coreServices.rootConfig, + events: eventsServiceRef, logger: coreServices.logger, router: coreServices.httpRouter, }, - async init({ config, logger, router }) { - const winstonLogger = loggerToWinstonLogger(logger); - + async init({ config, events, logger, router }) { const ingresses = Object.fromEntries( extensionPoint.httpPostIngresses.map(ingress => [ ingress.topic, @@ -105,20 +87,13 @@ export const eventsPlugin = createBackendPlugin({ const http = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses, - logger: winstonLogger, + logger, }); const eventsRouter = Router(); http.bind(eventsRouter); router.use(eventsRouter); - - const eventBroker = - extensionPoint.eventBroker ?? new DefaultEventBroker(winstonLogger); - - eventBroker.subscribe(extensionPoint.subscribers); - [extensionPoint.publishers, http] - .flat() - .forEach(publisher => publisher.setEventBroker(eventBroker)); }, }); }, diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 72e0b94c57..665ac0b4a9 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -16,7 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import express from 'express'; import Router from 'express-promise-router'; import request from 'supertest'; @@ -36,9 +36,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: {}, }, @@ -46,9 +48,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const notFoundResponse = await request(app) .post('/http/unknown') .timeout(1000) @@ -69,18 +68,18 @@ describe('HttpPostIngressEventPublisher', () => { .send({ testB: 'data' }); expect(response2.status).toBe(202); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' }); - expect(eventBroker.published[0].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ testA: 'data' }); + expect(events.published[0].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }), ); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ testB: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', @@ -99,9 +98,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: { validator: async (req, context) => { @@ -146,9 +147,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const response1 = await request(app) .post('/http/testA') .timeout(1000) @@ -191,12 +189,12 @@ describe('HttpPostIngressEventPublisher', () => { expect(response6.status).toBe(403); expect(response6.body).toEqual({}); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'x-test-signature': 'testB-signature', }), @@ -205,10 +203,12 @@ describe('HttpPostIngressEventPublisher', () => { it('without configuration', async () => { const config = new ConfigReader({}); + const events = new TestEventsService(); expect(() => HttpPostIngressEventPublisher.fromConfig({ config, + events, logger, }), ).not.toThrow(); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index b5a6ccbca1..06dc4e463a 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -15,16 +15,15 @@ */ import { errorHandler } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { - EventBroker, - EventPublisher, + EventsService, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { RequestValidationContextImpl } from './validation'; /** @@ -34,13 +33,12 @@ import { RequestValidationContextImpl } from './validation'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class HttpPostIngressEventPublisher implements EventPublisher { - private eventBroker?: EventBroker; - +export class HttpPostIngressEventPublisher { static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher { const topics = env.config.getOptionalStringArray('events.http.topics') ?? []; @@ -54,11 +52,12 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } }); - return new HttpPostIngressEventPublisher(env.logger, ingresses); + return new HttpPostIngressEventPublisher(env.events, env.logger, ingresses); } private constructor( - private readonly logger: Logger, + private readonly events: EventsService, + private readonly logger: LoggerService, private readonly ingresses: { [topic: string]: Omit; }, @@ -68,10 +67,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher { router.use('/http', this.createRouter(this.ingresses)); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - private createRouter(ingresses: { [topic: string]: Omit; }): express.Router { @@ -108,7 +103,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } const eventPayload = request.body; - await this.eventBroker!.publish({ + await this.events.publish({ topic, eventPayload, metadata: request.headers, From 8f6afa94a9dc80ae11d5c1acd3ad70e67c9134f1 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 20:40:22 +0100 Subject: [PATCH 4/9] chore(events): migrate `DemoEventBasedEntityProvider` to use `EventsService` Signed-off-by: Patrick Jungermann --- .../plugins/DemoEventBasedEntityProvider.ts | 42 +++++++++---------- packages/backend/src/plugins/catalog.ts | 3 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 7a032198a3..11d073400a 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -18,40 +18,36 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; -import { - EventBroker, - EventParams, - EventSubscriber, -} from '@backstage/plugin-events-node'; +import { EventParams, EventsService } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; -export class DemoEventBasedEntityProvider - implements EntityProvider, EventSubscriber -{ +export class DemoEventBasedEntityProvider implements EntityProvider { private readonly logger: Logger; + private readonly events: EventsService; private readonly topics: string[]; constructor(opts: { - eventBroker: EventBroker; + events: EventsService; logger: Logger; topics: string[]; }) { - const { eventBroker, logger, topics } = opts; - this.logger = logger; - this.topics = topics; - eventBroker.subscribe(this); + this.events = opts.events; + this.logger = opts.logger; + this.topics = opts.topics; } - async onEvent(params: EventParams): Promise { - this.logger.info( - `onEvent: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - } - - supportsEventTopics(): string[] { - return this.topics; + async subscribe() { + await this.events.subscribe({ + id: 'DemoEventBasedEntityProvider', + topics: this.topics, + onEvent: async (params: EventParams): Promise => { + this.logger.info( + `onEvent: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + }, + }); } async connect(_: EntityProviderConnection): Promise { diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 223acab818..00fe7ff4a0 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -28,10 +28,11 @@ export default async function createPlugin( builder.addProcessor(new ScaffolderEntitiesProcessor()); const demoProvider = new DemoEventBasedEntityProvider({ + events: env.events, logger: env.logger, topics: ['example'], - eventBroker: env.eventBroker, }); + await demoProvider.subscribe(); builder.addEntityProvider(demoProvider); const { processingEngine, router } = await builder.build(); From 132d672747d688d81c5eab76b2241712bf697b30 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 21:01:42 +0100 Subject: [PATCH 5/9] feat(events)!: migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/long-emus-talk.md | 32 +++++++++++++++++++ .../api-report.md | 12 +++---- .../package.json | 3 +- .../AwsSqsConsumingEventPublisher.test.ts | 30 +++++++++-------- .../AwsSqsConsumingEventPublisher.ts | 29 +++++++++-------- ...oduleAwsSqsConsumingEventPublisher.test.ts | 28 +++++++--------- ...entsModuleAwsSqsConsumingEventPublisher.ts | 15 ++++----- yarn.lock | 1 - 8 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 .changeset/long-emus-talk.md diff --git a/.changeset/long-emus-talk.md b/.changeset/long-emus-talk.md new file mode 100644 index 0000000000..ac8d5cbc68 --- /dev/null +++ b/.changeset/long-emus-talk.md @@ -0,0 +1,32 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': minor +--- + +BREAKING CHANGE: Migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `AwsSqsConsumingEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `AwsSqsConsumingEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); ++ await Promise.all(sqs.map(publisher => publisher.start())); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(sqs) +- .start(); + + // or for other kinds of setups +- await Promise.all(sqs.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsModuleAwsSqsConsumingEventPublisher` uses the `eventsServiceRef` as dependency, +instead of `eventsExtensionPoint`. diff --git a/plugins/events-backend-module-aws-sqs/api-report.md b/plugins/events-backend-module-aws-sqs/api-report.md index ffa9c888d6..ff863e4d8d 100644 --- a/plugins/events-backend-module-aws-sqs/api-report.md +++ b/plugins/events-backend-module-aws-sqs/api-report.md @@ -4,20 +4,20 @@ ```ts import { Config } from '@backstage/config'; -import { EventBroker } from '@backstage/plugin-events-node'; -import { EventPublisher } 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'; // @public -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { // (undocumented) static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[]; // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; + start(): Promise; } ``` diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 0b70eb57bf..41f91d804e 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -48,8 +48,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", - "luxon": "^3.0.0", - "winston": "^3.2.1" + "luxon": "^3.0.0" }, "devDependencies": { "@aws-sdk/types": "^3.347.0", diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts index e32245b483..60930fdcf7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts @@ -22,7 +22,7 @@ import { import { getVoidLogger } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { mockClient } from 'aws-sdk-client-mock'; import { AwsSqsConsumingEventPublisher } from './AwsSqsConsumingEventPublisher'; @@ -53,12 +53,14 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); @@ -85,21 +87,21 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( @@ -133,6 +135,7 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); let taskFn: (() => Promise) | undefined = undefined; const scheduler = { scheduleTask: (spec: { fn: () => Promise }) => { @@ -196,32 +199,31 @@ describe('AwsSqsConsumingEventPublisher', () => { const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); await taskFn!(); await taskFn!(); await taskFn!(); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('fake1'); - expect(eventBroker.published[0].eventPayload).toEqual({ + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('fake1'); + expect(events.published[0].eventPayload).toEqual({ event: 'payload1', }); - expect(eventBroker.published[0].metadata).toEqual({ + expect(events.published[0].metadata).toEqual({ 'X-Custom-Attr': 'value', }); - expect(eventBroker.published[1].topic).toEqual('fake1'); - expect(eventBroker.published[1].eventPayload).toEqual({ + expect(events.published[1].topic).toEqual('fake1'); + expect(events.published[1].eventPayload).toEqual({ event: 'payload2', }); - expect(eventBroker.published[1].metadata).toEqual({}); + expect(events.published[1].metadata).toEqual({}); }); }); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index a2f6a00fe7..26eba88c18 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -21,10 +21,10 @@ import { ReceiveMessageCommandInput, SQSClient, } from '@aws-sdk/client-sqs'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; import { AwsSqsEventSourceConfig, readConfig } from './config'; /** @@ -34,28 +34,34 @@ import { AwsSqsEventSourceConfig, readConfig } from './config'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { private readonly topic: string; private readonly receiveParams: ReceiveMessageCommandInput; private readonly sqs: SQSClient; private readonly queueUrl: string; private readonly taskTimeoutSeconds: number; private readonly waitTimeAfterEmptyReceiveMs; - private eventBroker?: EventBroker; static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[] { return readConfig(env.config).map( config => - new AwsSqsConsumingEventPublisher(env.logger, env.scheduler, config), + new AwsSqsConsumingEventPublisher( + env.logger, + env.events, + env.scheduler, + config, + ), ); } private constructor( - private readonly logger: Logger, + private readonly logger: LoggerService, + private readonly events: EventsService, private readonly scheduler: PluginTaskScheduler, config: AwsSqsEventSourceConfig, ) { @@ -80,12 +86,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { config.waitTimeAfterEmptyReceive.as('milliseconds'); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - return this.start(); - } - - private async start(): Promise { + async start(): Promise { const id = `events.awsSqs.publisher:${this.topic}`; const logger = this.logger.child({ class: AwsSqsConsumingEventPublisher.prototype.constructor.name, @@ -172,7 +173,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { } }); - this.eventBroker!.publish({ + this.events.publish({ topic: this.topic, eventPayload, metadata, diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts index 57e1831667..cbf29f96cd 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts @@ -14,26 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleAwsSqsConsumingEventPublisher } from './eventsModuleAwsSqsConsumingEventPublisher'; -import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; describe('eventsModuleAwsSqsConsumingEventPublisher', () => { it('should be correctly wired and set up', async () => { - let addedPublishers: AwsSqsConsumingEventPublisher[] | undefined; - const extensionPoint = { - addPublishers: (publishers: any) => { - addedPublishers = publishers; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - }; + }); const scheduler = mockServices.scheduler.mock(); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], features: [ + eventsServiceFactory(), eventsModuleAwsSqsConsumingEventPublisher(), mockServices.rootConfig.factory({ data: { @@ -65,14 +67,6 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { ], }); - expect(addedPublishers).not.toBeUndefined(); - expect(addedPublishers!.length).toEqual(2); - - const eventBroker = new TestEventBroker(); - await Promise.all( - addedPublishers!.map(publisher => publisher.setEventBroker(eventBroker)), - ); - // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( expect.objectContaining({ id: 'events.awsSqs.publisher:fake1' }), diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts index eabab94708..ea0f094bee 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts @@ -18,8 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; /** @@ -34,19 +33,19 @@ export const eventsModuleAwsSqsConsumingEventPublisher = createBackendModule({ env.registerInit({ deps: { config: coreServices.rootConfig, - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, }, async init({ config, events, logger, scheduler }) { - const winstonLogger = loggerToWinstonLogger(logger); const sqs = AwsSqsConsumingEventPublisher.fromConfig({ - config: config, - logger: winstonLogger, - scheduler: scheduler, + config, + events, + logger, + scheduler, }); - events.addPublishers(sqs); + await Promise.all(sqs.map(publisher => publisher.start())); }, }); }, diff --git a/yarn.lock b/yarn.lock index 39bfa662d7..a876413ec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6391,7 +6391,6 @@ __metadata: "@backstage/types": "workspace:^" aws-sdk-client-mock: ^3.0.0 luxon: ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft From 52479092dc8efcd3057218bb2398ab5f81d0b09c Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 21:09:52 +0100 Subject: [PATCH 6/9] feat(events): add `events: EventsService` to `LegacyPluginEnvironment` Signed-off-by: Patrick Jungermann --- .changeset/six-nails-hammer.md | 5 +++++ packages/backend-dynamic-feature-service/api-report.md | 2 ++ .../backend-dynamic-feature-service/src/manager/types.ts | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/six-nails-hammer.md diff --git a/.changeset/six-nails-hammer.md b/.changeset/six-nails-hammer.md new file mode 100644 index 0000000000..d0cf7a0ea7 --- /dev/null +++ b/.changeset/six-nails-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +--- + +Add `events: EventsService` to `LegacyPluginEnvironment`. diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 64bffb468c..8075acabfb 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -10,6 +10,7 @@ import { Config } from '@backstage/config'; import { ConfigSchema } from '@backstage/config-loader'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; +import { EventsService } from '@backstage/plugin-events-node'; import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -215,6 +216,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index 0038470686..60b2eb831e 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -29,6 +29,7 @@ import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { EventBroker, + EventsService, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; @@ -64,6 +65,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; From 3c5f58622e866ba63f6de855476dacd1a831a36d Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 22:36:52 +0100 Subject: [PATCH 7/9] docs(events): update docs about events-backend used with the new backend system Signed-off-by: Patrick Jungermann --- .../building-backends/08-migrating.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 9eada30ef6..27e4eb7324 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -632,7 +632,7 @@ A basic installation of the events plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); ``` If you have other customizations made to `plugins/events.ts`, such as adding @@ -646,6 +646,7 @@ depends on the appropriate extension point and interacts with it. ```ts title="packages/backend/src/index.ts" /* highlight-add-start */ +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { createBackendModule } from '@backstage/backend-plugin-api'; /* highlight-add-end */ @@ -663,7 +664,28 @@ const eventsModuleCustomExtensions = createBackendModule({ async init({ events /* ..., other dependencies */ }) { // Here you have the opportunity to interact with the extension // point before the plugin itself gets instantiated - events.addSubscribers(new MySubscriber()); // just an example + events.addHttpPostIngress({ + // ... + }); + }, + }); + }, +}); +/* highlight-add-end */ + +/* highlight-add-start */ +const otherPluginModuleCustomExtensions = createBackendModule({ + pluginId: 'other-plugin', // name of the plugin that the module is targeting + moduleId: 'custom-extensions', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + // ... and other dependencies as needed + }, + async init({ events /* ..., other dependencies */ }) { + // Here you have the opportunity to interact with the extension + // point before the plugin itself gets instantiated }, }); }, @@ -671,17 +693,11 @@ const eventsModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); /* highlight-add-next-line */ backend.add(eventsModuleCustomExtensions()); -``` - -This also requires that you have a dependency on the corresponding node package, -if you didn't already have one. - -```bash -# from the repository root -yarn --cwd packages/backend add @backstage/plugin-events-node +/* highlight-add-next-line */ +backend.add(otherPluginModuleCustomExtensions()); ``` Here we've placed the module directly in the backend index file just to get From 1ab76e523de915c497277ed8d7ebdf03aad09579 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Wed, 24 Jan 2024 03:06:54 +0100 Subject: [PATCH 8/9] docs(events): describe the new events setup, update README.md files Signed-off-by: Patrick Jungermann --- .../events-backend-module-aws-sqs/README.md | 48 ++- plugins/events-backend-module-azure/README.md | 32 +- .../README.md | 32 +- .../events-backend-module-gerrit/README.md | 28 +- .../events-backend-module-github/README.md | 52 ++-- .../events-backend-module-gitlab/README.md | 52 ++-- plugins/events-backend-test-utils/README.md | 7 +- plugins/events-backend/README.md | 277 +++++------------- plugins/events-node/README.md | 83 +++++- 9 files changed, 295 insertions(+), 316 deletions(-) diff --git a/plugins/events-backend-module-aws-sqs/README.md b/plugins/events-backend-module-aws-sqs/README.md index 7ca4bb280d..bd8e0c51d3 100644 --- a/plugins/events-backend-module-aws-sqs/README.md +++ b/plugins/events-backend-module-aws-sqs/README.md @@ -1,12 +1,12 @@ -# events-backend-module-aws-sqs +# `@backstage/plugins-events-backend-module-aws-sqs` -Welcome to the `events-backend-module-aws-sqs` backend plugin! +Welcome to the `events-backend-module-aws-sqs` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AwsSqsConsumingEventPublisher`. +This package is a module for the `events-backend` backend plugin +and extends the events system with an `AwsSqsConsumingEventPublisher`. -This event publisher will allow you to receive events from -an AWS SQS queue and will publish these to the used event broker. +This event publisher will allow you to receive events from an AWS SQS queue +and will publish these to the used `EventsService` implementation. ## Configuration @@ -32,15 +32,43 @@ events: ## Installation -1. Install the [`events-backend` plugin](../events-backend/README.md). -2. Install this module -3. Add your configuration. +1. Install this module +2. Add your configuration. ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-aws-sqs ``` -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); ``` + +### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +// ... +import { AwsSqsConsumingEventPublisher } from '@backstage/plugin-events-backend-module-aws-sqs'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const eventsRouter = Router(); + + // ... + + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); + await Promise.all(sqs.map(publisher => publisher.start())); + + return eventsRouter; +} +``` diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md index 61b3b63175..7c3920d499 100644 --- a/plugins/events-backend-module-azure/README.md +++ b/plugins/events-backend-module-azure/README.md @@ -1,9 +1,9 @@ # events-backend-module-azure -Welcome to the `events-backend-module-azure` backend plugin! +Welcome to the `events-backend-module-azure` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AzureDevOpsEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `AzureDevOpsEventRouter`. The event router will subscribe to the topic `azureDevOps` and route the events to more concrete topics based on the value @@ -22,30 +22,22 @@ and [webhooks](https://learn.microsoft.com/en-us/azure/devops/service-hooks/serv ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-azure ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-azure/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const azureEventRouter = new AzureDevOpsEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(azureEventRouter) -+ .addSubscribers(azureEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new AzureDevOpsEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/README.md b/plugins/events-backend-module-bitbucket-cloud/README.md index 0a40ab2eea..7ff743c06d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/README.md +++ b/plugins/events-backend-module-bitbucket-cloud/README.md @@ -1,9 +1,9 @@ # events-backend-module-bitbucket-cloud -Welcome to the `events-backend-module-bitbucket-cloud` backend plugin! +Welcome to the `events-backend-module-bitbucket-cloud` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `BitbucketCloudEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `BitbucketCloudEventRouter`. The event router will subscribe to the topic `bitbucketCloud` and route the events to more concrete topics based on the value @@ -22,32 +22,24 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-bitbucket-cloud ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add( import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'), ); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const bitbucketCloudEventRouter = new BitbucketCloudEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(bitbucketCloudEventRouter) -+ .addSubscribers(bitbucketCloudEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new BitbucketCloudEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-gerrit/README.md b/plugins/events-backend-module-gerrit/README.md index b658fba366..d5b9f9683a 100644 --- a/plugins/events-backend-module-gerrit/README.md +++ b/plugins/events-backend-module-gerrit/README.md @@ -1,8 +1,8 @@ # events-backend-module-gerrit -Welcome to the `events-backend-module-gerrit` backend plugin! +Welcome to the `events-backend-module-gerrit` backend module! -This plugin is a module for the `events-backend` backend plugin +This package is a module for the `events-backend` backend plugin and extends it with an `GerritEventRouter`. The event router will subscribe to the topic `gerrit` @@ -21,30 +21,20 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gerrit ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-gerrit/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const gerritEventRouter = new GerritEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(gerritEventRouter) -+ .addSubscribers(gerritEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GerritEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-github/README.md b/plugins/events-backend-module-github/README.md index 072877ee86..fec27b62e3 100644 --- a/plugins/events-backend-module-github/README.md +++ b/plugins/events-backend-module-github/README.md @@ -1,9 +1,9 @@ # events-backend-module-github -Welcome to the `events-backend-module-github` backend plugin! +Welcome to the `events-backend-module-github` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GithubEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GithubEventRouter`. The event router will subscribe to the topic `github` and route the events to more concrete topics based on the value @@ -22,37 +22,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-github ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const githubEventRouter = new GithubEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubEventRouter } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(githubEventRouter) -+ .addSubscribers(githubEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GithubEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Signature Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubWebhook } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubWebhook()); +``` + +#### Legacy Backend System + Add the signature validator for the topic `github`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGithubSignatureValidator } from '@backstage/plugin-events-backend-module-github'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + github: { + validator: createGithubSignatureValidator(env.config), + }, @@ -61,7 +73,7 @@ Add the signature validator for the topic `github`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-module-gitlab/README.md b/plugins/events-backend-module-gitlab/README.md index 3d4919302f..73d5bf2d87 100644 --- a/plugins/events-backend-module-gitlab/README.md +++ b/plugins/events-backend-module-gitlab/README.md @@ -1,9 +1,9 @@ # events-backend-module-gitlab -Welcome to the `events-backend-module-gitlab` backend plugin! +Welcome to the `events-backend-module-gitlab` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GitlabEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GitlabEventRouter`. The event router will subscribe to the topic `gitlab` and route the events to more concrete topics based on the value @@ -21,37 +21,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gitlab ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const gitlabEventRouter = new GitlabEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabEventRouter } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(gitlabEventRouter) -+ .addSubscribers(gitlabEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GitlabEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Token Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabWebhook } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabWebhook()); +``` + +#### Legacy Backend System + Add the token validator for the topic `gitlab`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGitlabTokenValidator } from '@backstage/plugin-events-backend-module-gitlab'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + gitlab: { + validator: createGitlabTokenValidator(env.config), + }, @@ -60,7 +72,7 @@ Add the token validator for the topic `gitlab`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-test-utils/README.md b/plugins/events-backend-test-utils/README.md index c8727b536b..84a1be754e 100644 --- a/plugins/events-backend-test-utils/README.md +++ b/plugins/events-backend-test-utils/README.md @@ -1,4 +1,5 @@ -# plugin-events-backend-test-utils +# `@backstage/plugin-events-backend-test-utils` -Houses implementations of plugin-events-node interfaces -which can be useful for test for events-backend and its modules. +This is a package that can be used as `devDependency` +and provides a test implementation for the `EventsService` +by [`events-node` package](../events-node/README.md): `TestEventsService`. diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index b8470101aa..2dba259986 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -1,166 +1,55 @@ -# events-backend +# `@backstage/plugin-events-backend` Welcome to the events-backend backend plugin! -This plugin provides the wiring of all extension points -for managing events as defined by [plugin-events-node](../events-node) -including backend plugin `EventsPlugin` and `EventsBackend`. - -Additionally, it uses a simple in-process implementation for -the `EventBroker` by default which you can replace with a more sophisticated -implementation of your choice as you need (e.g., via module). - -Some of these (non-exhaustive) may provide added persistence, -or use external systems like AWS EventBridge, AWS SNS, Kafka, etc. +This package is based on [events-node](../events-node) and its `eventsServiceRef` +that is at the core of the event support. +It provides an `eventsPlugin` (exported as `default`). By default, the plugin ships with support to receive events via HTTP endpoints -`POST /api/events/http/{topic}` and will publish these -to the used event broker. +`POST /api/events/http/{topic}` and will publish these to the `EventsService`. + +HTTP ingresses can be enabled by config, or using the extension point +of the `eventsPlugin`. +Additionally, the latter allows to add a request validator +(e.g., signature verification). ## Installation ```bash # From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-events-backend @backstage/plugin-events-node +yarn --cwd packages/backend add @backstage/plugin-events-backend ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -#### Event Broker +```ts +// packages/backend/src/plugins/events.ts +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; -First you will need to add and implementation of the `EventBroker` interface to the backend plugin environment. -This will allow event broker instance any backend plugins to publish and subscribe to events in order to communicate -between them. - -Add the following to `makeCreateEnv` - -```diff -// packages/backend/src/index.ts -+ import { DefaultEventBroker } from '@backstage/plugin-events-backend'; -+ const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); -``` - -Then update plugin environment to include the event broker. - -```diff -// packages/backend/src/types.ts -+ import { EventBroker } from '@backstage/plugin-events-node'; -+ eventBroker: EventBroker; -``` - -#### Publishing and Subscribing to events with the broker - -Backend plugins are passed the event broker in the plugin environment at startup of the application. The plugin can -make use of this to communicate between parts of the application. - -Here is an example of a plugin publishing a payload to a topic. - -```typescript jsx export default async function createPlugin( env: PluginEnvironment, ): Promise { - env.eventBroker.publish({ - topic: 'publish.example', - eventPayload: { message: 'Hello, World!' }, - metadata: {}, + const eventsRouter = Router(); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, }); + http.bind(eventsRouter); + + return eventsRouter; } ``` -Here is an example of a plugin subscribing to a topic. - -```typescript jsx -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - env.eventBroker.subscribe([ - { - supportsEventTopics: ['publish.example'], - onEvent: async (params: EventParams) => { - env.logger.info(`receieved ${params.topic} event`); - }, - }, - ]); -} -``` - -#### Implementing an `EventSubscriber` class - -More complex solutions might need the creation of a class that implements the `EventSubscriber` interface. e.g. - -```typescript jsx -import { EventSubscriber } from './EventSubscriber'; - -class ExampleSubscriber implements EventSubscriber { - // ... - - supportsEventTopics() { - return ['publish.example']; - } - - async onEvent(params: EventParams) { - env.logger.info(`receieved ${params.topic} event`); - } -} -``` - -#### Events Backend - -The events backend plugin provides a router to handler http events and publish the http requests onto the event -broker. - -To configure it add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) -to your Backstage project. - -Additionally, add the events plugin to your backend. - -```diff -// packages/backend/src/index.ts -// [...] -+import events from './plugins/events'; -// [...] -+ const eventsEnv = useHotMemoize(module, () => createEnv('events')); -// [...] -+ apiRouter.use('/events', await events(eventsEnv)); -// [...] -``` - -#### Configuration - -In order to create HTTP endpoints to receive events for a certain -topic, you need to add them at your configuration: - -```yaml -events: - http: - topics: - - bitbucketCloud - - github - - whatever -``` - -Only those topics added to the configuration will result in -available endpoints. - -The example above would result in the following endpoints: - -``` -POST /api/events/http/bitbucketCloud -POST /api/events/http/github -POST /api/events/http/whatever -``` - -You may want to use these for webhooks by SCM providers -in combination with suitable event subscribers. - -However, it is not limited to these use cases. - ### Event-based Entity Providers You can implement the `EventSubscriber` interface on an `EntityProviders` to allow it to handle events from other plugins e.g. the event backend plugin @@ -189,74 +78,42 @@ Assuming you have configured the `eventBroker` into the `PluginEnvironment` you } ``` +## Configuration + +In order to create HTTP endpoints to receive events for a certain +topic, you need to add them at your configuration: + +```yaml +events: + http: + topics: + - bitbucketCloud + - github + - whatever +``` + +Only those topics added to the configuration will result in +available endpoints. + +The example above would result in the following endpoints: + +``` +POST /api/events/http/bitbucketCloud +POST /api/events/http/github +POST /api/events/http/whatever +``` + +You may want to use these for webhooks by SCM providers +in combination with suitable event subscribers. + +However, it is not limited to these use cases. + ## Use Cases -### Custom Event Broker - -Example using the `EventsBackend`: - -```ts -new EventsBackend(env.logger) - .setEventBroker(yourEventBroker) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; - -// [...] - -export const yourModuleEventsModule = createBackendModule({ - pluginId: 'events', - moduleId: 'your-module', - register(env) { - // [...] - env.registerInit({ - deps: { - // [...] - events: eventsExtensionPoint, - // [...] - }, - async init({ /* ... */ events /*, ... */ }) { - // [...] - const yourEventBroker = new YourEventBroker(); - // [...] - events.setEventBroker(yourEventBroker); - }, - }); - }, -}); -``` - ### Request Validator -Example using the `EventsBackend`: - ```ts -const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { - yourTopic: { - validator: yourValidator, - }, - }, - logger: env.logger, -}); -http.bind(router); - -await new EventsBackend(env.logger) - .addPublishers(http) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; // [...] @@ -282,3 +139,19 @@ export const eventsModuleYourFeature = createBackendModule({ }, }); ``` + +#### Legacy Backend System + +```ts +const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + yourTopic: { + validator: yourValidator, + }, + }, + logger: env.logger, +}); +http.bind(router); +``` diff --git a/plugins/events-node/README.md b/plugins/events-node/README.md index 44738222bc..ce4d705025 100644 --- a/plugins/events-node/README.md +++ b/plugins/events-node/README.md @@ -1,3 +1,82 @@ -# plugin-events-node +# `@backstage/plugin-events-node` -Houses types and utilities for building events-related modules. +This package defined basic types for event-based interactions inside of Backstage. + +Additionally, it provides the core event service `eventsServiceRef` of type `EventsService` +with its default implementation that uses the `DefaultEventsService` implementation. + +`DefaultEventsService` is a simple in-memory implementation +that requires the co-deployment of producers and consumers of events. + +## Installation + +Add `@backstage/plugin-events-node` as dependency to your plugin or plugin module package +to which you want to add event support. + +Use `eventsServiceRef` as a dependency at your plugin or plugin module. + +### Legacy Backend System + +Create an `EventsService` instance and add it to the environment. + +```ts +// packages/backend/src/plugins/events.ts +import { DefaultEventsService } from '@backstage/plugin-events-node'; + +// ... + +function makeCreateEnv(config: Config) { + // ... + const eventsService = DefaultEventsService.create({ logger: root }); + // ... + return (plugin: string): PluginEnvironment => { + // ... + return { + // ... + events: eventsService, + // ... + }; + }; +} +``` + +Use the `events` from the `PluginEnvironment` as desired: + +```ts +// packages/backend/src/plugins/events.ts +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // ... + env.events; // ... + // ... +} +``` + +## Use Case + +### Exchange service implementation + +Create your custom service factory implementation: + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +// ... +export const customEventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: { + // add needed dependencies here + }, + async factory({ logger }) { + // add your custom logic here + return customEventsService; + }, +}); +``` + +and your custom implementation: + +```diff +// packages/backend/src/index.ts ++ backend.add(customEventsServiceFactory()); +``` From 9e527c920065fc015f2b6a045079545d2e6961c1 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 22:44:32 +0100 Subject: [PATCH 9/9] fix(events,catalog,bitbucket-cloud)!: fix new backend system support; migrates to `EventsService` - Fixes the support for the new backend system that was broken entirely (with and without events support). - Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`. Signed-off-by: Patrick Jungermann --- .changeset/silver-flowers-trade.md | 40 +++++++++++++++ docs/integrations/bitbucketCloud/discovery.md | 33 ++++++++++++- .../api-report.md | 18 +++---- .../package.json | 4 +- ...ModuleBitbucketCloudEntityProvider.test.ts | 32 +++++++----- ...talogModuleBitbucketCloudEntityProvider.ts | 12 ++--- .../BitbucketCloudEntityProvider.test.ts | 24 ++++----- .../providers/BitbucketCloudEntityProvider.ts | 49 ++++++++++--------- yarn.lock | 2 +- 9 files changed, 141 insertions(+), 73 deletions(-) create mode 100644 .changeset/silver-flowers-trade.md 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