Merge pull request #22344 from Bonial-International-GmbH/pjungermann/new-backend/events-service

new backend system: events service
This commit is contained in:
Patrik Oldsberg
2024-02-26 14:32:44 +01:00
committed by GitHub
103 changed files with 1742 additions and 946 deletions
+39
View File
@@ -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.
+76
View File
@@ -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;
- }
}
```
+32
View File
@@ -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`.
+40
View File
@@ -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'),
);
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Add `events: EventsService` to `LegacyPluginEnvironment`.
+35
View File
@@ -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';
```
@@ -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
+31 -2
View File
@@ -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:
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md>
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md>
### 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,
@@ -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;
};
@@ -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;
};
+7 -1
View File
@@ -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,
@@ -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<void> {
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<void> => {
this.logger.info(
`onEvent: topic=${params.topic}, metadata=${JSON.stringify(
params.metadata,
)}, payload=${JSON.stringify(params.eventPayload)}`,
);
},
});
}
async connect(_: EntityProviderConnection): Promise<void> {
+2 -1
View File
@@ -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();
+2 -9
View File
@@ -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;
}
+5 -1
View File
@@ -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;
};
@@ -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<void>;
// (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<void>;
// (undocumented)
onRepoPush(event: Events.RepoPushEvent): Promise<void>;
// (undocumented)
refresh(logger: Logger): Promise<void>;
// (undocumented)
supportsEventTopics(): string[];
refresh(logger: LoggerService): Promise<void>;
}
```
@@ -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"
},
@@ -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<BitbucketCloudEntityProvider> | undefined;
let addedSubscribers: Array<BitbucketCloudEntityProvider> | 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);
});
});
@@ -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);
},
});
},
@@ -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<string, string> },
@@ -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,
@@ -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<void>;
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<void> {
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<void> {
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;
+38 -10
View File
@@ -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<Router> {
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;
}
```
@@ -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<void>;
start(): Promise<void>;
}
```
@@ -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",
@@ -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<void>) | undefined = undefined;
const scheduler = {
scheduleTask: (spec: { fn: () => Promise<void> }) => {
@@ -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({});
});
});
@@ -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<void> {
this.eventBroker = eventBroker;
return this.start();
}
private async start(): Promise<void> {
async start(): Promise<void> {
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,
@@ -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' }),
@@ -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()));
},
});
},
+12 -20
View File
@@ -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();
```
@@ -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;
}
```
@@ -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:^",
@@ -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);
});
});
@@ -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 {
@@ -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');
});
});
@@ -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();
},
});
},
@@ -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();
```
@@ -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;
}
```
@@ -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:^",
@@ -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);
});
});
@@ -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 {
@@ -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');
});
});
@@ -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();
},
});
},
+9 -19
View File
@@ -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();
```
@@ -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;
}
```
@@ -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:^",
@@ -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);
});
});
@@ -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 {
@@ -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');
});
});
@@ -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();
},
});
},
+32 -20
View File
@@ -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:
@@ -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;
}
```
@@ -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:^",
@@ -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);
});
});
@@ -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 {
@@ -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');
});
});
@@ -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();
},
});
},
+32 -20
View File
@@ -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:
@@ -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;
}
```
@@ -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:^",
@@ -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);
});
});
@@ -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 {
@@ -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');
});
});
@@ -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();
},
});
},
+4 -3
View File
@@ -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`.
@@ -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<void>;
@@ -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<void>;
// (undocumented)
get published(): EventParams[];
// (undocumented)
reset(): void;
// (undocumented)
subscribe(options: EventsServiceSubscribeOptions): Promise<void>;
// (undocumented)
get subscribed(): EventsServiceSubscribeOptions[];
}
// @public @deprecated (undocumented)
export class TestEventSubscriber implements EventSubscriber {
constructor(name: string, topics: string[]);
// (undocumented)
@@ -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';
@@ -20,4 +20,5 @@
* @packageDocumentation
*/
export * from './deprecated';
export * from './testUtils';
@@ -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[] = [];
@@ -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;
@@ -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[];
@@ -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<void> {
this.#published.push(params);
}
async subscribe(options: EventsServiceSubscribeOptions): Promise<void> {
this.#subscribed.push(options);
}
get published(): EventParams[] {
return this.#published;
}
get subscribed(): EventsServiceSubscribeOptions[] {
return this.#subscribed;
}
reset(): void {
this.#published = [];
this.#subscribed = [];
}
}
@@ -14,6 +14,4 @@
* limitations under the License.
*/
export { TestEventBroker } from './TestEventBroker';
export { TestEventPublisher } from './TestEventPublisher';
export { TestEventSubscriber } from './TestEventSubscriber';
export { TestEventsService } from './TestEventsService';
+75 -202
View File
@@ -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<Router> {
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<Router> {
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);
```
+9 -7
View File
@@ -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<void>;
// (undocumented)
@@ -23,7 +26,7 @@ export class DefaultEventBroker implements EventBroker {
): void;
}
// @public
// @public @deprecated
export class EventsBackend {
constructor(logger: Logger);
// (undocumented)
@@ -40,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<HttpPostIngressOptions, 'topic'>;
};
logger: Logger;
logger: LoggerService;
}): HttpPostIngressEventPublisher;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
```
+18
View File
@@ -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';
+1 -2
View File
@@ -20,6 +20,5 @@
* @packageDocumentation
*/
export { EventsBackend } from './service/EventsBackend';
export * from './deprecated';
export { HttpPostIngressEventPublisher } from './service/http';
export { DefaultEventBroker } from './service/DefaultEventBroker';
@@ -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'),
);
});
@@ -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<void> {
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<EventSubscriber | Array<EventSubscriber>>
): 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),
});
});
}
@@ -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;
@@ -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',
});
});
});
@@ -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<EventPublisher | Array<EventPublisher>>
): void {
this.#publishers.push(...publishers.flat());
addPublishers(_: any): void {
throw new Error(
'addPublishers is not supported anymore; use EventsService instead',
);
}
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): 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));
},
});
},
@@ -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();
@@ -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<HttpPostIngressOptions, 'topic'> };
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<HttpPostIngressOptions, 'topic'>;
},
@@ -68,10 +67,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
router.use('/http', this.createRouter(this.ingresses));
}
async setEventBroker(eventBroker: EventBroker): Promise<void> {
this.eventBroker = eventBroker;
}
private createRouter(ingresses: {
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
}): 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,
+81 -2
View File
@@ -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<Router> {
// ...
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());
```
+3 -3
View File
@@ -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<EventPublisher | Array<EventPublisher>>
): void;
// (undocumented)
// @deprecated (undocumented)
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
// (undocumented)
// @deprecated (undocumented)
setEventBroker(eventBroker: EventBroker): void;
}
+43 -10
View File
@@ -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<void>;
// (undocumented)
subscribe(options: EventsServiceSubscribeOptions): Promise<void>;
}
// @public @deprecated
export interface EventBroker {
publish(params: EventParams): Promise<void>;
subscribe(
@@ -18,29 +32,50 @@ export interface EventParams<TPayload = unknown> {
topic: string;
}
// @public
// @public @deprecated
export interface EventPublisher {
// (undocumented)
// @deprecated (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
// @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<void>;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
// (undocumented)
abstract supportsEventTopics(): string[];
subscribe(): Promise<void>;
}
// @public
export interface EventsService {
publish(params: EventParams): Promise<void>;
subscribe(options: EventsServiceSubscribeOptions): Promise<void>;
}
// @public (undocumented)
export type EventsServiceEventHandler = (params: EventParams) => Promise<void>;
// @public
export const eventsServiceRef: ServiceRef<EventsService, 'plugin'>;
// @public (undocumented)
export type EventsServiceSubscribeOptions = {
id: string;
topics: string[];
onEvent: EventsServiceEventHandler;
};
// @public @deprecated
export interface EventSubscriber {
// @deprecated
onEvent(params: EventParams): Promise<void>;
// @deprecated
supportsEventTopics(): string[];
}
@@ -79,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[];
}
```
+1
View File
@@ -45,6 +45,7 @@
"@backstage/backend-plugin-api": "workspace:^"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/cli": "workspace:^"
},
"files": [
@@ -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'),
);
});
});
@@ -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<EventsServiceSubscribeOptions, 'topics'>[]
>();
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<void> => {
return this.publish(params);
},
subscribe: (options: EventsServiceSubscribeOptions): Promise<void> => {
return this.subscribe({
...options,
id: `${pluginId}.${options.id}`,
});
},
};
}
async publish(params: EventParams): Promise<void> {
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<void>[] = [];
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<void> {
options.topics.forEach(topic => {
if (!this.subscribers.has(topic)) {
this.subscribers.set(topic, []);
}
this.subscribers.get(topic)!.push({
id: options.id,
onEvent: options.onEvent,
});
});
}
}
@@ -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 {
/**
@@ -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<void>;
}
+17 -22
View File
@@ -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 });
+32 -12
View File
@@ -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<void> {
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<void> {
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<void> {
this.eventBroker = eventBroker;
}
abstract supportsEventTopics(): string[];
}
@@ -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<void>;
}
@@ -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<void>;
/**
* Subscribes to one or more topics, registering an event handler for them.
*
* @param options - event subscription options.
*/
subscribe(options: EventsServiceSubscribeOptions): Promise<void>;
}
/**
* @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<void>;
@@ -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);
@@ -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];
}
}
+6 -3
View File
@@ -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';
+19
View File
@@ -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';
+9
View File
@@ -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<EventPublisher | Array<EventPublisher>>
): void;
/**
* @deprecated use `EventsService.subscribe` instead
*/
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;

Some files were not shown because too many files have changed in this diff Show More