Merge pull request #19633 from davelil4/bitbucket-server-events
Bitbucket Server Catalog Plugin Event Support
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-server': minor
|
||||
'@backstage/plugin-events-backend-module-bitbucket-server': minor
|
||||
---
|
||||
|
||||
Added the ability for the plugin to receive events coming from Bitbucket Server push webhooks. It then performs a delta mutation on the catalog.
|
||||
@@ -26,17 +26,30 @@ dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to you
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server
|
||||
```
|
||||
|
||||
And update your backend by adding the following line:
|
||||
### Installation with New Backend System
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
```ts
|
||||
// optional if you want HTTP endpojnts to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend'));
|
||||
// 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'));
|
||||
backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
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-server/README.md>
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md).
|
||||
|
||||
@@ -21,16 +21,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
@@ -51,17 +47,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"p-throttle": "^4.1.1",
|
||||
"uuid": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^",
|
||||
"luxon": "^3.0.0",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
## API Report File for "@backstage/plugin-catalog-backend-module-bitbucket-server"
|
||||
|
||||
> 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';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _feature: BackendFeature;
|
||||
export default _feature;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -3,13 +3,16 @@
|
||||
> 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 { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerIntegrationConfig } from '@backstage/integration';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-node';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
|
||||
@@ -22,6 +25,11 @@ export class BitbucketServerClient {
|
||||
config: BitbucketServerIntegrationConfig;
|
||||
}): BitbucketServerClient;
|
||||
// (undocumented)
|
||||
getDefaultBranch(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
}): Promise<BitbucketServerDefaultBranch>;
|
||||
// (undocumented)
|
||||
getFile(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
@@ -47,6 +55,16 @@ export class BitbucketServerClient {
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BitbucketServerDefaultBranch = {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: string;
|
||||
latestCommit: string;
|
||||
latestChangeset: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
@@ -55,9 +73,12 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
events?: EventsService;
|
||||
parser?: BitbucketServerLocationParser;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
catalogApi?: CatalogService;
|
||||
auth?: AuthService;
|
||||
},
|
||||
): BitbucketServerEntityProvider[];
|
||||
getProviderName(): string;
|
||||
@@ -108,6 +129,7 @@ export type BitbucketServerRepository = {
|
||||
}[]
|
||||
>;
|
||||
archived: boolean;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
*/
|
||||
|
||||
export { default } from './module';
|
||||
|
||||
export { BitbucketServerClient } from './lib';
|
||||
|
||||
export type {
|
||||
BitbucketServerProject,
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerPagedResponse,
|
||||
BitbucketServerListOptions,
|
||||
BitbucketServerDefaultBranch,
|
||||
} from './lib';
|
||||
|
||||
export { BitbucketServerEntityProvider } from './providers';
|
||||
export type { BitbucketServerLocationParser } from './providers';
|
||||
|
||||
+86
-1
@@ -23,7 +23,12 @@ import {
|
||||
BitbucketServerPagedResponse,
|
||||
paginated,
|
||||
} from './BitbucketServerClient';
|
||||
import { BitbucketServerProject, BitbucketServerRepository } from './types';
|
||||
import {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerProject,
|
||||
BitbucketServerRepository,
|
||||
} from './types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
@@ -122,6 +127,7 @@ describe('BitbucketServerClient', () => {
|
||||
],
|
||||
},
|
||||
archived: false,
|
||||
defaultBranch: 'master',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -200,6 +206,7 @@ describe('BitbucketServerClient', () => {
|
||||
],
|
||||
},
|
||||
archived: false,
|
||||
defaultBranch: 'master',
|
||||
};
|
||||
|
||||
return res(ctx.json(response));
|
||||
@@ -217,4 +224,82 @@ describe('BitbucketServerClient', () => {
|
||||
'https://bitbucket.mycompany.com/projects/test-project',
|
||||
);
|
||||
});
|
||||
|
||||
it('getRepository no repo', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/wrong-repo`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const error = async () => {
|
||||
await client.getRepository({
|
||||
projectKey: 'test-project',
|
||||
repo: 'wrong-repo',
|
||||
});
|
||||
};
|
||||
|
||||
await expect(error).rejects.toThrow(
|
||||
"Repository 'wrong-repo' in project 'test-project' does not exist.",
|
||||
);
|
||||
await expect(error).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('getDefaultBranch success', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/test-repo/default-branch`,
|
||||
(req, res, ctx) => {
|
||||
if (
|
||||
req.headers.get('authorization') !==
|
||||
'Basic dGVzdC11c2VyOnRlc3QtcHc='
|
||||
) {
|
||||
return res(ctx.status(400));
|
||||
}
|
||||
const response: BitbucketServerDefaultBranch = {
|
||||
id: 'refs/heads/master',
|
||||
displayId: 'master',
|
||||
type: 'BRANCH',
|
||||
latestCommit: '0f2f3ae484054696568bf4560ba4da280f7df82a',
|
||||
latestChangeset: '0f2f3ae484054696568bf4560ba4da280f7df82a',
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
return res(ctx.json(response));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const repo = await client.getDefaultBranch({
|
||||
projectKey: 'test-project',
|
||||
repo: 'test-repo',
|
||||
});
|
||||
expect(repo.displayId).toEqual('master');
|
||||
});
|
||||
|
||||
it('getDefaultBranch endpoint', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/wrong-repo/default-branch`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const error = async () => {
|
||||
await client.getDefaultBranch({
|
||||
projectKey: 'test-project',
|
||||
repo: 'wrong-repo',
|
||||
});
|
||||
};
|
||||
|
||||
await expect(error).rejects.toThrow(
|
||||
"Your Bitbucket Server version no longer supports the default branch endpoint or 'wrong-repo' in 'test-project' does not exist.",
|
||||
);
|
||||
await expect(error).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,14 @@ import {
|
||||
BitbucketServerIntegrationConfig,
|
||||
getBitbucketServerRequestOptions,
|
||||
} from '@backstage/integration';
|
||||
import { BitbucketServerProject, BitbucketServerRepository } from './types';
|
||||
import pThrottle from 'p-throttle';
|
||||
import {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerRepository,
|
||||
} from './index';
|
||||
import { BitbucketServerProject } from './types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
// 1 per second
|
||||
const throttle = pThrottle({
|
||||
@@ -92,7 +98,35 @@ export class BitbucketServerClient {
|
||||
request,
|
||||
getBitbucketServerRequestOptions(this.config),
|
||||
);
|
||||
return response.json();
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(
|
||||
`Repository '${options.repo}' in project '${options.projectKey}' does not exist.`,
|
||||
);
|
||||
}
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
async getDefaultBranch(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
}): Promise<BitbucketServerDefaultBranch> {
|
||||
const request = `${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/default-branch`;
|
||||
const response = await fetch(
|
||||
request,
|
||||
getBitbucketServerRequestOptions(this.config),
|
||||
);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(
|
||||
`Your Bitbucket Server version no longer supports the default branch endpoint or '${options.repo}' in '${options.projectKey}' does not exist.`,
|
||||
);
|
||||
}
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
resolvePath(options: { projectKey: string; repo: string; path: string }): {
|
||||
|
||||
@@ -20,6 +20,9 @@ export type {
|
||||
BitbucketServerListOptions,
|
||||
} from './BitbucketServerClient';
|
||||
export type {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerEvents,
|
||||
BitbucketServerProject,
|
||||
} from './types';
|
||||
// export * from './types';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type BitbucketServerRepository = {
|
||||
type BitbucketServerRepository = {
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
@@ -28,9 +28,55 @@ export type BitbucketServerRepository = {
|
||||
}[]
|
||||
>;
|
||||
archived: boolean;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type BitbucketServerProject = {
|
||||
type BitbucketServerProject = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
type BitbucketServerDefaultBranch = {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: string;
|
||||
latestCommit: string;
|
||||
latestChangeset: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
namespace BitbucketServerEvents {
|
||||
interface Event {
|
||||
eventKey: string;
|
||||
}
|
||||
|
||||
export interface RefsChangedEvent extends Event {
|
||||
date: string;
|
||||
actor: Actor;
|
||||
repository: Repository;
|
||||
changes: Change[];
|
||||
commits: undefined;
|
||||
ToCommit: undefined;
|
||||
}
|
||||
type Actor = {
|
||||
name?: string;
|
||||
id: number;
|
||||
};
|
||||
type Change = {
|
||||
ref: { id: string; displayId: string; type: string };
|
||||
};
|
||||
type Repository = {
|
||||
slug: string;
|
||||
id: number;
|
||||
name: string;
|
||||
project: BitbucketServerProject;
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerProject,
|
||||
BitbucketServerEvents,
|
||||
BitbucketServerRepository,
|
||||
};
|
||||
|
||||
+49
-29
@@ -16,20 +16,33 @@
|
||||
|
||||
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider';
|
||||
import { BitbucketServerEntityProvider } from '../providers';
|
||||
import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider';
|
||||
|
||||
describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
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<BitbucketServerEntityProvider> | undefined;
|
||||
let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined;
|
||||
|
||||
const extensionPoint = {
|
||||
const catalogExtensionPointImpl = {
|
||||
addEntityProvider: (providers: any) => {
|
||||
addedProviders = providers;
|
||||
},
|
||||
};
|
||||
const connection = jest.fn() as unknown as EntityProviderConnection;
|
||||
const runner = jest.fn();
|
||||
const scheduler = mockServices.scheduler.mock({
|
||||
createScheduledTaskRunner(schedule) {
|
||||
@@ -38,33 +51,35 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const config = {
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: 'bitbucket.mycompany.com',
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
await startTestBackend({
|
||||
extensionPoints: [
|
||||
[catalogProcessingExtensionPoint, catalogExtensionPointImpl],
|
||||
],
|
||||
features: [
|
||||
eventsServiceFactory,
|
||||
catalogModuleBitbucketServerEntityProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: 'bitbucket.mycompany.com',
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
|
||||
features: [
|
||||
catalogModuleBitbucketServerEntityProvider,
|
||||
mockServices.rootConfig.factory({ data: config }),
|
||||
mockServices.logger.factory(),
|
||||
}),
|
||||
scheduler.factory,
|
||||
],
|
||||
});
|
||||
@@ -72,9 +87,14 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
expect(usedSchedule?.frequency).toEqual({ months: 1 });
|
||||
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
|
||||
expect(addedProviders?.length).toEqual(1);
|
||||
expect(addedProviders?.pop()?.getProviderName()).toEqual(
|
||||
expect(runner).not.toHaveBeenCalled();
|
||||
const provider = addedProviders!.pop()!;
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'bitbucketServer-provider:default',
|
||||
);
|
||||
expect(runner).not.toHaveBeenCalled();
|
||||
await provider.connect(connection);
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('bitbucketServer-provider:default');
|
||||
expect(runner).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-2
@@ -18,8 +18,10 @@ import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { BitbucketServerEntityProvider } from '../providers';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -31,14 +33,28 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
catalogApi: catalogServiceRef,
|
||||
config: coreServices.rootConfig,
|
||||
events: eventsServiceRef,
|
||||
logger: coreServices.logger,
|
||||
scheduler: coreServices.scheduler,
|
||||
auth: coreServices.auth,
|
||||
},
|
||||
async init({ catalog, config, logger, scheduler }) {
|
||||
async init({
|
||||
catalog,
|
||||
catalogApi,
|
||||
config,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
auth,
|
||||
}) {
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
auth,
|
||||
});
|
||||
|
||||
catalog.addEntityProvider(providers);
|
||||
|
||||
+478
-6
@@ -18,17 +18,30 @@ import {
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
SchedulerServiceTaskInvocationDefinition,
|
||||
BackstageCredentials,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
CatalogService,
|
||||
DeferredEntity,
|
||||
EntityProviderConnection,
|
||||
locationSpecToLocationEntity,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { BitbucketServerEntityProvider } from './BitbucketServerEntityProvider';
|
||||
import {
|
||||
BitbucketServerEntityProvider,
|
||||
toDeferredEntities,
|
||||
} from './BitbucketServerEntityProvider';
|
||||
import { BitbucketServerPagedResponse } from '../lib';
|
||||
import { Entity, LocationEntity } from '@backstage/catalog-model';
|
||||
import { BitbucketServerEvents } from '../lib/index';
|
||||
import { DefaultEventsService } from '@backstage/plugin-events-node';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
class PersistingTaskRunner implements SchedulerServiceTaskRunner {
|
||||
private tasks: SchedulerServiceTaskInvocationDefinition[] = [];
|
||||
@@ -57,9 +70,17 @@ function pagedResponse(values: any): BitbucketServerPagedResponse<any> {
|
||||
|
||||
const logger = mockServices.logger.mock();
|
||||
|
||||
const authService = mockServices.auth.mock();
|
||||
|
||||
const events = DefaultEventsService.create({ logger });
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
function setupStubs(projects: Project[], baseUrl: string) {
|
||||
function setupStubs(
|
||||
projects: Project[],
|
||||
baseUrl: string,
|
||||
defaultBranch: string,
|
||||
) {
|
||||
// Stub projects
|
||||
server.use(
|
||||
rest.get(`${baseUrl}/rest/api/1.0/projects`, (_, res, ctx) => {
|
||||
@@ -93,6 +114,7 @@ function setupStubs(projects: Project[], baseUrl: string) {
|
||||
],
|
||||
},
|
||||
archived: repo.archived ?? false,
|
||||
defaultBranch: defaultBranch,
|
||||
});
|
||||
}
|
||||
return res(ctx.json(pagedResponse(response)));
|
||||
@@ -102,6 +124,102 @@ function setupStubs(projects: Project[], baseUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const targetPath = `/catalog-info.yaml`;
|
||||
const test1RepoUrl = `https://${host}/projects/TEST/repos/test1/browse`;
|
||||
|
||||
function setupRepositoryReqHandler(defaultBranch: string) {
|
||||
server.use(
|
||||
rest.get(
|
||||
`https://${host}/rest/api/1.0/projects/TEST/repos/test1`,
|
||||
(_, res, ctx) => {
|
||||
const response = {
|
||||
slug: 'test1',
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
project: {
|
||||
key: 'TEST',
|
||||
id: 1,
|
||||
name: 'TEST',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: `https://${host}/projects/TEST`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: `${test1RepoUrl}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultBranch: defaultBranch,
|
||||
};
|
||||
return res(ctx.json(response));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = {
|
||||
eventKey: 'repo:refs_changed',
|
||||
date: '2017-09-19T09:45:32+1000',
|
||||
actor: {
|
||||
name: 'admin',
|
||||
id: 1,
|
||||
},
|
||||
repository: {
|
||||
slug: 'test1',
|
||||
id: 84,
|
||||
name: 'test1',
|
||||
project: {
|
||||
key: 'TEST',
|
||||
},
|
||||
},
|
||||
changes: [
|
||||
{
|
||||
ref: {
|
||||
id: 'refs/heads/master',
|
||||
displayId: 'master',
|
||||
type: 'BRANCH',
|
||||
},
|
||||
},
|
||||
],
|
||||
commits: undefined,
|
||||
ToCommit: undefined,
|
||||
};
|
||||
const repoPushEventParams = {
|
||||
topic: 'bitbucketServer.repo:refs_changed',
|
||||
eventPayload: repoPushEvent,
|
||||
metadata: { 'x-event-key': 'repo:refs_changed' },
|
||||
};
|
||||
|
||||
const createLocationEntity = (
|
||||
repoUrl: string,
|
||||
path: string,
|
||||
defaultBranch: string,
|
||||
): LocationEntity => {
|
||||
const target = `${repoUrl}${path}`;
|
||||
|
||||
const entity = locationSpecToLocationEntity({
|
||||
location: {
|
||||
type: 'url',
|
||||
target: target,
|
||||
presence: 'optional',
|
||||
},
|
||||
});
|
||||
entity.metadata.annotations = {
|
||||
...entity.metadata.annotations,
|
||||
[`${host}/repo-url`]: target,
|
||||
['bitbucket.org/default-branch']: defaultBranch,
|
||||
};
|
||||
|
||||
return entity;
|
||||
};
|
||||
|
||||
describe('BitbucketServerEntityProvider', () => {
|
||||
registerMswTestHooks(server);
|
||||
afterEach(() => {
|
||||
@@ -157,6 +275,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -191,6 +310,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -204,7 +324,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update on scheduled execution with filters', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -253,6 +372,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -270,6 +390,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:${url}`,
|
||||
'backstage.io/managed-by-origin-location': `url:${url}`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -291,7 +412,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update on scheduled execution without filters', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -329,6 +449,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -345,6 +466,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -364,6 +486,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
@@ -441,7 +564,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update with schedule in config', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -488,6 +610,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -504,6 +627,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -523,6 +647,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
@@ -542,4 +667,351 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
entities: expectedEntities,
|
||||
});
|
||||
});
|
||||
|
||||
it('Multiple location entities to deferred entities', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: host,
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
expect(providers).toHaveLength(1);
|
||||
expect(providers[0].getProviderName()).toEqual(
|
||||
'bitbucketServer-provider:default',
|
||||
);
|
||||
|
||||
const locationEntities = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
[`${host}/repo-url`]: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
spec: {
|
||||
presence: 'optional',
|
||||
target: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
[`${host}/repo-url`]: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
spec: {
|
||||
presence: 'optional',
|
||||
target: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const deferredEntities = toDeferredEntities(
|
||||
locationEntities,
|
||||
providers[0].getProviderName(),
|
||||
);
|
||||
|
||||
expect(deferredEntities).toEqual([
|
||||
{
|
||||
locationKey: providers[0].getProviderName(),
|
||||
entity: locationEntities[0],
|
||||
},
|
||||
{
|
||||
locationKey: providers[0].getProviderName(),
|
||||
entity: locationEntities[1],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('refresh onRepoPush', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const keptModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/kept-module:${targetPath}`,
|
||||
'master',
|
||||
);
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
setupRepositoryReqHandler('master');
|
||||
|
||||
// authService.getOwnServiceCredentials();
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/kept-module:/catalog-info.yaml`,
|
||||
defaultBranch: 'master',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock.mock({
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
credentials: { credentials: BackstageCredentials },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
credentials.credentials !==
|
||||
(await authService.getOwnServiceCredentials()) ||
|
||||
request.filter.kind !== 'Location' ||
|
||||
request.filter[`metadata.annotations.${host}/repo-url`] !==
|
||||
`${test1RepoUrl}/kept-module:${targetPath}`
|
||||
) {
|
||||
return { items: [] };
|
||||
}
|
||||
return {
|
||||
items: [keptModule],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledWith({
|
||||
keys: [`url:${test1RepoUrl}/kept-module:${targetPath}`],
|
||||
});
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('no refresh onRepoPush due to different default branch', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const keptModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/kept-module:${targetPath}`,
|
||||
'main',
|
||||
);
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
setupRepositoryReqHandler('main');
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/kept-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock.mock({
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
credentials: { credentials: BackstageCredentials },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
credentials.credentials !==
|
||||
(await authService.getOwnServiceCredentials()) ||
|
||||
request.filter.kind !== 'Location' ||
|
||||
request.filter[`metadata.annotations.${host}/repo-url`] !==
|
||||
`${test1RepoUrl}/kept-module:${targetPath}`
|
||||
) {
|
||||
return { items: [] };
|
||||
}
|
||||
return {
|
||||
items: [keptModule],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('add onRepoPush', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
setupRepositoryReqHandler('master');
|
||||
authService.getPluginRequestToken.mockResolvedValue({
|
||||
token: 'fake-token',
|
||||
});
|
||||
const addedModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/added-module:${targetPath}`,
|
||||
'master',
|
||||
);
|
||||
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/added-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock({ entities: [] });
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
const addedEntities = [
|
||||
{
|
||||
entity: addedModule,
|
||||
locationKey: 'bitbucketServer-provider:mainProvider',
|
||||
},
|
||||
];
|
||||
const removedEntities: DeferredEntity[] = [];
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledWith({
|
||||
keys: [],
|
||||
});
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'delta',
|
||||
added: addedEntities,
|
||||
removed: removedEntities,
|
||||
});
|
||||
});
|
||||
|
||||
it('fail add onRepoPush from wrong default branch', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
setupRepositoryReqHandler('main');
|
||||
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/added-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock({ entities: [] });
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi as any as CatalogService,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
+280
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, LocationEntity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
DeferredEntity,
|
||||
CatalogService,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import * as uuid from 'uuid';
|
||||
import { BitbucketServerClient, paginated } from '../lib';
|
||||
@@ -36,10 +38,16 @@ import {
|
||||
defaultBitbucketServerLocationParser,
|
||||
} from './BitbucketServerLocationParser';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
LoggerService,
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerEvents } from '../lib';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
|
||||
const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed';
|
||||
|
||||
/**
|
||||
* Discovers catalog files located in Bitbucket Server.
|
||||
@@ -56,14 +64,23 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
private readonly logger: LoggerService;
|
||||
private readonly scheduleFn: () => Promise<void>;
|
||||
private connection?: EntityProviderConnection;
|
||||
private readonly catalogApi?: CatalogService;
|
||||
private readonly events?: EventsService;
|
||||
private readonly auth?: AuthService;
|
||||
private eventConfigErrorThrown = false;
|
||||
private readonly targetAnnotation: string;
|
||||
private readonly defaultBranchAnnotation: string;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
events?: EventsService;
|
||||
parser?: BitbucketServerLocationParser;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
catalogApi?: CatalogService;
|
||||
auth?: AuthService;
|
||||
},
|
||||
): BitbucketServerEntityProvider[] {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -98,6 +115,9 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
options.logger,
|
||||
taskRunner,
|
||||
options.parser,
|
||||
options.catalogApi,
|
||||
options.events,
|
||||
options.auth,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -108,6 +128,9 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
logger: LoggerService,
|
||||
taskRunner: SchedulerServiceTaskRunner,
|
||||
parser?: BitbucketServerLocationParser,
|
||||
catalogApi?: CatalogService,
|
||||
events?: EventsService,
|
||||
auth?: AuthService,
|
||||
) {
|
||||
this.integration = integration;
|
||||
this.config = config;
|
||||
@@ -116,6 +139,11 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
target: this.getProviderName(),
|
||||
});
|
||||
this.scheduleFn = this.createScheduleFn(taskRunner);
|
||||
this.catalogApi = catalogApi;
|
||||
this.auth = auth;
|
||||
this.targetAnnotation = `${this.config.host.split(':')[0]}/repo-url`;
|
||||
this.defaultBranchAnnotation = 'bitbucket.org/default-branch';
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
private createScheduleFn(
|
||||
@@ -154,6 +182,22 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
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_REFS_CHANGED],
|
||||
onEvent: async params => {
|
||||
if (params.topic !== TOPIC_REPO_REFS_CHANGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onRepoPush(
|
||||
params.eventPayload as BitbucketServerEvents.RefsChangedEvent,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(logger: LoggerService) {
|
||||
@@ -218,10 +262,245 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
presence: 'optional',
|
||||
},
|
||||
})) {
|
||||
if (entity.metadata.annotations === undefined) {
|
||||
entity.metadata.annotations = {};
|
||||
}
|
||||
if (repository.defaultBranch === undefined) {
|
||||
const defaultBranchResponse = await client.getDefaultBranch({
|
||||
repo: repository.slug,
|
||||
projectKey: project.key,
|
||||
});
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
defaultBranchResponse.displayId;
|
||||
} else {
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
repository.defaultBranch;
|
||||
}
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private isDefaultBranchPush(
|
||||
defaultBranch: String,
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): boolean {
|
||||
return event.changes.some(c => defaultBranch === c.ref.displayId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provider is able to handle events
|
||||
* @returns Boolean
|
||||
*/
|
||||
private canHandleEvents(): boolean {
|
||||
if (this.catalogApi && this.auth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.eventConfigErrorThrown) {
|
||||
this.eventConfigErrorThrown = true;
|
||||
throw new Error(
|
||||
`${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or AuthService.`,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getLocationEntity(
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): Promise<Entity[]> {
|
||||
const client = BitbucketServerClient.fromConfig({
|
||||
config: this.integration.config,
|
||||
});
|
||||
const result: Entity[] = [];
|
||||
try {
|
||||
const repository = await client.getRepository({
|
||||
projectKey: event.repository.project.key,
|
||||
repo: event.repository.slug,
|
||||
});
|
||||
|
||||
for await (const entity of this.parser({
|
||||
client,
|
||||
logger: this.logger,
|
||||
location: {
|
||||
type: 'url',
|
||||
target: `${repository.links.self[0].href}${this.config.catalogPath}`,
|
||||
presence: 'optional',
|
||||
},
|
||||
})) {
|
||||
entity.metadata.annotations![
|
||||
this.targetAnnotation
|
||||
] = `${repository.links.self[0].href}${this.config.catalogPath}`;
|
||||
|
||||
if (entity.metadata.annotations === undefined) {
|
||||
entity.metadata.annotations = {};
|
||||
}
|
||||
|
||||
if (repository.defaultBranch === undefined) {
|
||||
const defaultBranchResponse = await client.getDefaultBranch({
|
||||
repo: repository.slug,
|
||||
projectKey: event.repository.project.key,
|
||||
});
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
defaultBranchResponse.displayId;
|
||||
} else {
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
repository.defaultBranch;
|
||||
}
|
||||
result.push(entity);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'NotFoundError') {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async onRepoPush(
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): Promise<void> {
|
||||
if (!this.canHandleEvents()) {
|
||||
this.logger.error(
|
||||
'Bitbucket Server catalog entity provider is not set up to handle events. Missing authService or catalogApi.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
const repoSlug = event.repository.slug;
|
||||
const catalogRepoUrl: string = `https://${this.config.host}/projects/${event.repository.project.key}/repos/${repoSlug}/browse${this.config.catalogPath}`;
|
||||
this.logger.info(`handle repo:push event for ${catalogRepoUrl}`);
|
||||
const targets = await this.getLocationEntity(event);
|
||||
if (targets.length === 0) {
|
||||
this.logger.error('Failed to create location entity.');
|
||||
return;
|
||||
}
|
||||
const existing = await this.findExistingLocations(
|
||||
catalogRepoUrl,
|
||||
await this.auth!.getOwnServiceCredentials(),
|
||||
);
|
||||
const stillExisting: LocationEntity[] = [];
|
||||
const removed: DeferredEntity[] = [];
|
||||
existing.forEach(item => {
|
||||
if (
|
||||
targets.find(
|
||||
value =>
|
||||
value.metadata.annotations![this.targetAnnotation] ===
|
||||
item.spec.target,
|
||||
)
|
||||
) {
|
||||
stillExisting.push(item);
|
||||
} else {
|
||||
removed.push({
|
||||
locationKey: this.getProviderName(),
|
||||
entity: item,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const added = await this.getAddedEntities(targets, existing);
|
||||
|
||||
if (
|
||||
stillExisting.length > 0 &&
|
||||
stillExisting[0].metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
stillExisting[0].metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
added.length > 0 &&
|
||||
added[0].entity.metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
added[0].entity.metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
removed.length > 0 &&
|
||||
removed[0].entity.metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
removed[0].entity.metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [
|
||||
this.connection.refresh({
|
||||
keys: stillExisting.map(entity => `url:${entity.spec.target}`),
|
||||
}),
|
||||
];
|
||||
|
||||
if (added.length > 0 || removed.length > 0) {
|
||||
promises.push(
|
||||
this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
added: added,
|
||||
removed: removed,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async getAddedEntities(
|
||||
targets: Entity[],
|
||||
existing: LocationEntity[],
|
||||
): Promise<DeferredEntity[]> {
|
||||
const added: DeferredEntity[] = toDeferredEntities(
|
||||
targets.filter(
|
||||
target =>
|
||||
!existing.find(
|
||||
item =>
|
||||
item.spec.target ===
|
||||
target.metadata.annotations![this.targetAnnotation],
|
||||
),
|
||||
),
|
||||
this.getProviderName(),
|
||||
);
|
||||
return added;
|
||||
}
|
||||
|
||||
private async findExistingLocations(
|
||||
catalogRepoUrl: string,
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<LocationEntity[]> {
|
||||
const filter: Record<string, string> = {};
|
||||
filter.kind = 'Location';
|
||||
filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl;
|
||||
|
||||
return this.catalogApi!.getEntities({ filter }, { credentials }).then(
|
||||
result => result.items,
|
||||
) as Promise<LocationEntity[]>;
|
||||
}
|
||||
}
|
||||
|
||||
export function toDeferredEntities(
|
||||
targets: Entity[],
|
||||
locationKey: string,
|
||||
): DeferredEntity[] {
|
||||
return targets.map(entity => {
|
||||
return {
|
||||
locationKey,
|
||||
entity,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
-2
@@ -48,10 +48,8 @@ export function readProviderConfigs(
|
||||
// simple/single config variant
|
||||
return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)];
|
||||
}
|
||||
|
||||
return providersConfig.keys().map(id => {
|
||||
const providerConfig = providersConfig.getConfig(id);
|
||||
|
||||
return readProviderConfig(id, providerConfig);
|
||||
});
|
||||
}
|
||||
|
||||
+3
-5
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LocationSpec,
|
||||
locationSpecToLocationEntity,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { locationSpecToLocationEntity } from '@backstage/plugin-catalog-node';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { BitbucketServerClient } from '../lib';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
import { BitbucketServerClient } from '../lib';
|
||||
|
||||
/**
|
||||
* A custom callback that reacts to finding a location by yielding entities.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,36 @@
|
||||
# events-backend-module-bitbucket-server
|
||||
|
||||
Welcome to the `events-backend-module-bitbucket-server` backend plugin!
|
||||
|
||||
This package is a module for the `events-backend` backend plugin
|
||||
and extends the event system with an `BitbucketServerEventRouter`.
|
||||
|
||||
The event router will subscribe to the topic `bitbucketServer`
|
||||
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:refs_changed` | `bitbucketServer.repo:refs_changed` |
|
||||
| `repo:modified` | `bitbucketServer.repo:modified` |
|
||||
|
||||
Please find all possible webhook event types at the
|
||||
[official documentation](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.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-bitbucket-server
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server'));
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-events-backend-module-bitbucket-server
|
||||
title: '@backstage/plugin-events-backend-module-bitbucket-server'
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@backstage/plugin-events-backend-module-bitbucket-server",
|
||||
"version": "0.0.0",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "events",
|
||||
"pluginPackage": "@backstage/plugin-events-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/events-backend-module-bitbucket-server"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server"
|
||||
|
||||
> 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';
|
||||
|
||||
// @public
|
||||
const eventsModuleBitbucketServerEventRouter: BackendFeature;
|
||||
export default eventsModuleBitbucketServerEventRouter;
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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-server" for the Backstage backend plugin "events-backend"
|
||||
* adding an event router for Bitbucket Server.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter';
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { BitbucketServerEventRouter } from './BitbucketServerEventRouter';
|
||||
|
||||
describe('BitbucketServerEventRouter', () => {
|
||||
const events = new TestEventsService();
|
||||
const eventRouter = new BitbucketServerEventRouter({ events });
|
||||
const topic = 'bitbucketServer';
|
||||
const eventPayload = { test: 'payload' };
|
||||
const metadata = { 'x-event-key': 'test:type' };
|
||||
|
||||
beforeEach(() => {
|
||||
events.reset();
|
||||
});
|
||||
|
||||
it('subscribed to topic', () => {
|
||||
eventRouter.subscribe();
|
||||
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter');
|
||||
expect(events.subscribed[0].topics).toEqual([topic]);
|
||||
});
|
||||
|
||||
it('no x-event-key', () => {
|
||||
eventRouter.onEvent({ topic, eventPayload });
|
||||
|
||||
expect(events.published).toEqual([]);
|
||||
});
|
||||
|
||||
it('with x-event-key', () => {
|
||||
eventRouter.onEvent({ topic, eventPayload, metadata });
|
||||
|
||||
expect(events.published.length).toBe(1);
|
||||
expect(events.published[0].topic).toEqual('bitbucketServer.test:type');
|
||||
expect(events.published[0].eventPayload).toEqual(eventPayload);
|
||||
expect(events.published[0].metadata).toEqual(metadata);
|
||||
});
|
||||
});
|
||||
+45
@@ -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 {
|
||||
EventParams,
|
||||
EventsService,
|
||||
SubTopicEventRouter,
|
||||
} from '@backstage/plugin-events-node';
|
||||
|
||||
/**
|
||||
* Subscribes to the generic `bitbucketServer` topic
|
||||
* and publishes the events under the more concrete sub-topic
|
||||
* depending on the `x-event-key` provided.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class BitbucketServerEventRouter extends SubTopicEventRouter {
|
||||
constructor(options: { events: EventsService }) {
|
||||
super({
|
||||
events: options.events,
|
||||
topic: 'bitbucketServer',
|
||||
});
|
||||
}
|
||||
|
||||
protected getSubscriberId(): string {
|
||||
return 'BitbucketServerEventRouter';
|
||||
}
|
||||
|
||||
protected determineSubTopic(params: EventParams): string | undefined {
|
||||
return params.metadata?.['x-event-key'] as string | undefined;
|
||||
}
|
||||
}
|
||||
+44
@@ -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 { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { BitbucketServerEventRouter } from '../router/BitbucketServerEventRouter';
|
||||
|
||||
/**
|
||||
* Module for the events-backend plugin, adding an event router for Bitbucket Server.
|
||||
*
|
||||
* Registers the `BitbucketServerEventRouter`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const eventsModuleBitbucketServerEventRouter = createBackendModule({
|
||||
pluginId: 'events',
|
||||
moduleId: 'bitbucket-server-event-router',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
events: eventsServiceRef,
|
||||
},
|
||||
async init({ events }) {
|
||||
const eventRouter = new BitbucketServerEventRouter({
|
||||
events,
|
||||
});
|
||||
await eventRouter.subscribe();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { eventsModuleBitbucketServerEventRouter } from './eventsModuleBitbucketServerEventRouter';
|
||||
|
||||
describe('eventsModuleBitbucketServerEventRouter', () => {
|
||||
it('should be correctly wired and set up', async () => {
|
||||
const events = new TestEventsService();
|
||||
const eventsServiceFactory = createServiceFactory({
|
||||
service: eventsServiceRef,
|
||||
deps: {},
|
||||
async factory({}) {
|
||||
return events;
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
features: [eventsServiceFactory, eventsModuleBitbucketServerEventRouter],
|
||||
});
|
||||
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter');
|
||||
});
|
||||
});
|
||||
+2
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,8 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { default as feature } from './module';
|
||||
|
||||
/** @alpha */
|
||||
const _feature = feature;
|
||||
export default _feature;
|
||||
export {};
|
||||
@@ -5536,12 +5536,16 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/catalog-client": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/integration": "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
|
||||
p-throttle: ^4.1.1
|
||||
@@ -6356,6 +6360,18 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server"
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^"
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit"
|
||||
|
||||
Reference in New Issue
Block a user