Merge pull request #22344 from Bonial-International-GmbH/pjungermann/new-backend/events-service
new backend system: events service
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
- }
|
||||
}
|
||||
```
|
||||
@@ -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`.
|
||||
@@ -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'),
|
||||
);
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Add `events: EventsService` to `LegacyPluginEnvironment`.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+21
-11
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-8
@@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
+10
-14
@@ -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,
|
||||
|
||||
+26
-23
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
+16
-14
@@ -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({});
|
||||
});
|
||||
});
|
||||
|
||||
+15
-14
@@ -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,
|
||||
|
||||
+11
-17
@@ -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' }),
|
||||
|
||||
+7
-8
@@ -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()));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+15
-19
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+6
-6
@@ -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:^",
|
||||
|
||||
+20
-13
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -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 {
|
||||
|
||||
+18
-19
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+6
-6
@@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+15
-19
@@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+15
-19
@@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+15
-19
@@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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
Reference in New Issue
Block a user