Add Bitbucket Server Event Integration
Signed-off-by: David Lilienfeld <davidlilienfeld4@gmail.com>
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,7 +26,47 @@ 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
|
||||
// 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-server/alpha'),
|
||||
);
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server/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-server/README.md>
|
||||
|
||||
### Installation with Legacy Backend System
|
||||
|
||||
#### Installation without Events Support
|
||||
|
||||
You will have to add the entity provider in the catalog initialization code of your
|
||||
backend. The provider is not installed by default, therefore you have to add a
|
||||
dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to your backend
|
||||
package.
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server
|
||||
```
|
||||
|
||||
And then add the entity provider to your catalog builder:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
@@ -37,6 +77,60 @@ backend.add(
|
||||
/* 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>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-server/README.md>
|
||||
|
||||
Additionally, you need to decide how you want to receive events from external sources like
|
||||
|
||||
- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md)
|
||||
- Bitbucket Server events webhook url should be set to `{backstageBaseUrl}/api/events/http/bitbucketServer`
|
||||
- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md)
|
||||
|
||||
Set up your provider
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
|
||||
/* highlight-add-start */
|
||||
import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server';
|
||||
/* highlight-add-end */
|
||||
|
||||
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
builder.addProcessor(new ScaffolderEntitiesProcessor());
|
||||
/* highlight-add-start */
|
||||
const bitbucketServerProvider = BitbucketServerEntityProvider.fromConfig(
|
||||
env.config,
|
||||
{
|
||||
catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
|
||||
logger: env.logger,
|
||||
scheduler: env.scheduler,
|
||||
events: env.events,
|
||||
tokenManager: env.tokenManager,
|
||||
},
|
||||
);
|
||||
env.eventBroker.subscribe(bitbucketServerProvider);
|
||||
builder.addEntityProvider(bitbucketServerProvider);
|
||||
/* highlight-add-end */
|
||||
const { processingEngine, router } = await builder.build();
|
||||
await processingEngine.start();
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
**Attention:**
|
||||
`catalogApi` and `tokenManager` are required at this variant
|
||||
compared to the one without events support.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md).
|
||||
|
||||
@@ -51,17 +51,25 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/backend-tasks": "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:^",
|
||||
"p-throttle": "^4.1.1",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"node-fetch": "^2.7.0",
|
||||
"uuid": "^11.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"
|
||||
},
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerIntegrationConfig } from '@backstage/integration';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
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';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
|
||||
// @public
|
||||
export class BitbucketServerClient {
|
||||
@@ -22,6 +26,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 +56,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 +74,12 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
events?: EventsService;
|
||||
parser?: BitbucketServerLocationParser;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
schedule?: TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
catalogApi?: CatalogApi;
|
||||
tokenManager?: TokenManager;
|
||||
},
|
||||
): BitbucketServerEntityProvider[];
|
||||
getProviderName(): string;
|
||||
@@ -65,6 +87,52 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
refresh(logger: LoggerService): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace BitbucketServerEvents {
|
||||
// (undocumented)
|
||||
export type Actor = {
|
||||
name?: string;
|
||||
id: number;
|
||||
};
|
||||
// (undocumented)
|
||||
export type Change = {
|
||||
ref: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
// (undocumented)
|
||||
export interface Event {
|
||||
// (undocumented)
|
||||
eventKey: string;
|
||||
}
|
||||
// (undocumented)
|
||||
export interface RefsChangedEvent extends Event {
|
||||
// (undocumented)
|
||||
actor: Actor;
|
||||
// (undocumented)
|
||||
changes: Change[];
|
||||
// (undocumented)
|
||||
commits: undefined;
|
||||
// (undocumented)
|
||||
date: string;
|
||||
// (undocumented)
|
||||
repository: Repository;
|
||||
// (undocumented)
|
||||
ToCommit: undefined;
|
||||
}
|
||||
// (undocumented)
|
||||
export type Repository = {
|
||||
slug: string;
|
||||
id: number;
|
||||
name: string;
|
||||
project: BitbucketServerProject;
|
||||
};
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BitbucketServerListOptions = {
|
||||
[key: string]: number | undefined;
|
||||
@@ -108,6 +176,7 @@ export type BitbucketServerRepository = {
|
||||
}[]
|
||||
>;
|
||||
archived: boolean;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -21,12 +21,7 @@
|
||||
*/
|
||||
|
||||
export { default } from './module';
|
||||
export { BitbucketServerClient } from './lib';
|
||||
export type {
|
||||
BitbucketServerProject,
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerPagedResponse,
|
||||
BitbucketServerListOptions,
|
||||
} from './lib';
|
||||
export * 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,7 +18,6 @@ import {
|
||||
BitbucketServerIntegrationConfig,
|
||||
getBitbucketServerRequestOptions,
|
||||
} from '@backstage/integration';
|
||||
import { BitbucketServerProject, BitbucketServerRepository } from './types';
|
||||
import pThrottle from 'p-throttle';
|
||||
|
||||
// 1 per second
|
||||
@@ -32,6 +31,15 @@ const throttledFetch = throttle(
|
||||
return await fetch(url, options);
|
||||
},
|
||||
);
|
||||
import {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerRepository,
|
||||
} from './index';
|
||||
|
||||
import { BitbucketServerProject } from './types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* A client for interacting with a Bitbucket Server instance
|
||||
@@ -92,7 +100,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 }): {
|
||||
@@ -163,7 +199,9 @@ export type BitbucketServerPagedResponse<T> = {
|
||||
values: T[];
|
||||
nextPageStart: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function* paginated(
|
||||
request: (
|
||||
options: BitbucketServerListOptions,
|
||||
|
||||
@@ -19,7 +19,4 @@ export type {
|
||||
BitbucketServerPagedResponse,
|
||||
BitbucketServerListOptions,
|
||||
} from './BitbucketServerClient';
|
||||
export type {
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerProject,
|
||||
} from './types';
|
||||
export * from './types';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type BitbucketServerRepository = {
|
||||
type BitbucketServerRepository = {
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
@@ -28,9 +28,56 @@ 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;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
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,
|
||||
};
|
||||
|
||||
+27
-5
@@ -16,20 +16,35 @@
|
||||
|
||||
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
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 { Duration } from 'luxon';
|
||||
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) {
|
||||
@@ -60,7 +75,9 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
};
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
|
||||
extensionPoints: [
|
||||
[catalogProcessingExtensionPoint, catalogExtensionPointImpl],
|
||||
],
|
||||
features: [
|
||||
catalogModuleBitbucketServerEntityProvider,
|
||||
mockServices.rootConfig.factory({ data: config }),
|
||||
@@ -72,9 +89,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);
|
||||
});
|
||||
});
|
||||
|
||||
+21
-3
@@ -18,8 +18,12 @@ import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { BitbucketServerEntityProvider } from '../providers';
|
||||
import {
|
||||
catalogProcessingExtensionPoint,
|
||||
catalogServiceRef,
|
||||
} from '@backstage/plugin-catalog-node/alpha';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -31,14 +35,28 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
catalogApi: catalogServiceRef,
|
||||
config: coreServices.rootConfig,
|
||||
events: eventsServiceRef,
|
||||
logger: coreServices.logger,
|
||||
scheduler: coreServices.scheduler,
|
||||
tokenManager: coreServices.tokenManager,
|
||||
},
|
||||
async init({ catalog, config, logger, scheduler }) {
|
||||
async init({
|
||||
catalog,
|
||||
catalogApi,
|
||||
config,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
tokenManager,
|
||||
}) {
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
tokenManager,
|
||||
});
|
||||
|
||||
catalog.addEntityProvider(providers);
|
||||
|
||||
+489
-6
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
@@ -24,11 +25,22 @@ import {
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
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 { CatalogApi } from '@backstage/catalog-client';
|
||||
import { DefaultEventsService } from '@backstage/plugin-events-node';
|
||||
|
||||
class PersistingTaskRunner implements SchedulerServiceTaskRunner {
|
||||
private tasks: SchedulerServiceTaskInvocationDefinition[] = [];
|
||||
@@ -57,9 +69,15 @@ function pagedResponse(values: any): BitbucketServerPagedResponse<any> {
|
||||
|
||||
const logger = mockServices.logger.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 +111,7 @@ function setupStubs(projects: Project[], baseUrl: string) {
|
||||
],
|
||||
},
|
||||
archived: repo.archived ?? false,
|
||||
defaultBranch: defaultBranch,
|
||||
});
|
||||
}
|
||||
return res(ctx.json(pagedResponse(response)));
|
||||
@@ -102,6 +121,107 @@ 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 tokenManager = {
|
||||
getToken: async () => {
|
||||
return { token: 'fake-token' };
|
||||
},
|
||||
} as any as TokenManager;
|
||||
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 +277,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -191,6 +312,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -204,7 +326,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 +374,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -270,6 +392,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 +414,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 +451,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -345,6 +468,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 +488,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 +566,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 +612,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -504,6 +629,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 +649,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 +669,360 @@ 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');
|
||||
|
||||
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 = {
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
options: { token: string },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
options.token !== 'fake-token' ||
|
||||
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 as any as CatalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
tokenManager,
|
||||
})[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 = {
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
options: { token: string },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
options.token !== 'fake-token' ||
|
||||
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 as any as CatalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
tokenManager,
|
||||
})[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');
|
||||
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 = {
|
||||
getEntities: async (
|
||||
_request: { filter: Record<string, string> },
|
||||
_options: { token: string },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi as any as CatalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
tokenManager,
|
||||
})[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 = {
|
||||
getEntities: async (
|
||||
_request: { filter: Record<string, string> },
|
||||
_options: { token: string },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi as any as CatalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
tokenManager,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
+410
-3
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
import { Entity, LocationEntity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
DeferredEntity,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import * as uuid from 'uuid';
|
||||
import { BitbucketServerClient, paginated } from '../lib';
|
||||
@@ -40,6 +42,12 @@ import {
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerEvents } from '../lib';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
|
||||
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?: CatalogApi;
|
||||
private readonly events?: EventsService;
|
||||
private readonly tokenManager?: TokenManager;
|
||||
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;
|
||||
schedule?: TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
catalogApi?: CatalogApi;
|
||||
tokenManager?: TokenManager;
|
||||
},
|
||||
): 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.tokenManager,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -108,6 +128,9 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
logger: LoggerService,
|
||||
taskRunner: SchedulerServiceTaskRunner,
|
||||
parser?: BitbucketServerLocationParser,
|
||||
catalogApi?: CatalogApi,
|
||||
events?: EventsService,
|
||||
tokenManager?: TokenManager,
|
||||
) {
|
||||
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.tokenManager = tokenManager;
|
||||
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,373 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the webhook was triggered on a commit to the head branch of a repository
|
||||
* @param event Bitbucket Server webhook repo:refs_changed event
|
||||
*/
|
||||
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 !== undefined &&
|
||||
this.catalogApi !== null &&
|
||||
this.tokenManager !== undefined &&
|
||||
this.tokenManager !== null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Bitbucket Server location entity for the repository that is referenced in the push event passed in.
|
||||
*
|
||||
* @param event A Bitbucket Server push event with repository information.
|
||||
*
|
||||
* @returns An array of location entities for the repository.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const RefsChangedEvent = {
|
||||
* "eventKey": "repo:refs_changed",
|
||||
* "date": "2022-01-01T00:00:00Z",
|
||||
* "actor": {
|
||||
* "name": "johndoe",
|
||||
* "emailAddress": "johndoe@example.com",
|
||||
* "id": 123,
|
||||
* "displayName": "John Doe",
|
||||
* "active": true,
|
||||
* "slug": "johndoe",
|
||||
* "type": "NORMAL"
|
||||
* },
|
||||
* "repository": {
|
||||
* "slug": "my-repo",
|
||||
* "id": 123,
|
||||
* "name": "My Repository",
|
||||
* "project": {
|
||||
* "key": "my-project",
|
||||
* "id": 456,
|
||||
* "name": "My Project",
|
||||
* "description": "My project description",
|
||||
* "public": true,
|
||||
* "type": "NORMAL"
|
||||
* }
|
||||
* },
|
||||
* "changes": [
|
||||
* {
|
||||
* "ref": {
|
||||
* "id": "refs/heads/master",
|
||||
* "displayId": "master",
|
||||
* "type": "BRANCH"
|
||||
* },
|
||||
* "refId": "refs/heads/master",
|
||||
* "fromHash": "0123456789abcdef0123456789abcdef0123456",
|
||||
* "toHash": "fedcba9876543210fedcba9876543210fedcba9",
|
||||
* "type": "UPDATE"
|
||||
* }
|
||||
* ]
|
||||
* };
|
||||
*
|
||||
* const locationEntities = await getLocationEntity(RefsChangedEvent);
|
||||
*
|
||||
* // locationEntities:
|
||||
* // [
|
||||
* // {
|
||||
* // kind: 'Location',
|
||||
* // metadata: {
|
||||
* // name: 'my-repo',
|
||||
* // namespace: 'my-project',
|
||||
* // annotations: {
|
||||
* // 'backstage.io/managed-by-location': 'url/catalog-info-path',
|
||||
* // 'backstage.io/managed-by-origin-location': 'url/catalog-info-path',
|
||||
* // 'host/repo-url': 'url',
|
||||
* // },
|
||||
* // },
|
||||
* // spec: {
|
||||
* // type: 'bitbucket',
|
||||
* // target: 'url/catalog-info-path',
|
||||
* // presence: 'optional',
|
||||
* // },
|
||||
* // },
|
||||
* // ]
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds if there are existing location entities for the repository that was pushed. If there are, it simply refreshes those entities,
|
||||
* if not, it discovers any entity that was added and removed in the list of entities
|
||||
* @param event - A Bitbucket Server webhook event for repo:refs_change
|
||||
*/
|
||||
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 tokenManager 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 { token } = await this.tokenManager!.getToken();
|
||||
const existing = await this.findExistingLocations(catalogRepoUrl, token);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location entities that are to be newly added to the catalog.
|
||||
* @param targets Location entities for catalog files in the repository that was pushed
|
||||
* @param existing The location entities in the repository that was pushed that already exist
|
||||
* @returns Returns all deferred entities that represent location entities that don't exist in the catalog yet
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all location entities in the catalog that already have the annotation `metadata.annotations.${this.config.host}/repo-url`
|
||||
* that is equivalent to @param repoURL\.
|
||||
* @param repoURL URL for the reposity that the method finds the existing location entities for
|
||||
* @param token Token from class token manager
|
||||
*/
|
||||
private async findExistingLocations(
|
||||
catalogRepoUrl: string,
|
||||
token: string,
|
||||
): Promise<LocationEntity[]> {
|
||||
const filter: Record<string, string> = {};
|
||||
filter.kind = 'Location';
|
||||
filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl;
|
||||
|
||||
return this.catalogApi!.getEntities({ filter }, { token }).then(
|
||||
result => result.items,
|
||||
) as Promise<LocationEntity[]>;
|
||||
}
|
||||
|
||||
// private static toLocationSpec(target: string): LocationSpec {
|
||||
// return {
|
||||
// type: 'url',
|
||||
// target: target,
|
||||
// presence: 'required',
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of entities into an array of deferred entities with the provider's name as the location key.
|
||||
*
|
||||
* @param targets An array of entities to convert.
|
||||
*
|
||||
* @returns An array of deferred entities with the provider's name as the location key.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const entities = [
|
||||
* { kind: 'Component', namespace: 'default', name: 'my-component' },
|
||||
* { kind: 'System', namespace: 'default', name: 'my-system' },
|
||||
* { kind: 'API', namespace: 'default', name: 'my-api' },
|
||||
* ];
|
||||
*
|
||||
* const deferredEntities = toDeferredEntities(entities);
|
||||
*
|
||||
* // deferredEntities:
|
||||
* // [
|
||||
* // { locationKey: 'my-provider', entity: { kind: 'Component', namespace: 'default', name: 'my-component' } },
|
||||
* // { locationKey: 'my-provider', entity: { kind: 'System', namespace: 'default', name: 'my-system' } },
|
||||
* // { locationKey: 'my-provider', entity: { kind: 'API', namespace: 'default', name: 'my-api' } },
|
||||
* // ]
|
||||
*/
|
||||
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,48 @@
|
||||
# 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/alpha'),
|
||||
);
|
||||
```
|
||||
|
||||
### Legacy Backend System
|
||||
|
||||
```ts
|
||||
// packages/backend/src/plugins/events.ts
|
||||
const eventRouter = new BitbucketCloudEventRouter({
|
||||
events: env.events,
|
||||
});
|
||||
await eventRouter.subscribe();
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
## 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';
|
||||
|
||||
// @alpha
|
||||
const eventsModuleBitbucketServerEventRouter: () => BackendFeature;
|
||||
export default eventsModuleBitbucketServerEventRouter;
|
||||
export { eventsModuleBitbucketServerEventRouter };
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
## 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 { EventParams } from '@backstage/plugin-events-node';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
import { SubTopicEventRouter } from '@backstage/plugin-events-node';
|
||||
|
||||
// @public
|
||||
export class BitbucketServerEventRouter extends SubTopicEventRouter {
|
||||
constructor(options: { events: EventsService });
|
||||
// (undocumented)
|
||||
protected determineSubTopic(params: EventParams): string | undefined;
|
||||
// (undocumented)
|
||||
protected getSubscriberId(): string;
|
||||
}
|
||||
```
|
||||
@@ -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,55 @@
|
||||
{
|
||||
"name": "@backstage/plugin-events-backend-module-bitbucket-server",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/events-backend-module-bitbucket-server"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/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:^",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { eventsModuleBitbucketServerEventRouter } from './service/eventsModuleBitbucketServerEventRouter';
|
||||
export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter';
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { BitbucketServerEventRouter } from './router/BitbucketServerEventRouter';
|
||||
+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`.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
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();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+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 { 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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -5739,11 +5739,13 @@ __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:^"
|
||||
luxon: ^3.0.0
|
||||
msw: ^1.0.0
|
||||
@@ -6557,6 +6559,19 @@ __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:^"
|
||||
supertest: ^6.1.3
|
||||
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