fix(events,catalog,bitbucket-cloud)!: fix new backend system support; migrates to EventsService

- Fixes the support for the new backend system that was broken entirely
  (with and without events support).
- Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`.

Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2024-01-23 22:44:32 +01:00
parent 1ab76e523d
commit 9e527c9200
9 changed files with 141 additions and 73 deletions
+40
View File
@@ -0,0 +1,40 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor
---
BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support.
`BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`.
With provided `events`, the event-based updates/refresh will be available.
However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`.
The event subscription happens on `connect(connection)` if the `events` is available.
**Migration:**
```diff
const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig(
env.config,
{
catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
+ events: env.events,
logger: env.logger,
scheduler: env.scheduler,
tokenManager: env.tokenManager,
},
);
- env.eventBroker.subscribe(bitbucketCloudProvider);
```
**New Backend System:**
Before this change, using this module with the new backend system was broken.
Now, you can add the catalog module for Bitbucket Cloud incl. event support backend.
Event support will always be enabled.
However, no updates/refresh will happen without receiving events.
```ts
backend.add(
import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'),
);
```
+31 -2
View File
@@ -24,7 +24,35 @@ package.
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud
```
### Installation without Events Support
### Installation with New Backend System
```ts
// optional if you want HTTP endpojnts to receive external events
// backend.add(import('@backstage/plugin-events-backend/alpha'));
// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha'));
backend.add(
import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'),
);
backend.add(
import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'),
);
```
You need to decide how you want to receive events from external sources like
- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md)
- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md)
Further documentation:
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md>
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md>
### Installation with Legacy Backend System
#### Installation without Events Support
And then add the entity provider to your catalog builder:
@@ -49,7 +77,7 @@ export default async function createPlugin(
}
```
### Installation with Events Support
#### Installation with Events Support
Please follow the installation instructions at
@@ -83,6 +111,7 @@ export default async function createPlugin(
env.config,
{
catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
events: env.events,
logger: env.logger,
scheduler: env.scheduler,
tokenManager: env.tokenManager,
@@ -7,18 +7,15 @@ import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { EventParams } from '@backstage/plugin-events-node';
import { Events } from '@backstage/plugin-bitbucket-cloud-common';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
import { EventsService } from '@backstage/plugin-events-node';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { TaskRunner } from '@backstage/backend-tasks';
import { TokenManager } from '@backstage/backend-common';
// @public
export class BitbucketCloudEntityProvider
implements EntityProvider, EventSubscriber
{
export class BitbucketCloudEntityProvider implements EntityProvider {
// (undocumented)
connect(connection: EntityProviderConnection): Promise<void>;
// (undocumented)
@@ -26,7 +23,8 @@ export class BitbucketCloudEntityProvider
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
events?: EventsService;
logger: LoggerService;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
@@ -37,12 +35,8 @@ export class BitbucketCloudEntityProvider
// (undocumented)
getTaskId(): string;
// (undocumented)
onEvent(params: EventParams): Promise<void>;
// (undocumented)
onRepoPush(event: Events.RepoPushEvent): Promise<void>;
// (undocumented)
refresh(logger: Logger): Promise<void>;
// (undocumented)
supportsEventTopics(): string[];
refresh(logger: LoggerService): Promise<void>;
}
```
@@ -56,13 +56,13 @@
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"uuid": "^9.0.0",
"winston": "^3.2.1"
"uuid": "^9.0.0"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^",
"luxon": "^3.0.0",
"msw": "^1.0.0"
},
@@ -14,18 +14,28 @@
* limitations under the License.
*/
import { createServiceFactory } from '@backstage/backend-plugin-api';
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { startTestBackend, mockServices } from '@backstage/backend-test-utils';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha';
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { Duration } from 'luxon';
import { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider';
import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider';
describe('catalogModuleBitbucketCloudEntityProvider', () => {
it('should register provider at the catalog extension point', async () => {
const events = new TestEventsService();
const eventsServiceFactory = createServiceFactory({
service: eventsServiceRef,
deps: {},
async factory({}) {
return events;
},
});
let addedProviders: Array<BitbucketCloudEntityProvider> | undefined;
let addedSubscribers: Array<BitbucketCloudEntityProvider> | undefined;
let usedSchedule: TaskScheduleDefinition | undefined;
const catalogExtensionPointImpl = {
@@ -33,11 +43,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => {
addedProviders = providers;
},
};
const eventsExtensionPointImpl = {
addSubscribers: (subscribers: any) => {
addedSubscribers = subscribers;
},
};
const connection = jest.fn() as unknown as EntityProviderConnection;
const runner = jest.fn();
const scheduler = mockServices.scheduler.mock({
createScheduledTaskRunner(schedule) {
@@ -49,9 +55,9 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => {
await startTestBackend({
extensionPoints: [
[catalogProcessingExtensionPoint, catalogExtensionPointImpl],
[eventsExtensionPoint, eventsExtensionPointImpl],
],
features: [
eventsServiceFactory(),
catalogModuleBitbucketCloudEntityProvider(),
mockServices.rootConfig.factory({
data: {
@@ -75,10 +81,14 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => {
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
expect(runner).not.toHaveBeenCalled();
const provider = addedProviders!.pop()!;
expect(provider.getProviderName()).toEqual(
'bitbucketCloud-provider:default',
);
expect(addedSubscribers).toEqual(addedProviders);
expect(runner).not.toHaveBeenCalled();
await provider.connect(connection);
expect(events.subscribed).toHaveLength(1);
expect(events.subscribed[0].id).toEqual('bitbucketCloud-provider:default');
expect(runner).toHaveBeenCalledTimes(1);
});
});
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { loggerToWinstonLogger } from '@backstage/backend-common';
import {
coreServices,
createBackendModule,
@@ -23,7 +22,7 @@ import {
catalogProcessingExtensionPoint,
catalogServiceRef,
} from '@backstage/plugin-catalog-node/alpha';
import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider';
/**
@@ -38,9 +37,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({
catalog: catalogProcessingExtensionPoint,
catalogApi: catalogServiceRef,
config: coreServices.rootConfig,
// TODO(pjungermann): How to make this optional for those which only want the provider without event support?
// Do we even want to support this?
events: eventsExtensionPoint,
events: eventsServiceRef,
logger: coreServices.logger,
scheduler: coreServices.scheduler,
tokenManager: coreServices.tokenManager,
@@ -54,16 +51,15 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({
scheduler,
tokenManager,
}) {
const winstonLogger = loggerToWinstonLogger(logger);
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
catalogApi,
logger: winstonLogger,
events,
logger,
scheduler,
tokenManager,
});
catalog.addEntityProvider(providers);
events.addSubscribers(providers);
},
});
},
@@ -29,6 +29,7 @@ import {
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-node';
import { Events } from '@backstage/plugin-bitbucket-cloud-common';
import { DefaultEventsService } from '@backstage/plugin-events-node';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import {
@@ -436,6 +437,7 @@ describe('BitbucketCloudEntityProvider', () => {
'added-module/catalog-custom.yaml',
);
const events = DefaultEventsService.create({ logger });
const catalogApi = {
getEntities: async (
request: { filter: Record<string, string> },
@@ -457,6 +459,7 @@ describe('BitbucketCloudEntityProvider', () => {
};
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
catalogApi: catalogApi as any as CatalogApi,
events,
logger,
schedule,
tokenManager,
@@ -537,7 +540,7 @@ describe('BitbucketCloudEntityProvider', () => {
);
await provider.connect(entityProviderConnection);
await provider.onEvent(repoPushEventParams);
await events.publish(repoPushEventParams);
const addedEntities = [
{
@@ -566,31 +569,22 @@ describe('BitbucketCloudEntityProvider', () => {
});
});
it('onRepoPush fail on incomplete setup', async () => {
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
logger,
schedule,
})[0];
await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow(
'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.',
);
});
it('no onRepoPush update on non-matching workspace slug', async () => {
const catalogApi = {
getEntities: jest.fn(),
refreshEntity: jest.fn(),
};
const events = DefaultEventsService.create({ logger });
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
catalogApi: catalogApi as any as CatalogApi,
events,
logger,
schedule,
tokenManager,
})[0];
await provider.connect(entityProviderConnection);
await provider.onEvent({
await events.publish({
...repoPushEventParams,
eventPayload: {
...repoPushEventParams.eventPayload,
@@ -613,15 +607,17 @@ describe('BitbucketCloudEntityProvider', () => {
getEntities: jest.fn(),
refreshEntity: jest.fn(),
};
const events = DefaultEventsService.create({ logger });
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
catalogApi: catalogApi as any as CatalogApi,
events,
logger,
schedule,
tokenManager,
})[0];
await provider.connect(entityProviderConnection);
await provider.onEvent({
await events.publish({
...repoPushEventParams,
eventPayload: {
...repoPushEventParams.eventPayload,
@@ -15,6 +15,7 @@
*/
import { TokenManager } from '@backstage/backend-common';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
import { CatalogApi } from '@backstage/catalog-client';
import { LocationEntity } from '@backstage/catalog-model';
@@ -35,13 +36,12 @@ import {
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
import { EventsService } from '@backstage/plugin-events-node';
import {
BitbucketCloudEntityProviderConfig,
readProviderConfigs,
} from './BitbucketCloudEntityProviderConfig';
import * as uuid from 'uuid';
import { Logger } from 'winston';
const DEFAULT_BRANCH = 'master';
const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push';
@@ -62,14 +62,13 @@ interface IngestionTarget {
*
* @public
*/
export class BitbucketCloudEntityProvider
implements EntityProvider, EventSubscriber
{
export class BitbucketCloudEntityProvider implements EntityProvider {
private readonly client: BitbucketCloudClient;
private readonly config: BitbucketCloudEntityProviderConfig;
private readonly logger: Logger;
private readonly logger: LoggerService;
private readonly scheduleFn: () => Promise<void>;
private readonly catalogApi?: CatalogApi;
private readonly events?: EventsService;
private readonly tokenManager?: TokenManager;
private connection?: EntityProviderConnection;
@@ -79,7 +78,8 @@ export class BitbucketCloudEntityProvider
config: Config,
options: {
catalogApi?: CatalogApi;
logger: Logger;
events?: EventsService;
logger: LoggerService;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
tokenManager?: TokenManager;
@@ -114,6 +114,7 @@ export class BitbucketCloudEntityProvider
options.logger,
taskRunner,
options.catalogApi,
options.events,
options.tokenManager,
);
});
@@ -122,9 +123,10 @@ export class BitbucketCloudEntityProvider
private constructor(
config: BitbucketCloudEntityProviderConfig,
integration: BitbucketCloudIntegration,
logger: Logger,
logger: LoggerService,
taskRunner: TaskRunner,
catalogApi?: CatalogApi,
events?: EventsService,
tokenManager?: TokenManager,
) {
this.client = BitbucketCloudClient.fromConfig(integration.config);
@@ -134,6 +136,7 @@ export class BitbucketCloudEntityProvider
});
this.scheduleFn = this.createScheduleFn(taskRunner);
this.catalogApi = catalogApi;
this.events = events;
this.tokenManager = tokenManager;
}
@@ -176,9 +179,23 @@ export class BitbucketCloudEntityProvider
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.scheduleFn();
if (this.events) {
await this.events.subscribe({
id: this.getProviderName(),
topics: [TOPIC_REPO_PUSH],
onEvent: async params => {
if (params.topic !== TOPIC_REPO_PUSH) {
return;
}
await this.onRepoPush(params.eventPayload as Events.RepoPushEvent);
},
});
}
}
async refresh(logger: Logger) {
async refresh(logger: LoggerService) {
if (!this.connection) {
throw new Error('Not initialized');
}
@@ -198,20 +215,6 @@ export class BitbucketCloudEntityProvider
);
}
/** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */
supportsEventTopics(): string[] {
return [TOPIC_REPO_PUSH];
}
/** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */
async onEvent(params: EventParams): Promise<void> {
if (params.topic !== TOPIC_REPO_PUSH) {
return;
}
await this.onRepoPush(params.eventPayload as Events.RepoPushEvent);
}
private canHandleEvents(): boolean {
if (this.catalogApi && this.tokenManager) {
return true;
+1 -1
View File
@@ -5303,11 +5303,11 @@ __metadata:
"@backstage/plugin-bitbucket-cloud-common": "workspace:^"
"@backstage/plugin-catalog-common": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/plugin-events-backend-test-utils": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
luxon: ^3.0.0
msw: ^1.0.0
uuid: ^9.0.0
winston: ^3.2.1
languageName: unknown
linkType: soft