Merge pull request #13931 from Bonial-International-GmbH/pjungermann/bitbucket-cloud/events

feat: events
This commit is contained in:
Fredrik Adelöw
2022-11-14 12:08:52 +01:00
committed by GitHub
142 changed files with 6958 additions and 36 deletions
+41
View File
@@ -0,0 +1,41 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
---
Handle Bitbucket Cloud `repo:push` events at the `BitbucketCloudEntityProvider`
by subscribing to the topic `bitbucketCloud.repo:push.`
Implements `EventSubscriber` to receive events for the topic `bitbucketCloud.repo:push`.
On `repo:push`, the affected repository will be refreshed.
This includes adding new Location entities, refreshing existing ones,
and removing obsolete ones.
To support this, a new annotation `bitbucket.org/repo-url` was added
to Location entities.
A full refresh will require 1 API call to Bitbucket Cloud to discover all catalog files.
When we handle one `repo:push` event, we also need 1 API call in order to know
which catalog files exist.
This may lead to more discovery-related API calls (code search).
The main cause for hitting the rate limits are Locations refresh-related operations.
A reduction of total API calls to reduce the rate limit issues can only be achieved in
combination with
1. reducing the full refresh frequency (e.g., to monthly)
2. reducing the frequency of general Location refresh operations by the processing loop
For (2.), it is not possible to reduce the frequency only for Bitbucket Cloud-related
Locations though.
Further optimizations might be required to resolve the rate limit issue.
**Installation and Migration**
Please find more information at
https://backstage.io/docs/integrations/bitbucketCloud/discovery,
in particular the section about "_Installation with Events Support_".
In case of the new backend-plugin-api _(alpha)_ the module will take care of
registering itself at both.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend-module-bitbucket-cloud': minor
---
Adds a new module `bitbucket-cloud` to plugin-events-backend.
The module adds a new event router `BitbucketCloudEventRouter`.
The event router will re-publish events received at topic `bitbucketCloud`
under a more specific topic depending on their `x-event-key` value
(e.g., `bitbucketCloud.repo:push`).
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend-module-github': minor
---
Adds a new module `github` to plugin-events-backend.
The module adds a new event router `GithubEventRouter`.
The event router will re-publish events received at topic `github`
under a more specific topic depending on their `x-github-event` value
(e.g., `github.push`).
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend-module-gerrit': minor
---
Adds a new module `gerrit` to plugin-events-backend.
The module adds a new event router `GerritEventRouter`.
The event router will re-publish events received at topic `gerrit`
under a more specific topic depending on their `$.type` value
(e.g., `gerrit.change-merged`).
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gerrit/README.md.
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-events-backend-module-aws-sqs': minor
---
Adds a new module `aws-sqs` for plugin-events-backend.
The module provides an event publisher `AwsSqsConsumingEventPublisher`
which will allow you to receive events from
an AWS SQS queue and will publish these to the used event broker.
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend-module-azure': minor
---
Adds a new module `azure` to plugin-events-backend.
The module adds a new event router `AzureDevOpsEventRouter`.
The event router will re-publish events received at topic `azureDevOps`
under a more specific topic depending on their `$.eventType` value
(e.g., `azureDevOps.git.push`).
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-azure/README.md.
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/plugin-events-backend': minor
'@backstage/plugin-events-node': minor
---
Support events received via HTTP endpoints at plugin-events-backend.
The plugin provides an event publisher `HttpPostIngressEventPublisher`
which will allow you to receive events via
HTTP endpoints `POST /api/events/http/{topic}`
and will publish these to the used event broker.
Using a provided custom validator, you can participate in the decision
which events are accepted, e.g. by verifying the source of the request.
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend': minor
'@backstage/plugin-events-node': minor
'@backstage/plugin-events-backend-test-utils': minor
---
Adds a new backend plugin plugin-events-backend for managing events.
plugin-events-node exposes interfaces which can be used by modules.
plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules.
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-bitbucket-cloud-common': patch
---
Add interfaces for Bitbucket Cloud (webhook) events.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend-module-gitlab': minor
---
Adds a new module `gitlab` to plugin-events-backend.
The module adds a new event router `GitlabEventRouter`.
The event router will re-publish events received at topic `gitlab`
under a more specific topic depending on their `$.event_name` value
(e.g., `gitlab.push`).
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gitlab/README.md.
+9
View File
@@ -36,6 +36,15 @@ yarn.lock @backstage/reviewers @backst
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
/plugins/cost-insights @backstage/reviewers @backstage/silver-lining
/plugins/cost-insights-* @backstage/reviewers @backstage/silver-lining
/plugins/events-backend @backstage/reviewers @pjungermann
/plugins/events-backend-module-aws-sqs @backstage/reviewers @pjungermann
/plugins/events-backend-module-azure @backstage/reviewers @pjungermann
/plugins/events-backend-module-bitbucket-cloud @backstage/reviewers @pjungermann
/plugins/events-backend-module-gerrit @backstage/reviewers @pjungermann
/plugins/events-backend-module-github @backstage/reviewers @pjungermann
/plugins/events-backend-module-gitlab @backstage/reviewers @pjungermann
/plugins/events-backend-test-utils @backstage/reviewers @pjungermann
/plugins/events-node @backstage/reviewers @pjungermann
/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers
/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers
/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers
+60 -6
View File
@@ -24,10 +24,12 @@ package.
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbucket-cloud
```
### Installation without Events Support
And then add the entity provider to your catalog builder:
```diff
// In packages/backend/src/plugins/catalog.ts
// packages/backend/src/plugins/catalog.ts
+ import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
export default async function createPlugin(
@@ -37,11 +39,7 @@ And then add the entity provider to your catalog builder:
+ builder.addEntityProvider(
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
+ logger: env.logger,
+ // optional: alternatively, configure via app-config.yaml
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 30 },
+ timeout: { minutes: 3 },
+ }),
+ scheduler: env.scheduler,
+ }),
+ );
@@ -49,6 +47,62 @@ And then add the entity provider to your catalog builder:
}
```
Alternatively to the config-based schedule, you can use
```diff
- scheduler: env.scheduler,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 30 },
+ timeout: { minutes: 3 },
+ }),
```
### Installation with Events Support
Please follow the installation instructions at
- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md
- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md
Additionally, 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)
Set up your provider
```diff
// packages/backend/src/plugins/catalogEventBasedProviders.ts
+import { CatalogClient } from '@backstage/catalog-client';
+import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { PluginEnvironment } from '../types';
export default async function createCatalogEventBasedProviders(
- _: PluginEnvironment,
+ env: PluginEnvironment,
): Promise<Array<EntityProvider & EventSubscriber>> {
const providers: Array<
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
> = [];
- // add your event-based entity providers here
+ providers.push(
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
+ catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
+ logger: env.logger,
+ scheduler: env.scheduler,
+ tokenManager: env.tokenManager,
+ }),
+ );
return providers.flat();
}
```
**Attention:**
`catalogApi` and `tokenManager` are required at this variant
compared to the one without events support.
## Configuration
To use the entity provider, you'll need a [Bitbucket Cloud integration set up](locations.md).
+3
View File
@@ -39,7 +39,10 @@
"@backstage/plugin-azure-sites-backend": "workspace:^",
"@backstage/plugin-badges-backend": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-code-coverage-backend": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-graphql-backend": "workspace:^",
"@backstage/plugin-jenkins-backend": "workspace:^",
"@backstage/plugin-kafka-backend": "workspace:^",
+12 -1
View File
@@ -42,7 +42,9 @@ import { metricsInit, metricsHandler } from './metrics';
import auth from './plugins/auth';
import azureDevOps from './plugins/azure-devops';
import catalog from './plugins/catalog';
import catalogEventBasedProviders from './plugins/catalogEventBasedProviders';
import codeCoverage from './plugins/codecoverage';
import events from './plugins/events';
import kubernetes from './plugins/kubernetes';
import kafka from './plugins/kafka';
import rollbar from './plugins/rollbar';
@@ -141,10 +143,19 @@ async function main() {
);
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
const playlistEnv = useHotMemoize(module, () => createEnv('playlist'));
const eventsEnv = useHotMemoize(module, () => createEnv('events'));
const eventBasedEntityProviders = await catalogEventBasedProviders(
catalogEnv,
);
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use(
'/catalog',
await catalog(catalogEnv, eventBasedEntityProviders),
);
apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv));
apiRouter.use('/events', await events(eventsEnv, eventBasedEntityProviders));
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/tech-insights', await techInsights(techInsightsEnv));
+3
View File
@@ -15,15 +15,18 @@
*/
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
providers?: Array<EntityProvider>,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
builder.addEntityProvider(providers ?? []);
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
@@ -0,0 +1,61 @@
/*
* Copyright 2020 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 {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
import { PluginEnvironment } from '../types';
class DemoEventBasedEntityProvider implements EntityProvider, EventSubscriber {
constructor(
private readonly logger: Logger,
private readonly topics: string[],
) {}
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 connect(_: EntityProviderConnection): Promise<void> {
// not doing anything here
}
getProviderName(): string {
return DemoEventBasedEntityProvider.name;
}
}
export default async function createCatalogEventBasedProviders(
env: PluginEnvironment,
): Promise<Array<EntityProvider & EventSubscriber>> {
const providers: Array<
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
> = [];
providers.push(new DemoEventBasedEntityProvider(env.logger, ['example']));
// add your event-based entity providers here
return providers.flat();
}
+45
View File
@@ -0,0 +1,45 @@
/*
* 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 {
EventsBackend,
HttpPostIngressEventPublisher,
} from '@backstage/plugin-events-backend';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
subscribers: EventSubscriber[],
): Promise<Router> {
const eventsRouter = Router();
const httpRouter = Router();
eventsRouter.use('/http', httpRouter);
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
logger: env.logger,
router: httpRouter,
});
await new EventsBackend(env.logger)
.addPublishers(http)
.addSubscribers(subscribers)
.start();
return eventsRouter;
}
@@ -24,6 +24,57 @@ export class BitbucketCloudClient {
): WithPagination<Models.SearchResultPage, Models.SearchCodeSearchResult>;
}
// @public (undocumented)
export namespace Events {
// (undocumented)
export interface Change {
// (undocumented)
closed: boolean;
// (undocumented)
commits: Models.Commit[];
// (undocumented)
created: boolean;
// (undocumented)
forced: boolean;
// (undocumented)
links: ChangeLinks;
// (undocumented)
new: Models.Branch;
// (undocumented)
old: Models.Branch;
// (undocumented)
truncated: boolean;
}
// (undocumented)
export interface ChangeLinks {
// (undocumented)
commits: Models.Link;
// (undocumented)
diff: Models.Link;
// (undocumented)
html: Models.Link;
}
// (undocumented)
export interface RepoEvent {
// (undocumented)
actor: Models.Account;
// (undocumented)
repository: Models.Repository & {
workspace: Models.Workspace;
};
}
// (undocumented)
export interface RepoPush {
// (undocumented)
changes: Change[];
}
// (undocumented)
export interface RepoPushEvent extends RepoEvent {
// (undocumented)
push: RepoPush;
}
}
// @public (undocumented)
export type FilterAndSortOptions = {
q?: string;
@@ -340,6 +391,37 @@ export namespace Models {
// (undocumented)
self?: Link;
}
export interface Workspace extends ModelObject {
// (undocumented)
created_on?: string;
is_private?: boolean;
// (undocumented)
links?: WorkspaceLinks;
name?: string;
slug?: string;
// (undocumented)
updated_on?: string;
uuid?: string;
}
// (undocumented)
export interface WorkspaceLinks {
// (undocumented)
avatar?: Link;
// (undocumented)
html?: Link;
// (undocumented)
members?: Link;
// (undocumented)
owners?: Link;
// (undocumented)
projects?: Link;
// (undocumented)
repositories?: Link;
// (undocumented)
self?: Link;
// (undocumented)
snippets?: Link;
}
}
// @public (undocumented)
@@ -30,6 +30,14 @@ const modelsModule = modelsFile.getModuleOrThrow('Models');
const clientFile = project.getSourceFile('src/BitbucketCloudClient.ts');
const clientClass = clientFile.getClassOrThrow('BitbucketCloudClient');
const eventsFile = project.getSourceFile('src/events/index.ts');
const eventsModule = eventsFile.getModuleOrThrow('Events');
const eventsStmts = [
...eventsModule.getClasses(),
...eventsModule.getInterfaces(),
...eventsModule.getTypeAliases(),
];
/**
* Returns an array of the unique items of the provided array.
*
@@ -79,7 +87,11 @@ function referencedModelsIdentifiers(stmt, processed) {
}
// all directly or transitively referenced/used `Models.[...]` are allowed to stay
const allowed = referencedModelsIdentifiers(clientClass);
const processed = [];
const allowed = referencedModelsIdentifiers(clientClass, processed);
allowed.push(
...eventsStmts.flatMap(stmt => referencedModelsIdentifiers(stmt, processed)),
);
// remove everything not part of the "allow list"
modelsModule
@@ -0,0 +1,56 @@
/*
* 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 { Models } from '../models';
// source: https://support.atlassian.com/bitbucket-cloud/docs/event-payloads
/** @public */
export namespace Events {
/** @public */
export interface RepoEvent {
repository: Models.Repository & { workspace: Models.Workspace };
actor: Models.Account;
}
/** @public */
export interface RepoPushEvent extends RepoEvent {
push: RepoPush;
}
/** @public */
export interface RepoPush {
changes: Change[];
}
/** @public */
export interface Change {
old: Models.Branch;
new: Models.Branch;
truncated: boolean;
created: boolean;
forced: boolean;
closed: boolean;
links: ChangeLinks;
commits: Models.Commit[];
}
/** @public */
export interface ChangeLinks {
commits: Models.Link;
diff: Models.Link;
html: Models.Link;
}
}
@@ -21,6 +21,7 @@
*/
export * from './BitbucketCloudClient';
export * from './events';
export * from './models';
export * from './pagination';
export * from './types';
@@ -518,4 +518,46 @@ export namespace Models {
repositories?: Link;
self?: Link;
}
/**
* A Bitbucket workspace.
* Workspaces are used to organize repositories.
* @public
*/
export interface Workspace extends ModelObject {
created_on?: string;
/**
* Indicates whether the workspace is publicly accessible, or whether it is
* private to the members and consequently only visible to members.
*/
is_private?: boolean;
links?: WorkspaceLinks;
/**
* The name of the workspace.
*/
name?: string;
/**
* The short label that identifies this workspace.
*/
slug?: string;
updated_on?: string;
/**
* The workspace's immutable id.
*/
uuid?: string;
}
/**
* @public
*/
export interface WorkspaceLinks {
avatar?: Link;
html?: Link;
members?: Link;
owners?: Link;
projects?: Link;
repositories?: Link;
self?: Link;
snippets?: Link;
}
}
@@ -4,24 +4,33 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
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 { PluginTaskScheduler } from '@backstage/backend-tasks';
import { TaskRunner } from '@backstage/backend-tasks';
import { TokenManager } from '@backstage/backend-common';
// @public
export class BitbucketCloudEntityProvider implements EntityProvider {
export class BitbucketCloudEntityProvider
implements EntityProvider, EventSubscriber
{
// (undocumented)
connect(connection: EntityProviderConnection): Promise<void>;
// (undocumented)
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
},
): BitbucketCloudEntityProvider[];
// (undocumented)
@@ -29,7 +38,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
// (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[];
}
// @alpha (undocumented)
@@ -33,13 +33,19 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-bitbucket-cloud-common": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"p-limit": "^3.1.0",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
@@ -333,6 +333,8 @@ describe('BitbucketCloudEntityProvider', () => {
annotations: {
'backstage.io/managed-by-location': `url:${url}`,
'backstage.io/managed-by-origin-location': `url:${url}`,
'bitbucket.org/repo-url':
'https://bitbucket.org/test-ws/test-repo2',
},
name: 'generated-7c2e6263b6cc2d14e69fd4d029afba601ad6dc3b',
},
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
import { CatalogApi } from '@backstage/catalog-client';
import {
Entity,
LocationEntity,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
BitbucketCloudIntegration,
@@ -22,22 +29,35 @@ import {
} from '@backstage/integration';
import {
BitbucketCloudClient,
Events,
Models,
} from '@backstage/plugin-bitbucket-cloud-common';
import {
DeferredEntity,
EntityProvider,
EntityProviderConnection,
LocationSpec,
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-backend';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
import {
BitbucketCloudEntityProviderConfig,
readProviderConfigs,
} from './BitbucketCloudEntityProviderConfig';
import limiterFactory from 'p-limit';
import * as uuid from 'uuid';
import { Logger } from 'winston';
const DEFAULT_BRANCH = 'master';
const TOPIC_REPO_PUSH = 'bitbucketCloud/repo:push';
/** @public */
export const ANNOTATION_BITBUCKET_CLOUD_REPO_URL = 'bitbucket.org/repo-url';
interface IngestionTarget {
fileUrl: string;
repoUrl: string;
}
/**
* Discovers catalog files located in [Bitbucket Cloud](https://bitbucket.org).
@@ -47,19 +67,27 @@ const DEFAULT_BRANCH = 'master';
*
* @public
*/
export class BitbucketCloudEntityProvider implements EntityProvider {
export class BitbucketCloudEntityProvider
implements EntityProvider, EventSubscriber
{
private readonly client: BitbucketCloudClient;
private readonly config: BitbucketCloudEntityProviderConfig;
private readonly logger: Logger;
private readonly scheduleFn: () => Promise<void>;
private readonly catalogApi?: CatalogApi;
private readonly tokenManager?: TokenManager;
private connection?: EntityProviderConnection;
private eventConfigErrorThrown = false;
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
},
): BitbucketCloudEntityProvider[] {
const integrations = ScmIntegrations.fromConfig(config);
@@ -90,6 +118,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
integration,
options.logger,
taskRunner,
options.catalogApi,
options.tokenManager,
);
});
}
@@ -99,6 +129,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
integration: BitbucketCloudIntegration,
logger: Logger,
taskRunner: TaskRunner,
catalogApi?: CatalogApi,
tokenManager?: TokenManager,
) {
this.client = BitbucketCloudClient.fromConfig(integration.config);
this.config = config;
@@ -106,6 +138,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(taskRunner);
this.catalogApi = catalogApi;
this.tokenManager = tokenManager;
}
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
@@ -154,15 +188,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
logger.info('Discovering catalog files in Bitbucket Cloud repositories');
const targets = await this.findCatalogFiles();
const entities = targets
.map(BitbucketCloudEntityProvider.toLocationSpec)
.map(location => locationSpecToLocationEntity({ location }))
.map(entity => {
return {
locationKey: this.getProviderName(),
entity: entity,
};
});
const entities = this.toDeferredEntities(targets);
await this.connection.applyMutation({
type: 'full',
@@ -174,7 +200,135 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
);
}
private async findCatalogFiles(): Promise<string[]> {
/** {@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;
}
if (params.metadata?.['x-event-key'] === 'repo:push') {
await this.onRepoPush(params.eventPayload as Events.RepoPushEvent);
}
}
private canHandleEvents(): boolean {
if (this.catalogApi && this.tokenManager) {
return true;
}
// throw only once
if (!this.eventConfigErrorThrown) {
this.eventConfigErrorThrown = true;
throw new Error(
`${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.`,
);
}
return false;
}
async onRepoPush(event: Events.RepoPushEvent): Promise<void> {
if (!this.canHandleEvents()) {
return;
}
if (!this.connection) {
throw new Error('Not initialized');
}
if (event.repository.workspace.slug !== this.config.workspace) {
return;
}
if (!this.matchesFilters(event.repository)) {
return;
}
const repoName = event.repository.slug;
const repoUrl = event.repository.links!.html!.href!;
this.logger.info(`handle repo:push event for ${repoUrl}`);
// The commit information at the webhook only contains some high level metadata.
// In order to understand whether relevant files have changed we would need to
// look up all commits which would cost additional API calls.
// The overall goal is to optimize the necessary amount of API calls.
// Hence, we will just trigger a refresh for catalog file(s) within the repository
// if we get notified about changes there.
const targets = await this.findCatalogFiles(repoName);
const { token } = await this.tokenManager!.getToken();
const existing = await this.findExistingLocations(repoUrl, token);
const added: DeferredEntity[] = this.toDeferredEntities(
targets.filter(
// All Locations are managed by this provider and only have `target`, never `targets`.
// All URLs (fileUrl, target) are created using `BitbucketCloudEntityProvider.toUrl`.
// Hence, we can keep the comparison simple and don't need to handle different
// casing or encoding, etc.
target => !existing.find(item => item.spec.target === target.fileUrl),
),
);
const limiter = limiterFactory(10);
const stillExisting: Entity[] = [];
const removed: DeferredEntity[] = [];
existing.forEach(item => {
if (targets.find(value => value.fileUrl === item.spec.target)) {
stillExisting.push(item);
} else {
removed.push({
locationKey: this.getProviderName(),
entity: item,
});
}
});
const promises: Promise<void>[] = stillExisting.map(entity =>
limiter(async () =>
this.catalogApi!.refreshEntity(stringifyEntityRef(entity), { token }),
),
);
if (added.length > 0 || removed.length > 0) {
const connection = this.connection;
promises.push(
limiter(async () =>
connection.applyMutation({
type: 'delta',
added: added,
removed: removed,
}),
),
);
}
await Promise.all(promises);
}
private async findExistingLocations(
repoUrl: string,
token: string,
): Promise<LocationEntity[]> {
const filter: Record<string, string> = {};
filter.kind = 'Location';
filter[`metadata.annotations.${ANNOTATION_BITBUCKET_CLOUD_REPO_URL}`] =
repoUrl;
return this.catalogApi!.getEntities({ filter }, { token }).then(
result => result.items,
) as Promise<LocationEntity[]>;
}
private async findCatalogFiles(
repoName?: string,
): Promise<IngestionTarget[]> {
const workspace = this.config.workspace;
const catalogPath = this.config.catalogPath;
@@ -197,12 +351,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
// ...except the one we need
'+values.file.commit.repository.links.html.href',
].join(',');
const query = `"${catalogFilename}" path:${catalogPath}`;
const optRepoFilter = repoName ? ` repo:${repoName}` : '';
const query = `"${catalogFilename}" path:${catalogPath}${optRepoFilter}`;
const searchResults = this.client
.searchCode(workspace, query, { fields })
.iterateResults();
const result: string[] = [];
const result: IngestionTarget[] = [];
for await (const searchResult of searchResults) {
// not a file match, but a code match
@@ -212,12 +367,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
const repository = searchResult.file!.commit!.repository!;
if (this.matchesFilters(repository)) {
result.push(
BitbucketCloudEntityProvider.toUrl(
result.push({
fileUrl: BitbucketCloudEntityProvider.toUrl(
repository,
searchResult.file!.path!,
),
);
repoUrl: repository.links!.html!.href!,
});
}
}
@@ -234,11 +390,32 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
);
}
private toDeferredEntities(targets: IngestionTarget[]): DeferredEntity[] {
return targets
.map(target => {
const location = BitbucketCloudEntityProvider.toLocationSpec(
target.fileUrl,
);
const entity = locationSpecToLocationEntity({ location });
entity.metadata.annotations = {
...entity.metadata.annotations,
[ANNOTATION_BITBUCKET_CLOUD_REPO_URL]: target.repoUrl,
};
return entity;
})
.map(entity => {
return {
locationKey: this.getProviderName(),
entity: entity,
};
});
}
private static toUrl(
repository: Models.Repository,
filePath: string,
): string {
const repoUrl = repository.links!.html!.href;
const repoUrl = repository.links!.html!.href!;
const branch = repository.mainbranch?.name ?? DEFAULT_BRANCH;
return `${repoUrl}/src/${branch}/${filePath}`;
@@ -15,11 +15,17 @@
*/
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import {
getVoidLogger,
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common';
import {
configServiceRef,
discoveryServiceRef,
loggerServiceRef,
schedulerServiceRef,
tokenManagerServiceRef,
} from '@backstage/backend-plugin-api';
import {
PluginTaskScheduler,
@@ -27,20 +33,27 @@ import {
} from '@backstage/backend-tasks';
import { startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { Duration } from 'luxon';
import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule';
import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider';
describe('bitbucketCloudEntityProviderCatalogModule', () => {
it('should register provider at the catalog extension point', async () => {
let addedProviders: Array<BitbucketCloudEntityProvider> | undefined;
let addedSubscribers: Array<BitbucketCloudEntityProvider> | undefined;
let usedSchedule: TaskScheduleDefinition | undefined;
const extensionPoint = {
const catalogExtensionPointImpl = {
addEntityProvider: (providers: any) => {
addedProviders = providers;
},
};
const eventsExtensionPointImpl = {
addSubscribers: (subscribers: any) => {
addedSubscribers = subscribers;
},
};
const runner = jest.fn();
const scheduler = {
createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => {
@@ -48,6 +61,8 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => {
return runner;
},
} as unknown as PluginTaskScheduler;
const discovery = jest.fn() as any as PluginEndpointDiscovery;
const tokenManager = jest.fn() as any as TokenManager;
const config = new ConfigReader({
catalog: {
@@ -64,11 +79,16 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => {
});
await startTestBackend({
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
extensionPoints: [
[catalogProcessingExtensionPoint, catalogExtensionPointImpl],
[eventsExtensionPoint, eventsExtensionPointImpl],
],
services: [
[configServiceRef, config],
[discoveryServiceRef, discovery],
[loggerServiceRef, getVoidLogger()],
[schedulerServiceRef, scheduler],
[tokenManagerServiceRef, tokenManager],
],
features: [bitbucketCloudEntityProviderCatalogModule()],
});
@@ -79,6 +99,7 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => {
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'bitbucketCloud-provider:default',
);
expect(addedSubscribers).toEqual(addedProviders);
expect(runner).not.toHaveBeenCalled();
});
});
@@ -20,8 +20,13 @@ import {
loggerServiceRef,
loggerToWinstonLogger,
schedulerServiceRef,
tokenManagerServiceRef,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import {
catalogProcessingExtensionPoint,
catalogServiceRef,
} from '@backstage/plugin-catalog-node';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider';
/**
@@ -34,18 +39,34 @@ export const bitbucketCloudEntityProviderCatalogModule = createBackendModule({
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
catalogApi: catalogServiceRef,
config: configServiceRef,
// 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,
logger: loggerServiceRef,
scheduler: schedulerServiceRef,
tokenManager: tokenManagerServiceRef,
},
async init({ catalog, config, logger, scheduler }) {
async init({
catalog,
catalogApi,
config,
events,
logger,
scheduler,
tokenManager,
}) {
const winstonLogger = loggerToWinstonLogger(logger);
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
catalogApi,
logger: winstonLogger,
scheduler,
tokenManager,
});
catalog.addEntityProvider(providers);
events.addSubscribers(providers);
},
});
},
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,42 @@
# events-backend-module-aws-sqs
Welcome to the `events-backend-module-aws-sqs` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it 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.
## Configuration
The polled AWS SQS queues depend on your configuration:
```yaml
events:
modules:
awsSqs:
awsSqsConsumingEventPublisher:
topics:
topicName1: # replace with actual topic name as expected by subscribers
queue:
url: 'https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue'
region: us-east-2
# visibilityTimeout - as HumanDuration; defaults to queue-based config
# waitTime - as HumanDuration; defaults to max of 20 seconds (long polling)
# timeout - as HumanDuration; timeout for the task execution
# waitTimeAfterEmptyReceive - as HumanDuration; time to wait before a retry when there was no message.
topicName2:
# [...]
```
## Installation
1. Install the [`events-backend` plugin](../events-backend/README.md).
2. Install this module
3. Add your configuration.
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-aws-sqs
```
@@ -0,0 +1,29 @@
## API Report File for "@backstage/plugin-events-backend-module-aws-sqs"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { EventBroker } from '@backstage/plugin-events-node';
import { EventPublisher } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
// @public
export class AwsSqsConsumingEventPublisher implements EventPublisher {
// (undocumented)
static fromConfig(env: {
config: Config;
logger: Logger;
scheduler: PluginTaskScheduler;
}): AwsSqsConsumingEventPublisher[];
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
// @alpha
export const awsSqsConsumingEventPublisherEventsModule: (
options?: undefined,
) => BackendFeature;
```
+78
View File
@@ -0,0 +1,78 @@
/*
* 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 { HumanDuration } from '@backstage/types';
export interface Config {
events?: {
modules?: {
/**
* events-backend-module-aws-sqs plugin configuration.
*/
awsSqs?: {
/**
* Configuration for AwsSqsConsumingEventPublisher.
*/
awsSqsConsumingEventPublisher?: {
/**
* Contains a record per topic for which an AWS SQS queue
* should be used as source of events.
*/
topics: Record<
string,
{
/**
* (Required) Queue-related configuration.
*/
queue: {
/**
* (Required) The region of the AWS SQS queue.
*/
region: string;
/**
* (Required) The absolute URL for the AWS SQS queue to be used.
*/
url: string;
/**
* (Optional) Visibility timeout for messages in flight.
*/
visibilityTimeout: HumanDuration;
/**
* (Optional) Wait time when polling for available messages.
* Default: 20 seconds.
*/
waitTime: HumanDuration;
};
/**
* (Optional) Timeout for the task execution which includes polling for messages
* and publishing the events to the event broker
* and the wait time after empty receives.
*
* Must be greater than `queue.waitTime` + `waitTimeAfterEmptyReceive`.
*/
timeout: HumanDuration;
/**
* (Optional) Wait time before polling again if no message was received.
* Default: 1 minute.
*/
waitTimeAfterEmptyReceive: HumanDuration;
}
>;
};
};
};
};
}
@@ -0,0 +1,48 @@
{
"name": "@backstage/plugin-events-backend-module-aws-sqs",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.0.0",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/types": "workspace:^",
"luxon": "^3.0.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"aws-sdk-client-mock": "^2.0.0"
},
"files": [
"alpha",
"config.d.ts",
"dist"
],
"configSchema": "config.d.ts"
}
@@ -0,0 +1,27 @@
/*
* Copyright 2020 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.
*/
/**
* The module "sqs" for the Backstage backend plugin "events"
* adding an AWS SQS-based publisher,
* receiving events from an AWS SQS queue and passing it to the
* internal event broker.
*
* @packageDocumentation
*/
export { AwsSqsConsumingEventPublisher } from './publisher/AwsSqsConsumingEventPublisher';
export { awsSqsConsumingEventPublisherEventsModule } from './service/AwsSqsConsumingEventPublisherEventsModule';
@@ -0,0 +1,227 @@
/*
* 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 {
DeleteMessageBatchCommand,
ReceiveMessageCommand,
SQSClient,
} from '@aws-sdk/client-sqs';
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 { mockClient } from 'aws-sdk-client-mock';
import { AwsSqsConsumingEventPublisher } from './AwsSqsConsumingEventPublisher';
describe('AwsSqsConsumingEventPublisher', () => {
it('creates one publisher instance per configured topic', async () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
},
},
fake2: {
queue: {
region: 'us-east-1',
url: 'https://fake2.queue.url',
},
},
},
},
},
},
},
});
const logger = getVoidLogger();
const scheduler = {
scheduleTask: jest.fn(),
} as unknown as PluginTaskScheduler;
const publishers = AwsSqsConsumingEventPublisher.fromConfig({
config,
logger,
scheduler,
});
expect(publishers.length).toEqual(2);
});
it('polling will be scheduled after connecting to the EventBroker', async () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
},
},
},
},
},
},
},
});
const logger = getVoidLogger();
const scheduler = {
scheduleTask: jest.fn(),
} as unknown as PluginTaskScheduler;
const publishers = AwsSqsConsumingEventPublisher.fromConfig({
config,
logger,
scheduler,
});
expect(publishers.length).toEqual(1);
const publisher = publishers[0];
const eventBroker = new TestEventBroker();
await 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',
frequency: { seconds: 0 },
timeout: { seconds: 260 },
scope: 'local',
}),
);
});
it('publishes events for received messages and deletes them in bulk', async () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
},
waitTimeAfterEmptyReceive: { seconds: 1 },
},
},
},
},
},
},
});
const logger = getVoidLogger();
let taskFn: (() => Promise<void>) | undefined = undefined;
const scheduler = {
scheduleTask: (spec: { fn: () => Promise<void> }) => {
taskFn = spec.fn;
},
} as unknown as PluginTaskScheduler;
// on the first attempt, we will return 1 message and 0 messages afterwards
const sqsMock = mockClient(SQSClient);
sqsMock
.on(ReceiveMessageCommand, {
MaxNumberOfMessages: 10,
QueueUrl: 'https://fake1.queue.url',
WaitTimeSeconds: 20,
})
.resolvesOnce({
Messages: [],
})
.resolvesOnce({
Messages: [
{
Body: '{"event":"payload1"}',
ReceiptHandle: 'fake-handle1',
MessageAttributes: {
'X-Custom-Attr': {
DataType: 'String',
StringValue: 'value',
},
},
},
{
Body: '{"event":"payload2"}',
ReceiptHandle: 'fake-handle2',
},
],
})
.on(DeleteMessageBatchCommand, {
Entries: [
{
Id: 'message-0',
ReceiptHandle: 'fake-handle1',
},
{
Id: 'message-1',
ReceiptHandle: 'fake-handle2',
},
],
QueueUrl: 'https://fake1.queue.url',
})
.resolvesOnce({
Failed: [
{
Id: 'message-1',
Message: 'test failure',
SenderFault: true,
Code: '400',
},
],
Successful: [{ Id: 'message-0' }],
});
const publishers = AwsSqsConsumingEventPublisher.fromConfig({
config,
logger,
scheduler,
});
expect(publishers.length).toEqual(1);
const publisher = publishers[0];
const eventBroker = new TestEventBroker();
await publisher.setEventBroker(eventBroker);
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({
event: 'payload1',
});
expect(eventBroker.published[0].metadata).toEqual({
'X-Custom-Attr': 'value',
});
expect(eventBroker.published[1].topic).toEqual('fake1');
expect(eventBroker.published[1].eventPayload).toEqual({
event: 'payload2',
});
expect(eventBroker.published[1].metadata).toEqual({});
});
});
@@ -0,0 +1,191 @@
/*
* 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 {
DeleteMessageBatchCommand,
Message,
ReceiveMessageCommand,
ReceiveMessageCommandInput,
SQSClient,
} from '@aws-sdk/client-sqs';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import { EventBroker, EventPublisher } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
import { AwsSqsEventSourceConfig, readConfig } from './config';
/**
* Publishes events received from an AWS SQS queue.
* The message payload will be used as event payload and passed to registered subscribers.
*
* @public
*/
// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.)
export class AwsSqsConsumingEventPublisher implements EventPublisher {
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;
scheduler: PluginTaskScheduler;
}): AwsSqsConsumingEventPublisher[] {
return readConfig(env.config).map(
config =>
new AwsSqsConsumingEventPublisher(env.logger, env.scheduler, config),
);
}
private constructor(
private readonly logger: Logger,
private readonly scheduler: PluginTaskScheduler,
config: AwsSqsEventSourceConfig,
) {
this.topic = config.topic;
this.receiveParams = {
MaxNumberOfMessages: 10,
MessageAttributeNames: ['All'],
QueueUrl: config.queueUrl,
VisibilityTimeout: config.visibilityTimeout?.as('seconds'),
WaitTimeSeconds: config.pollingWaitTime.as('seconds'),
};
this.sqs = new SQSClient({ region: config.region });
this.queueUrl = config.queueUrl;
this.taskTimeoutSeconds = config.timeout.as('seconds');
this.waitTimeAfterEmptyReceiveMs =
config.waitTimeAfterEmptyReceive.as('milliseconds');
}
async setEventBroker(eventBroker: EventBroker): Promise<void> {
this.eventBroker = eventBroker;
return this.start();
}
private async start(): Promise<void> {
const id = `events.awsSqs.publisher:${this.topic}`;
const logger = this.logger.child({
class: AwsSqsConsumingEventPublisher.prototype.constructor.name,
taskId: id,
});
await this.scheduler.scheduleTask({
id: id,
frequency: { seconds: 0 },
timeout: { seconds: this.taskTimeoutSeconds },
scope: 'local',
fn: async () => {
try {
const numMessages = await this.consumeMessages();
if (numMessages === 0) {
await this.sleep(this.waitTimeAfterEmptyReceiveMs);
}
} catch (error) {
logger.error(error);
}
},
});
}
private async deleteMessages(messages?: Message[]): Promise<void> {
if (!messages) {
return;
}
const deleteParams = {
QueueUrl: this.queueUrl,
Entries: messages.map((message, index) => {
return {
Id: message.MessageId ?? `message-${index}`,
ReceiptHandle: message.ReceiptHandle,
};
}),
};
try {
const result = await this.sqs.send(
new DeleteMessageBatchCommand(deleteParams),
);
if (result.Failed) {
this.logger.error(
`Failed to delete ${result.Failed!.length} of ${
messages.length
} messages from AWS SQS ${this.queueUrl}. First: ${
result.Failed[0].Message
}`,
);
}
} catch (error) {
this.logger.error(
`Failed to delete message from AWS SQS ${this.queueUrl}`,
error,
);
}
}
private async consumeMessages(): Promise<number> {
try {
const data = await this.sqs.send(
new ReceiveMessageCommand(this.receiveParams),
);
data.Messages?.forEach(message => {
const eventPayload = JSON.parse(message.Body!);
const metadata: Record<string, string | string[]> = {};
Object.keys(message.MessageAttributes ?? {}).forEach(key => {
const attrValue = message.MessageAttributes![key];
if (
!attrValue ||
!attrValue.DataType ||
!['String', 'Number'].includes(attrValue.DataType)
) {
return;
}
const value = attrValue.StringListValues ?? attrValue.StringValue;
if (value !== undefined) {
metadata[key] = value;
}
});
this.eventBroker!.publish({
topic: this.topic,
eventPayload,
metadata,
});
});
await this.deleteMessages(data.Messages);
return data.Messages?.length ?? 0;
} catch (error) {
this.logger.error(
`Failed to receive events from AWS SQS ${this.queueUrl}`,
error,
);
return 0;
}
}
private sleep(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
}
@@ -0,0 +1,222 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import { readConfig } from './config';
describe('readConfig', () => {
it('not configured', () => {
const config = new ConfigReader({});
const publisherConfigs = readConfig(config);
expect(publisherConfigs.length).toBe(0);
});
it('only required fields configured', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
},
},
fake2: {
queue: {
region: 'us-east-1',
url: 'https://fake2.queue.url',
},
},
},
},
},
},
},
});
const publisherConfigs = readConfig(config);
expect(publisherConfigs.length).toBe(2);
expect(publisherConfigs[0].topic).toEqual('fake1');
expect(publisherConfigs[0].region).toEqual('eu-west-1');
expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url');
expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(20);
expect(publisherConfigs[0].timeout.as('seconds')).toBe(260);
expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe(
60,
);
});
it('all fields configured', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: 10 },
},
timeout: { minutes: 5 },
waitTimeAfterEmptyReceive: { seconds: 30 },
},
},
},
},
},
},
});
const publisherConfigs = readConfig(config);
expect(publisherConfigs.length).toBe(1);
expect(publisherConfigs[0].topic).toEqual('fake1');
expect(publisherConfigs[0].region).toEqual('eu-west-1');
expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url');
expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(10);
expect(publisherConfigs[0].timeout.as('seconds')).toBe(300);
expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe(
30,
);
});
it('fail on negative queue.waitTime', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: -10 },
},
timeout: { minutes: 5 },
waitTimeAfterEmptyReceive: { seconds: 30 },
},
},
},
},
},
},
});
expect(() => readConfig(config)).toThrow(
'events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime must be within 0..20 seconds',
);
});
it('fail on too high queue.waitTime', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: 30 },
},
timeout: { minutes: 5 },
waitTimeAfterEmptyReceive: { seconds: 30 },
},
},
},
},
},
},
});
expect(() => readConfig(config)).toThrow(
'events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime must be within 0..20 seconds',
);
});
it('fail on too low timeout', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: 10 },
},
timeout: { seconds: 10 },
waitTimeAfterEmptyReceive: { seconds: 30 },
},
},
},
},
},
},
});
expect(() => readConfig(config)).toThrow(
'The events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.timeout must be greater than events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime',
);
});
it('fail on negative waitTimeAfterEmptyReceive', () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: 10 },
},
timeout: { minutes: 5 },
waitTimeAfterEmptyReceive: { seconds: -30 },
},
},
},
},
},
},
});
expect(() => readConfig(config)).toThrow(
'The events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.waitTimeAfterEmptyReceive must not be negative',
);
});
});
@@ -0,0 +1,117 @@
/*
* 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 { Config } from '@backstage/config';
import { HumanDuration, JsonObject } from '@backstage/types';
import { Duration } from 'luxon';
const CONFIG_PREFIX_MODULE = 'events.modules.awsSqs.';
const CONFIG_PREFIX_PUBLISHER = `${CONFIG_PREFIX_MODULE}awsSqsConsumingEventPublisher.`;
const DEFAULT_WAIT_TIME_AFTER_EMPTY_RECEIVE = { minutes: 1 };
const MAX_WAIT_SECONDS = 20;
export interface AwsSqsEventSourceConfig {
pollingWaitTime: Duration;
queueUrl: string;
region: string;
timeout: Duration;
topic: string;
visibilityTimeout?: Duration;
waitTimeAfterEmptyReceive: Duration;
}
// TODO(pjungermann): validation could be improved similar to `convertToHumanDuration` at @backstage/backend-tasks
function readOptionalHumanDuration(
config: Config,
key: string,
): HumanDuration | undefined {
return config.getOptional<JsonObject>(key) as HumanDuration;
}
function readOptionalDuration(
config: Config,
key: string,
): Duration | undefined {
const duration = readOptionalHumanDuration(config, key);
return duration ? Duration.fromObject(duration) : undefined;
}
export function readConfig(config: Config): AwsSqsEventSourceConfig[] {
const key = `${CONFIG_PREFIX_PUBLISHER}topics`;
const topics = config.getOptionalConfig(key);
return (
topics?.keys()?.map(topic => {
const topicConfig = topics.getConfig(topic);
const keyPrefix = `${key}.${topic}.`;
// queue config:
const pollingWaitTime = Duration.fromObject(
readOptionalHumanDuration(topicConfig, 'queue.waitTime') ?? {
seconds: MAX_WAIT_SECONDS,
},
);
if (
pollingWaitTime.valueOf() < 0 ||
pollingWaitTime.as('seconds') > MAX_WAIT_SECONDS
) {
throw new Error(
`${keyPrefix}queue.waitTime must be within 0..${MAX_WAIT_SECONDS} seconds.`,
);
}
const queueUrl = topicConfig.getString('queue.url');
const region = topicConfig.getString('queue.region');
const visibilityTimeout = readOptionalDuration(
topicConfig,
'queue.visibilityTimeout',
);
// task:
const waitTimeAfterEmptyReceive = Duration.fromObject(
readOptionalHumanDuration(topicConfig, 'waitTimeAfterEmptyReceive') ??
DEFAULT_WAIT_TIME_AFTER_EMPTY_RECEIVE,
);
if (waitTimeAfterEmptyReceive.valueOf() < 0) {
throw new Error(
`The ${keyPrefix}waitTimeAfterEmptyReceive must not be negative.`,
);
}
const timeout =
readOptionalDuration(topicConfig, 'timeout') ??
pollingWaitTime
.plus(waitTimeAfterEmptyReceive)
.plus(Duration.fromObject({ seconds: 180 }));
if (
timeout.valueOf() <=
pollingWaitTime.valueOf() + waitTimeAfterEmptyReceive.valueOf()
) {
throw new Error(
`The ${keyPrefix}timeout must be greater than ${keyPrefix}queue.waitTime + ${keyPrefix}waitTimeAfterEmptyReceive.`,
);
}
return {
pollingWaitTime,
queueUrl,
region,
timeout,
topic,
visibilityTimeout,
waitTimeAfterEmptyReceive,
};
}) ?? []
);
}
@@ -0,0 +1,94 @@
/*
* 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 {
configServiceRef,
loggerServiceRef,
schedulerServiceRef,
} from '@backstage/backend-plugin-api';
import { startTestBackend } from '@backstage/backend-test-utils';
import { ConfigReader } from '@backstage/config';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { awsSqsConsumingEventPublisherEventsModule } from './AwsSqsConsumingEventPublisherEventsModule';
import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher';
describe('awsSqsEventsModule', () => {
it('should be correctly wired and set up', async () => {
const config = new ConfigReader({
events: {
modules: {
awsSqs: {
awsSqsConsumingEventPublisher: {
topics: {
fake1: {
queue: {
region: 'eu-west-1',
url: 'https://fake1.queue.url',
},
},
fake2: {
queue: {
region: 'us-east-1',
url: 'https://fake2.queue.url',
},
},
},
},
},
},
},
});
let addedPublishers: AwsSqsConsumingEventPublisher[] | undefined;
const extensionPoint = {
addPublishers: (publishers: any) => {
addedPublishers = publishers;
},
};
const scheduler = {
scheduleTask: jest.fn(),
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [
[configServiceRef, config],
[loggerServiceRef, getVoidLogger()],
[schedulerServiceRef, scheduler],
],
features: [awsSqsConsumingEventPublisherEventsModule()],
});
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' }),
);
expect(scheduler.scheduleTask).toHaveBeenCalledWith(
expect.objectContaining({ id: 'events.awsSqs.publisher:fake2' }),
);
});
});
@@ -0,0 +1,55 @@
/*
* 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 {
configServiceRef,
createBackendModule,
loggerServiceRef,
loggerToWinstonLogger,
schedulerServiceRef,
} from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher';
/**
* AWS SQS module for the Events plugin.
*
* @alpha
*/
export const awsSqsConsumingEventPublisherEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'awsSqsConsumingEventPublisherEventsModule',
register(env) {
env.registerInit({
deps: {
config: configServiceRef,
events: eventsExtensionPoint,
logger: loggerServiceRef,
scheduler: schedulerServiceRef,
},
async init({ config, events, logger, scheduler }) {
const winstonLogger = loggerToWinstonLogger(logger);
const sqs = AwsSqsConsumingEventPublisher.fromConfig({
config: config,
logger: winstonLogger,
scheduler: scheduler,
});
events.addPublishers(sqs);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,43 @@
# events-backend-module-azure
Welcome to the `events-backend-module-azure` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it with an `AzureDevOpsEventRouter`.
The event router will subscribe to the topic `azureDevOps`
and route the events to more concrete topics based on the value
of the provided `$.eventType` payload field.
Examples:
| `$.eventType` | topic |
| ------------------------- | ------------------------------------- |
| `git.push` | `azureDevOps.git.push` |
| `git.pullrequest.created` | `azureDevOps.git.pullrequest.created` |
Please find all possible webhook event types at the
[official documentation of events](https://learn.microsoft.com/en-us/azure/devops/service-hooks/events?source=recommendations&view=azure-devops)
and [webhooks](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops).
## Installation
Install the [`events-backend` plugin](../events-backend/README.md).
Install this module:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-azure
```
Add the event router to the `EventsBackend`:
```diff
+const githubEventRouter = new AzureDevOpsEventRouter();
EventsBackend
+ .addPublishers(githubEventRouter)
+ .addSubscribers(githubEventRouter);
// [...]
```
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-events-backend-module-azure"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventParams } from '@backstage/plugin-events-node';
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
// @public
export class AzureDevOpsEventRouter extends SubTopicEventRouter {
constructor();
// (undocumented)
protected determineSubTopic(params: EventParams): string | undefined;
}
// @alpha
export const azureDevOpsEventRouterEventsModule: (
options?: undefined,
) => BackendFeature;
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend-module-azure",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"supertest": "^6.1.3"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
/**
* The module "azure" for the Backstage backend plugin "events-backend"
* adding an event router for Azure DevOps.
*
* @packageDocumentation
*/
export { AzureDevOpsEventRouter } from './router/AzureDevOpsEventRouter';
export { azureDevOpsEventRouterEventsModule } from './service/AzureDevOpsEventRouterEventsModule';
@@ -0,0 +1,50 @@
/*
* 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter';
describe('AzureDevOpsEventRouter', () => {
const eventRouter = new AzureDevOpsEventRouter();
const topic = 'azureDevOps';
const eventPayload = { eventType: 'test.type', test: 'payload' };
const metadata = {};
it('no $.eventType', () => {
const eventBroker = new TestEventBroker();
eventRouter.setEventBroker(eventBroker);
eventRouter.onEvent({
topic,
eventPayload: { invalid: 'payload' },
metadata,
});
expect(eventBroker.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);
});
});
@@ -0,0 +1,42 @@
/*
* 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 {
EventParams,
SubTopicEventRouter,
} from '@backstage/plugin-events-node';
/**
* Subscribes to the generic `azureDevOps` topic
* and publishes the events under the more concrete sub-topic
* depending on the `$.eventType` provided.
*
* @public
*/
export class AzureDevOpsEventRouter extends SubTopicEventRouter {
constructor() {
super('azureDevOps');
}
protected determineSubTopic(params: EventParams): string | undefined {
if ('eventType' in (params.eventPayload as object)) {
const payload = params.eventPayload as { eventType: string };
return payload.eventType;
}
return undefined;
}
}
@@ -0,0 +1,46 @@
/*
* 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 { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { azureDevOpsEventRouterEventsModule } from './AzureDevOpsEventRouterEventsModule';
import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter';
describe('azureDevOpsEventRouterEventsModule', () => {
it('should be correctly wired and set up', async () => {
let addedPublisher: AzureDevOpsEventRouter | undefined;
let addedSubscriber: AzureDevOpsEventRouter | undefined;
const extensionPoint = {
addPublishers: (publisher: any) => {
addedPublisher = publisher;
},
addSubscribers: (subscriber: any) => {
addedSubscriber = subscriber;
},
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [],
features: [azureDevOpsEventRouterEventsModule()],
});
expect(addedPublisher).not.toBeUndefined();
expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter);
expect(addedSubscriber).not.toBeUndefined();
expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter';
/**
* Module for the events-backend plugin, adding an event router for Azure DevOps.
*
* Registers the {@link AzureDevOpsEventRouter}.
*
* @alpha
*/
export const azureDevOpsEventRouterEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'azureDevOpsEventRouter',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
const eventRouter = new AzureDevOpsEventRouter();
events.addPublishers(eventRouter);
events.addSubscribers(eventRouter);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,43 @@
# events-backend-module-bitbucket-cloud
Welcome to the `events-backend-module-bitbucket-cloud` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it with an `BitbucketCloudEventRouter`.
The event router will subscribe to the topic `bitbucketCloud`
and route the events to more concrete topics based on the value
of the provided `x-event-key` metadata field.
Examples:
| x-event-key | topic |
| --------------------- | ------------------------------------ |
| `repo:push` | `bitbucketCloud.repo:push` |
| `repo:updated` | `bitbucketCloud.repo:updated` |
| `pullrequest:created` | `bitbucketCloud.pullrequest:created` |
Please find all possible webhook event types at the
[official documentation](https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/).
## Installation
Install the [`events-backend` plugin](../events-backend/README.md).
Install this module:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-cloud
```
Add the event router to the `EventsBackend`:
```diff
+const bitbucketCloudEventRouter = new BitbucketCloudEventRouter();
EventsBackend
+ .addPublishers(bitbucketCloudEventRouter)
+ .addSubscribers(bitbucketCloudEventRouter);
// [...]
```
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-events-backend-module-bitbucket-cloud"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventParams } from '@backstage/plugin-events-node';
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
// @public
export class BitbucketCloudEventRouter extends SubTopicEventRouter {
constructor();
// (undocumented)
protected determineSubTopic(params: EventParams): string | undefined;
}
// @alpha
export const bitbucketCloudEventRouterEventsModule: (
options?: undefined,
) => BackendFeature;
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend-module-bitbucket-cloud",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"supertest": "^6.1.3"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
/**
* The module "bitbucket-cloud" for the Backstage backend plugin "events-backend"
* adding an event router for Bitbucket Cloud.
*
* @packageDocumentation
*/
export { BitbucketCloudEventRouter } from './router/BitbucketCloudEventRouter';
export { bitbucketCloudEventRouterEventsModule } from './service/BitbucketCloudEventRouterEventsModule';
@@ -0,0 +1,46 @@
/*
* 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter';
describe('BitbucketCloudEventRouter', () => {
const eventRouter = new BitbucketCloudEventRouter();
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);
eventRouter.onEvent({ topic, eventPayload });
expect(eventBroker.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);
});
});
@@ -0,0 +1,37 @@
/*
* 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 {
EventParams,
SubTopicEventRouter,
} from '@backstage/plugin-events-node';
/**
* Subscribes to the generic `bitbucketCloud` topic
* and publishes the events under the more concrete sub-topic
* depending on the `x-event-key` provided.
*
* @public
*/
export class BitbucketCloudEventRouter extends SubTopicEventRouter {
constructor() {
super('bitbucketCloud');
}
protected determineSubTopic(params: EventParams): string | undefined {
return params.metadata?.['x-event-key'] as string | undefined;
}
}
@@ -0,0 +1,46 @@
/*
* 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 { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { bitbucketCloudEventRouterEventsModule } from './BitbucketCloudEventRouterEventsModule';
import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter';
describe('bitbucketCloudEventRouterEventsModule', () => {
it('should be correctly wired and set up', async () => {
let addedPublisher: BitbucketCloudEventRouter | undefined;
let addedSubscriber: BitbucketCloudEventRouter | undefined;
const extensionPoint = {
addPublishers: (publisher: any) => {
addedPublisher = publisher;
},
addSubscribers: (subscriber: any) => {
addedSubscriber = subscriber;
},
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [],
features: [bitbucketCloudEventRouterEventsModule()],
});
expect(addedPublisher).not.toBeUndefined();
expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter);
expect(addedSubscriber).not.toBeUndefined();
expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter';
/**
* Module for the events-backend plugin, adding an event router for Bitbucket Cloud.
*
* Registers the {@link BitbucketCloudEventRouter}.
*
* @alpha
*/
export const bitbucketCloudEventRouterEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'bitbucketCloudEventRouter',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
const eventRouter = new BitbucketCloudEventRouter();
events.addPublishers(eventRouter);
events.addSubscribers(eventRouter);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,42 @@
# events-backend-module-gerrit
Welcome to the `events-backend-module-gerrit` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it with an `GerritEventRouter`.
The event router will subscribe to the topic `gerrit`
and route the events to more concrete topics based on the value
of the provided `$.type` payload field.
Examples:
| `$.type` | topic |
| ---------------- | ----------------------- |
| `change-created` | `gerrit.change-created` |
| `change-merged` | `gerrit.change-merged` |
Please find all possible webhook event types at the
[official documentation](https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events).
## Installation
Install the [`events-backend` plugin](../events-backend/README.md).
Install this module:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gerrit
```
Add the event router to the `EventsBackend`:
```diff
+const gerritEventRouter = new GerritEventRouter();
EventsBackend
+ .addPublishers(gerritEventRouter)
+ .addSubscribers(gerritEventRouter);
// [...]
```
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-events-backend-module-gerrit"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventParams } from '@backstage/plugin-events-node';
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
// @public
export class GerritEventRouter extends SubTopicEventRouter {
constructor();
// (undocumented)
protected determineSubTopic(params: EventParams): string | undefined;
}
// @alpha
export const gerritEventRouterEventsModule: (
options?: undefined,
) => BackendFeature;
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend-module-gerrit",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"supertest": "^6.1.3"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
/**
* The module `gerrit` for the Backstage backend plugin "events-backend"
* adding an event router for Gerrit.
*
* @packageDocumentation
*/
export { GerritEventRouter } from './router/GerritEventRouter';
export { gerritEventRouterEventsModule } from './service/GerritEventRouterEventsModule';
@@ -0,0 +1,50 @@
/*
* 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { GerritEventRouter } from './GerritEventRouter';
describe('GerritEventRouter', () => {
const eventRouter = new GerritEventRouter();
const topic = 'gerrit';
const eventPayload = { type: 'test-type', test: 'payload' };
const metadata = {};
it('no $.type', () => {
const eventBroker = new TestEventBroker();
eventRouter.setEventBroker(eventBroker);
eventRouter.onEvent({
topic,
eventPayload: { invalid: 'payload' },
metadata,
});
expect(eventBroker.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);
});
});
@@ -0,0 +1,42 @@
/*
* 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 {
EventParams,
SubTopicEventRouter,
} from '@backstage/plugin-events-node';
/**
* Subscribes to the generic `gerrit` topic
* and publishes the events under the more concrete sub-topic
* depending on the `$.type` field provided.
*
* @public
*/
export class GerritEventRouter extends SubTopicEventRouter {
constructor() {
super('gerrit');
}
protected determineSubTopic(params: EventParams): string | undefined {
if ('type' in (params.eventPayload as object)) {
const payload = params.eventPayload as { type: string };
return payload.type;
}
return undefined;
}
}
@@ -0,0 +1,46 @@
/*
* 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 { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { gerritEventRouterEventsModule } from './GerritEventRouterEventsModule';
import { GerritEventRouter } from '../router/GerritEventRouter';
describe('gerritEventRouterEventsModule', () => {
it('should be correctly wired and set up', async () => {
let addedPublisher: GerritEventRouter | undefined;
let addedSubscriber: GerritEventRouter | undefined;
const extensionPoint = {
addPublishers: (publisher: any) => {
addedPublisher = publisher;
},
addSubscribers: (subscriber: any) => {
addedSubscriber = subscriber;
},
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [],
features: [gerritEventRouterEventsModule()],
});
expect(addedPublisher).not.toBeUndefined();
expect(addedPublisher).toBeInstanceOf(GerritEventRouter);
expect(addedSubscriber).not.toBeUndefined();
expect(addedSubscriber).toBeInstanceOf(GerritEventRouter);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { GerritEventRouter } from '../router/GerritEventRouter';
/**
* Module for the events-backend plugin, adding an event router for Gerrit.
*
* Registers the {@link GerritEventRouter}.
*
* @alpha
*/
export const gerritEventRouterEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'gerritEventRouter',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
const eventRouter = new GerritEventRouter();
events.addPublishers(eventRouter);
events.addSubscribers(eventRouter);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,43 @@
# events-backend-module-github
Welcome to the `events-backend-module-github` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it with an `GithubEventRouter`.
The event router will subscribe to the topic `github`
and route the events to more concrete topics based on the value
of the provided `x-github-event` metadata field.
Examples:
| `x-github-event` | topic |
| ---------------- | --------------------- |
| `pull_request` | `github.pull_request` |
| `push` | `github.push` |
| `repository` | `github.repository` |
Please find all possible webhook event types at the
[official documentation](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads).
## Installation
Install the [`events-backend` plugin](../events-backend/README.md).
Install this module:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-github
```
Add the event router to the `EventsBackend`:
```diff
+const githubEventRouter = new GithubEventRouter();
EventsBackend
+ .addPublishers(githubEventRouter)
+ .addSubscribers(githubEventRouter);
// [...]
```
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-events-backend-module-github"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventParams } from '@backstage/plugin-events-node';
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
// @public
export class GithubEventRouter extends SubTopicEventRouter {
constructor();
// (undocumented)
protected determineSubTopic(params: EventParams): string | undefined;
}
// @alpha
export const githubEventRouterEventsModule: (
options?: undefined,
) => BackendFeature;
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend-module-github",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"supertest": "^6.1.3"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
/**
* The module `github` for the Backstage backend plugin "events-backend"
* adding an event router for GitHub.
*
* @packageDocumentation
*/
export { GithubEventRouter } from './router/GithubEventRouter';
export { githubEventRouterEventsModule } from './service/GithubEventRouterEventsModule';
@@ -0,0 +1,46 @@
/*
* 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { GithubEventRouter } from './GithubEventRouter';
describe('GithubEventRouter', () => {
const eventRouter = new GithubEventRouter();
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);
eventRouter.onEvent({ topic, eventPayload });
expect(eventBroker.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);
});
});
@@ -0,0 +1,37 @@
/*
* 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 {
EventParams,
SubTopicEventRouter,
} from '@backstage/plugin-events-node';
/**
* Subscribes to the generic `github` topic
* and publishes the events under the more concrete sub-topic
* depending on the `x-github-event` provided.
*
* @public
*/
export class GithubEventRouter extends SubTopicEventRouter {
constructor() {
super('github');
}
protected determineSubTopic(params: EventParams): string | undefined {
return params.metadata?.['x-github-event'] as string | undefined;
}
}
@@ -0,0 +1,46 @@
/*
* 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 { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { githubEventRouterEventsModule } from './GithubEventRouterEventsModule';
import { GithubEventRouter } from '../router/GithubEventRouter';
describe('githubEventRouterEventsModule', () => {
it('should be correctly wired and set up', async () => {
let addedPublisher: GithubEventRouter | undefined;
let addedSubscriber: GithubEventRouter | undefined;
const extensionPoint = {
addPublishers: (publisher: any) => {
addedPublisher = publisher;
},
addSubscribers: (subscriber: any) => {
addedSubscriber = subscriber;
},
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [],
features: [githubEventRouterEventsModule()],
});
expect(addedPublisher).not.toBeUndefined();
expect(addedPublisher).toBeInstanceOf(GithubEventRouter);
expect(addedSubscriber).not.toBeUndefined();
expect(addedSubscriber).toBeInstanceOf(GithubEventRouter);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { GithubEventRouter } from '../router/GithubEventRouter';
/**
* Module for the events-backend plugin, adding an event router for GitHub.
*
* Registers the {@link GithubEventRouter}.
*
* @alpha
*/
export const githubEventRouterEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'githubEventRouter',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
const eventRouter = new GithubEventRouter();
events.addPublishers(eventRouter);
events.addSubscribers(eventRouter);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,42 @@
# events-backend-module-gitlab
Welcome to the `events-backend-module-gitlab` backend plugin!
This plugin is a module for the `events-backend` backend plugin
and extends it with an `GitlabEventRouter`.
The event router will subscribe to the topic `gitlab`
and route the events to more concrete topics based on the value
of the provided `$.event_name` payload field.
Examples:
| `$.event_name` | topic |
| --------------- | ---------------------- |
| `push` | `gitlab.push` |
| `merge_request` | `gitlab.merge_request` |
Please find all possible webhook event types at the
[official documentation](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html).
## Installation
Install the [`events-backend` plugin](../events-backend/README.md).
Install this module:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gitlab
```
Add the event router to the `EventsBackend`:
```diff
+const gitlabEventRouter = new GitlabEventRouter();
EventsBackend
+ .addPublishers(gitlabEventRouter)
+ .addSubscribers(gitlabEventRouter);
// [...]
```
@@ -0,0 +1,21 @@
## API Report File for "@backstage/plugin-events-backend-module-gitlab"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventParams } from '@backstage/plugin-events-node';
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
// @public
export class GitlabEventRouter extends SubTopicEventRouter {
constructor();
// (undocumented)
protected determineSubTopic(params: EventParams): string | undefined;
}
// @alpha
export const gitlabEventRouterEventsModule: (
options?: undefined,
) => BackendFeature;
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend-module-gitlab",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"supertest": "^6.1.3"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/
/**
* The module "gitlab" for the Backstage backend plugin "events-backend"
* adding an event router for GitLab.
*
* @packageDocumentation
*/
export { GitlabEventRouter } from './router/GitlabEventRouter';
export { gitlabEventRouterEventsModule } from './service/GitlabEventRouterEventsModule';
@@ -0,0 +1,50 @@
/*
* 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import { GitlabEventRouter } from './GitlabEventRouter';
describe('GitlabEventRouter', () => {
const eventRouter = new GitlabEventRouter();
const topic = 'gitlab';
const eventPayload = { event_name: 'test_type', test: 'payload' };
const metadata = {};
it('no $.event_name', () => {
const eventBroker = new TestEventBroker();
eventRouter.setEventBroker(eventBroker);
eventRouter.onEvent({
topic,
eventPayload: { invalid: 'payload' },
metadata,
});
expect(eventBroker.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);
});
});
@@ -0,0 +1,42 @@
/*
* 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 {
EventParams,
SubTopicEventRouter,
} from '@backstage/plugin-events-node';
/**
* Subscribes to the generic `gitlab` topic
* and publishes the events under the more concrete sub-topic
* depending on the `$.event_name` field provided.
*
* @public
*/
export class GitlabEventRouter extends SubTopicEventRouter {
constructor() {
super('gitlab');
}
protected determineSubTopic(params: EventParams): string | undefined {
if ('event_name' in (params.eventPayload as object)) {
const payload = params.eventPayload as { event_name: string };
return payload.event_name;
}
return undefined;
}
}
@@ -0,0 +1,46 @@
/*
* 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 { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { gitlabEventRouterEventsModule } from './GitlabEventRouterEventsModule';
import { GitlabEventRouter } from '../router/GitlabEventRouter';
describe('gitlabEventRouterEventsModule', () => {
it('should be correctly wired and set up', async () => {
let addedPublisher: GitlabEventRouter | undefined;
let addedSubscriber: GitlabEventRouter | undefined;
const extensionPoint = {
addPublishers: (publisher: any) => {
addedPublisher = publisher;
},
addSubscribers: (subscriber: any) => {
addedSubscriber = subscriber;
},
};
await startTestBackend({
extensionPoints: [[eventsExtensionPoint, extensionPoint]],
services: [],
features: [gitlabEventRouterEventsModule()],
});
expect(addedPublisher).not.toBeUndefined();
expect(addedPublisher).toBeInstanceOf(GitlabEventRouter);
expect(addedSubscriber).not.toBeUndefined();
expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { GitlabEventRouter } from '../router/GitlabEventRouter';
/**
* Module for the events-backend plugin, adding an event router for GitLab.
*
* Registers the {@link GitlabEventRouter}.
*
* @alpha
*/
export const gitlabEventRouterEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'gitlabEventRouter',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
const eventRouter = new GitlabEventRouter();
events.addPublishers(eventRouter);
events.addSubscribers(eventRouter);
},
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,4 @@
# plugin-events-backend-test-utils
Houses implementations of plugin-events-node interfaces
which can be useful for test for events-backend and its modules.
@@ -0,0 +1,47 @@
## API Report File for "@backstage/plugin-events-backend-test-utils"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { EventBroker } from '@backstage/plugin-events-node';
import { EventParams } from '@backstage/plugin-events-node';
import { EventPublisher } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
// @public (undocumented)
export class TestEventBroker implements EventBroker {
// (undocumented)
publish(params: EventParams): Promise<void>;
// (undocumented)
readonly published: EventParams[];
// (undocumented)
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
// (undocumented)
readonly subscribed: EventSubscriber[];
}
// @public (undocumented)
export class TestEventPublisher implements EventPublisher {
// (undocumented)
get eventBroker(): EventBroker | undefined;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
// @public (undocumented)
export class TestEventSubscriber implements EventSubscriber {
constructor(name: string, topics: string[]);
// (undocumented)
readonly name: string;
// (undocumented)
onEvent(params: EventParams): Promise<void>;
// (undocumented)
readonly receivedEvents: Record<string, EventParams[]>;
// (undocumented)
supportsEventTopics(): string[];
// (undocumented)
readonly topics: string[];
}
```
@@ -0,0 +1,34 @@
{
"name": "@backstage/plugin-events-backend-test-utils",
"description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/plugin-events-node": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
]
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* The events-test-utils module for `@backstage/plugin-events-node`.
*
* @packageDocumentation
*/
export * from './testUtils';
@@ -0,0 +1,37 @@
/*
* 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 {
EventBroker,
EventParams,
EventSubscriber,
} from '@backstage/plugin-events-node';
/** @public */
export class TestEventBroker implements EventBroker {
readonly published: EventParams[] = [];
readonly subscribed: EventSubscriber[] = [];
async publish(params: EventParams): Promise<void> {
this.published.push(params);
}
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void {
this.subscribed.push(...subscribers.flat());
}
}
@@ -0,0 +1,30 @@
/*
* 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 { EventBroker, EventPublisher } from '@backstage/plugin-events-node';
/** @public */
export class TestEventPublisher implements EventPublisher {
#eventBroker?: EventBroker;
async setEventBroker(eventBroker: EventBroker): Promise<void> {
this.#eventBroker = eventBroker;
}
get eventBroker() {
return this.#eventBroker;
}
}
@@ -0,0 +1,39 @@
/*
* 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 { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
/** @public */
export class TestEventSubscriber implements EventSubscriber {
readonly name: string;
readonly topics: string[];
readonly receivedEvents: Record<string, EventParams[]> = {};
constructor(name: string, topics: string[]) {
this.name = name;
this.topics = topics;
}
supportsEventTopics(): string[] {
return this.topics;
}
async onEvent(params: EventParams): Promise<void> {
this.receivedEvents[params.topic] = this.receivedEvents[params.topic] ?? [];
this.receivedEvents[params.topic].push(params);
}
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { TestEventBroker } from './TestEventBroker';
export { TestEventPublisher } from './TestEventPublisher';
export { TestEventSubscriber } from './TestEventSubscriber';

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