fix!: fix event support for Bitbucket Cloud

Fixes the event-based updates at `BitbucketCloudEntityProvider`.

Previously, this entity provider had optional event support for legacy backends
that could be enabled by passing `catalogApi`, `events`, and `tokenManager`.

For the new/current backend system, the `catalogModuleBitbucketCloudEntityProvider`
(`catalog.bitbucket-cloud-entity-provider`), event support was enabled by default.

A recent change removed `tokenManager` as a dependency from the module as well as removed it as input.
While this didn't break the instantiation of the module, it broke the event-based updates
and led to a runtime misbehavior, accompanied by an info log message.

This change will replace the use of `tokenManager` with the use of `auth` (`AuthService`).

Additionally, it will make `catalogApi` and `events` required dependencies.
For the current backend system, this change is transparent and doesn't require any action.
For the legacy backend system, this change will require you to pass those dependencies if you didn't do it already.

**BREAKING CHANGES:**

> _(For legacy backend users only.)_
>
>  Previously optional `catalogApi`, and `events` are required now.
>  A new required dependency `auth` was added.

Fixes: #26925
Relates-to: PR #26141
Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2024-10-01 21:18:23 +02:00
parent 1b05958bb1
commit 6343c8d915
8 changed files with 135 additions and 105 deletions
+29
View File
@@ -0,0 +1,29 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor
---
Fixes the event-based updates at `BitbucketCloudEntityProvider`.
Previously, this entity provider had optional event support for legacy backends
that could be enabled by passing `catalogApi`, `events`, and `tokenManager`.
For the new/current backend system, the `catalogModuleBitbucketCloudEntityProvider`
(`catalog.bitbucket-cloud-entity-provider`), event support was enabled by default.
A recent change removed `tokenManager` as a dependency from the module as well as removed it as input.
While this didn't break the instantiation of the module, it broke the event-based updates,
and led to a runtime misbehavior, accompanied by an info log message.
This change will replace the use of `tokenManager` with the use of `auth` (`AuthService`).
Additionally, to simplify, it will make `catalogApi` and `events` required dependencies.
For the current backend system, this change is transparent and doesn't require any action.
For the legacy backend system, this change will require you to pass those dependencies
if you didn't do it already.
BREAKING CHANGES:
_(For legacy backend users only.)_
Previously optional `catalogApi`, and `events` are required now.
A new required dependency `auth` was added.
+1 -30
View File
@@ -51,33 +51,6 @@ Further documentation:
### Installation with Legacy Backend System
#### Installation without Events Support
And then add the entity provider to your catalog builder:
```ts title="packages/backend/src/plugins/catalog.ts"
/* highlight-add-next-line */
import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
/* highlight-add-start */
builder.addEntityProvider(
BitbucketCloudEntityProvider.fromConfig(env.config, {
logger: env.logger,
scheduler: env.scheduler,
}),
);
/* highlight-add-end */
// ..
}
```
#### Installation with Events Support
Please follow the installation instructions at
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
@@ -104,19 +77,17 @@ export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
/* highlight-add-start */
const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig(
env.config,
{
auth: env.auth,
catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
events: env.events,
logger: env.logger,
scheduler: env.scheduler,
tokenManager: env.tokenManager,
},
);
env.eventBroker.subscribe(bitbucketCloudProvider);
builder.addEntityProvider(bitbucketCloudProvider);
/* highlight-add-end */
const { processingEngine, router } = await builder.build();
@@ -51,7 +51,6 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-node';
@@ -12,7 +13,6 @@ import { EventsService } from '@backstage/plugin-events-node';
import { LoggerService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
import { TokenManager } from '@backstage/backend-common';
// @public
export class BitbucketCloudEntityProvider implements EntityProvider {
@@ -21,12 +21,12 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
events?: EventsService;
auth: AuthService;
catalogApi: CatalogApi;
events: EventsService;
logger: LoggerService;
schedule?: SchedulerServiceTaskRunner;
scheduler?: SchedulerService;
tokenManager?: TokenManager;
},
): BitbucketCloudEntityProvider[];
getProviderName(): string;
@@ -39,7 +39,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
// Warnings were encountered during analysis:
//
// src/providers/BitbucketCloudEntityProvider.d.ts:28:5 - (ae-undocumented) Missing documentation for "fromConfig".
// src/providers/BitbucketCloudEntityProvider.d.ts:44:5 - (ae-undocumented) Missing documentation for "refresh".
// src/providers/BitbucketCloudEntityProvider.d.ts:47:5 - (ae-undocumented) Missing documentation for "onRepoPush".
// src/providers/BitbucketCloudEntityProvider.d.ts:26:5 - (ae-undocumented) Missing documentation for "fromConfig".
// src/providers/BitbucketCloudEntityProvider.d.ts:42:5 - (ae-undocumented) Missing documentation for "refresh".
// src/providers/BitbucketCloudEntityProvider.d.ts:44:5 - (ae-undocumented) Missing documentation for "onRepoPush".
```
@@ -34,6 +34,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({
register(env) {
env.registerInit({
deps: {
auth: coreServices.auth,
catalog: catalogProcessingExtensionPoint,
catalogApi: catalogServiceRef,
config: coreServices.rootConfig,
@@ -41,8 +42,17 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({
logger: coreServices.logger,
scheduler: coreServices.scheduler,
},
async init({ catalog, catalogApi, config, events, logger, scheduler }) {
async init({
auth,
catalog,
catalogApi,
config,
events,
logger,
scheduler,
}) {
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
auth,
catalogApi,
events,
logger,
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import {
SchedulerServiceTaskInvocationDefinition,
SchedulerServiceTaskRunner,
@@ -92,11 +91,6 @@ describe('BitbucketCloudEntityProvider', () => {
applyMutation: jest.fn(),
refresh: jest.fn(),
};
const tokenManager = {
getToken: async () => {
return { token: 'fake-token' };
},
} as any as TokenManager;
const repoPushEvent: Events.RepoPushEvent = {
actor: {
type: 'user',
@@ -158,8 +152,14 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('no provider config', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const config = new ConfigReader({});
const events = DefaultEventsService.create({ logger });
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
auth,
catalogApi,
events,
logger,
schedule,
});
@@ -168,7 +168,13 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('single simple provider config', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const providers = BitbucketCloudEntityProvider.fromConfig(simpleConfig, {
auth,
catalogApi,
events,
logger,
schedule,
});
@@ -180,14 +186,24 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('fail without schedule and scheduler', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
expect(() =>
BitbucketCloudEntityProvider.fromConfig(simpleConfig, {
auth,
catalogApi,
events,
logger,
}),
).toThrow('Either schedule or scheduler must be provided.');
});
it('fail with scheduler but no schedule config', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const scheduler = mockServices.scheduler.mock();
const config = new ConfigReader({
catalog: {
@@ -201,6 +217,9 @@ describe('BitbucketCloudEntityProvider', () => {
expect(() =>
BitbucketCloudEntityProvider.fromConfig(config, {
auth,
catalogApi,
events,
logger,
scheduler,
}),
@@ -210,6 +229,9 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('single simple provider config with schedule in config', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const scheduler = mockServices.scheduler.mock();
const config = new ConfigReader({
catalog: {
@@ -226,6 +248,9 @@ describe('BitbucketCloudEntityProvider', () => {
});
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
auth,
catalogApi,
events,
logger,
scheduler,
});
@@ -237,6 +262,8 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('multiple provider configs', () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const config = new ConfigReader({
catalog: {
providers: {
@@ -251,7 +278,11 @@ describe('BitbucketCloudEntityProvider', () => {
},
},
});
const events = DefaultEventsService.create({ logger });
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
auth,
catalogApi,
events,
logger,
schedule,
});
@@ -266,7 +297,13 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('apply full update on scheduled execution', async () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
auth,
catalogApi,
events,
logger,
schedule,
})[0];
@@ -436,6 +473,9 @@ describe('BitbucketCloudEntityProvider', () => {
'added-module/catalog-custom.yaml',
);
const auth = mockServices.auth.mock({
getPluginRequestToken: async () => ({ token: 'fake-token' }),
});
const events = DefaultEventsService.create({ logger });
const catalogApi = catalogServiceMock.mock({
getEntities: async (
@@ -457,11 +497,11 @@ describe('BitbucketCloudEntityProvider', () => {
},
});
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
auth,
catalogApi,
events,
logger,
schedule,
tokenManager,
})[0];
server.use(
@@ -569,14 +609,15 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('no onRepoPush update on non-matching workspace slug', async () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
auth,
catalogApi,
events,
logger,
schedule,
tokenManager,
})[0];
await provider.connect(entityProviderConnection);
@@ -599,14 +640,15 @@ describe('BitbucketCloudEntityProvider', () => {
});
it('no onRepoPush update on non-matching repo slug', async () => {
const auth = mockServices.auth.mock();
const catalogApi = catalogServiceMock.mock();
const events = DefaultEventsService.create({ logger });
const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, {
auth,
catalogApi,
events,
logger,
schedule,
tokenManager,
})[0];
await provider.connect(entityProviderConnection);
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import {
AuthService,
LoggerService,
SchedulerService,
SchedulerServiceTaskRunner,
@@ -66,26 +66,25 @@ interface IngestionTarget {
* @public
*/
export class BitbucketCloudEntityProvider implements EntityProvider {
private readonly auth: AuthService;
private readonly catalogApi: CatalogApi;
private readonly client: BitbucketCloudClient;
private readonly config: BitbucketCloudEntityProviderConfig;
private readonly events: EventsService;
private readonly logger: LoggerService;
private readonly scheduleFn: () => Promise<void>;
private readonly catalogApi?: CatalogApi;
private readonly events?: EventsService;
private readonly tokenManager?: TokenManager;
private connection?: EntityProviderConnection;
private eventConfigErrorThrown = false;
private connection?: EntityProviderConnection;
static fromConfig(
config: Config,
options: {
catalogApi?: CatalogApi;
events?: EventsService;
auth: AuthService;
catalogApi: CatalogApi;
events: EventsService;
logger: LoggerService;
schedule?: SchedulerServiceTaskRunner;
scheduler?: SchedulerService;
tokenManager?: TokenManager;
},
): BitbucketCloudEntityProvider[] {
const integrations = ScmIntegrations.fromConfig(config);
@@ -112,35 +111,35 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
return new BitbucketCloudEntityProvider(
options.auth,
options.catalogApi,
providerConfig,
options.events,
integration,
options.logger,
taskRunner,
options.catalogApi,
options.events,
options.tokenManager,
);
});
}
private constructor(
auth: AuthService,
catalogApi: CatalogApi,
config: BitbucketCloudEntityProviderConfig,
events: EventsService,
integration: BitbucketCloudIntegration,
logger: LoggerService,
taskRunner: SchedulerServiceTaskRunner,
catalogApi?: CatalogApi,
events?: EventsService,
tokenManager?: TokenManager,
) {
this.auth = auth;
this.catalogApi = catalogApi;
this.client = BitbucketCloudClient.fromConfig(integration.config);
this.config = config;
this.events = events;
this.logger = logger.child({
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(taskRunner);
this.catalogApi = catalogApi;
this.events = events;
this.tokenManager = tokenManager;
}
private createScheduleFn(
@@ -185,19 +184,17 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
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.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);
},
});
}
await this.onRepoPush(params.eventPayload as Events.RepoPushEvent);
},
});
}
async refresh(logger: LoggerService) {
@@ -220,32 +217,12 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
);
}
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;
}
private enhanceEvent(event: Events.RepoPushEvent): void {
// add missing slug
event.repository.slug = event.repository.full_name!.split('/', 2)[1];
}
async onRepoPush(event: Events.RepoPushEvent): Promise<void> {
if (!this.canHandleEvents()) {
return;
}
if (!this.connection) {
throw new Error('Not initialized');
}
@@ -273,8 +250,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
const targets = await this.findCatalogFiles(repoSlug);
const { token } = await this.tokenManager!.getToken();
const existing = await this.findExistingLocations(repoUrl, token);
const existing = await this.findExistingLocations(repoUrl);
const added: DeferredEntity[] = this.toDeferredEntities(
targets.filter(
@@ -321,16 +297,20 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
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[]>;
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
return this.catalogApi
.getEntities({ filter }, { token })
.then(result => result.items) as Promise<LocationEntity[]>;
}
private async findCatalogFiles(
-1
View File
@@ -5581,7 +5581,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-bitbucket-cloud@workspace:plugins/catalog-backend-module-bitbucket-cloud"
dependencies:
"@backstage/backend-common": ^0.25.0
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"