Merge pull request #10515 from Bonial-International-GmbH/PJ_bitbucket_split-client

feat: split `BitbucketClient` into `BitbucketCloudClient`, `BitbucketServerClient`
This commit is contained in:
Ben Lambert
2022-03-31 12:40:02 +02:00
committed by GitHub
5 changed files with 135 additions and 95 deletions
@@ -27,10 +27,11 @@ import {
} from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import {
BitbucketClient,
BitbucketCloudClient,
BitbucketRepository,
BitbucketRepository20,
BitbucketRepositoryParser,
BitbucketServerClient,
defaultRepositoryParser,
paginated,
paginated20,
@@ -90,28 +91,25 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
);
}
const client = new BitbucketClient({
config: integration.config,
});
const startTimestamp = Date.now();
this.logger.info(`Reading Bitbucket repositories from ${location.target}`);
const isBitbucketCloud = integration.config.host === 'bitbucket.org';
this.logger.info(
`Reading ${integration.config.host} repositories from ${location.target}`,
);
const processOptions: ProcessOptions = {
client,
emit,
integration,
location,
};
const isBitbucketCloud = integration.config.host === 'bitbucket.org';
const { scanned, matches } = isBitbucketCloud
? await this.processCloudRepositories(processOptions)
: await this.processOrganizationRepositories(processOptions);
const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
this.logger.debug(
`Read ${scanned} Bitbucket repositories (${matches} matching the pattern) in ${duration} seconds`,
`Read ${scanned} ${integration.config.host} repositories (${matches} matching the pattern) in ${duration} seconds`,
);
return true;
@@ -120,7 +118,10 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
private async processCloudRepositories(
options: ProcessOptions,
): Promise<ResultSummary> {
const { client, location, integration, emit } = options;
const { location, integration, emit } = options;
const client = new BitbucketCloudClient({
config: integration.config,
});
const { searchEnabled } = parseBitbucketCloudUrl(location.target);
@@ -147,12 +148,16 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
private async processOrganizationRepositories(
options: ProcessOptions,
): Promise<ResultSummary> {
const { client, location, integration, emit } = options;
const { location, integration, emit } = options;
const { catalogPath: requestedCatalogPath } = parseUrl(location.target);
const catalogPath = requestedCatalogPath
? `/${requestedCatalogPath}`
: DEFAULT_CATALOG_LOCATION;
const client = new BitbucketServerClient({
config: integration.config,
});
const result = await readBitbucketOrg(client, location.target);
for (const repository of result.matches) {
for await (const entity of this.parser({
@@ -171,7 +176,7 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
}
export async function readBitbucketOrg(
client: BitbucketClient,
client: BitbucketServerClient,
target: string,
): Promise<Result<BitbucketRepository>> {
const { projectSearchPath, repoSearchPath } = parseUrl(target);
@@ -199,7 +204,7 @@ export async function readBitbucketOrg(
}
export async function searchBitbucketCloudLocations(
client: BitbucketClient,
client: BitbucketCloudClient,
target: string,
): Promise<Result<string>> {
const {
@@ -252,7 +257,7 @@ export async function searchBitbucketCloudLocations(
}
export async function readBitbucketCloudLocations(
client: BitbucketClient,
client: BitbucketCloudClient,
target: string,
): Promise<Result<string>> {
const { catalogPath: requestedCatalogPath } = parseBitbucketCloudUrl(target);
@@ -274,7 +279,7 @@ export async function readBitbucketCloudLocations(
}
export async function readBitbucketCloud(
client: BitbucketClient,
client: BitbucketCloudClient,
target: string,
): Promise<Result<BitbucketRepository20>> {
const {
@@ -285,7 +290,7 @@ export async function readBitbucketCloud(
} = parseBitbucketCloudUrl(target);
const repositories = paginated20(
options => client.listRepositoriesByWorkspace20(workspacePath, options),
options => client.listRepositoriesByWorkspace(workspacePath, options),
{
q,
},
@@ -380,7 +385,6 @@ function escapeRegExp(str: string): RegExp {
}
type ProcessOptions = {
client: BitbucketClient;
integration: BitbucketIntegration;
location: LocationSpec;
emit: CatalogProcessorEmit;
@@ -21,7 +21,7 @@ import {
import fetch from 'node-fetch';
import { BitbucketRepository20 } from './types';
export class BitbucketClient {
export class BitbucketCloudClient {
private readonly config: BitbucketIntegrationConfig;
constructor(options: { config: BitbucketIntegrationConfig }) {
@@ -49,7 +49,7 @@ export class BitbucketClient {
'+values.file.commit.repository.links.html.href',
].join(',');
return this.pagedRequest20<CodeSearchResultItem>(
return this.pagedRequest<CodeSearchResultItem>(
`${this.config.apiBaseUrl}/workspaces/${encodeURIComponent(
workspace,
)}/search/code`,
@@ -61,58 +61,17 @@ export class BitbucketClient {
);
}
async listProjects(options?: ListOptions): Promise<PagedResponse<any>> {
return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options);
}
async listRepositoriesByWorkspace20(
async listRepositoriesByWorkspace(
workspace: string,
options?: ListOptions20,
): Promise<PagedResponse20<BitbucketRepository20>> {
return this.pagedRequest20<BitbucketRepository20>(
return this.pagedRequest<BitbucketRepository20>(
`${this.config.apiBaseUrl}/repositories/${encodeURIComponent(workspace)}`,
options,
);
}
async listRepositories(
projectKey: string,
options?: ListOptions,
): Promise<PagedResponse<any>> {
return this.pagedRequest(
`${this.config.apiBaseUrl}/projects/${encodeURIComponent(
projectKey,
)}/repos`,
options,
);
}
private async pagedRequest(
endpoint: string,
options?: ListOptions,
): Promise<PagedResponse<any>> {
const request = new URL(endpoint);
for (const key in options) {
if (options[key]) {
request.searchParams.append(key, options[key]!.toString());
}
}
const response = await fetch(
request.toString(),
getBitbucketRequestOptions(this.config),
);
if (!response.ok) {
throw new Error(
`Unexpected response when fetching ${request.toString()}. Expected 200 but got ${
response.status
} - ${response.statusText}`,
);
}
return response.json() as Promise<PagedResponse<any>>;
}
private async pagedRequest20<T = any>(
private async pagedRequest<T = any>(
endpoint: string,
options?: ListOptions20,
): Promise<PagedResponse20<T>> {
@@ -154,21 +113,6 @@ export type CodeSearchResultItem = {
};
};
export type ListOptions = {
[key: string]: number | undefined;
limit?: number | undefined;
start?: number | undefined;
};
export type PagedResponse<T> = {
size: number;
limit: number;
start: number;
isLastPage: boolean;
values: T[];
nextPageStart: number;
};
export type ListOptions20 = {
[key: string]: string | number | undefined;
page?: number | undefined;
@@ -183,21 +127,6 @@ export type PagedResponse20<T> = {
next: string;
};
export async function* paginated(
request: (options: ListOptions) => Promise<PagedResponse<any>>,
options?: ListOptions,
) {
const opts = options || { start: 0 };
let res;
do {
res = await request(opts);
opts.start = res.nextPageStart;
for (const item of res.values) {
yield item;
}
} while (!res.isLastPage);
}
export async function* paginated20<T = any>(
request: (options: ListOptions20) => Promise<PagedResponse20<T>>,
options?: ListOptions20,
@@ -0,0 +1,100 @@
/*
* Copyright 2021 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 {
BitbucketIntegrationConfig,
getBitbucketRequestOptions,
} from '@backstage/integration';
import fetch from 'node-fetch';
export class BitbucketServerClient {
private readonly config: BitbucketIntegrationConfig;
constructor(options: { config: BitbucketIntegrationConfig }) {
this.config = options.config;
}
async listProjects(options?: ListOptions): Promise<PagedResponse<any>> {
return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options);
}
async listRepositories(
projectKey: string,
options?: ListOptions,
): Promise<PagedResponse<any>> {
return this.pagedRequest(
`${this.config.apiBaseUrl}/projects/${encodeURIComponent(
projectKey,
)}/repos`,
options,
);
}
private async pagedRequest(
endpoint: string,
options?: ListOptions,
): Promise<PagedResponse<any>> {
const request = new URL(endpoint);
for (const key in options) {
if (options[key]) {
request.searchParams.append(key, options[key]!.toString());
}
}
const response = await fetch(
request.toString(),
getBitbucketRequestOptions(this.config),
);
if (!response.ok) {
throw new Error(
`Unexpected response when fetching ${request.toString()}. Expected 200 but got ${
response.status
} - ${response.statusText}`,
);
}
return response.json() as Promise<PagedResponse<any>>;
}
}
export type ListOptions = {
[key: string]: number | undefined;
limit?: number | undefined;
start?: number | undefined;
};
export type PagedResponse<T> = {
size: number;
limit: number;
start: number;
isLastPage: boolean;
values: T[];
nextPageStart: number;
};
export async function* paginated(
request: (options: ListOptions) => Promise<PagedResponse<any>>,
options?: ListOptions,
) {
const opts = options || { start: 0 };
let res;
do {
res = await request(opts);
opts.start = res.nextPageStart;
for (const item of res.values) {
yield item;
}
} while (!res.isLastPage);
}
@@ -16,6 +16,8 @@
export { defaultRepositoryParser } from './BitbucketRepositoryParser';
export type { BitbucketRepositoryParser } from './BitbucketRepositoryParser';
export { BitbucketClient, paginated, paginated20 } from './client';
export type { PagedResponse, PagedResponse20 } from './client';
export { BitbucketCloudClient, paginated20 } from './BitbucketCloudClient';
export { BitbucketServerClient, paginated } from './BitbucketServerClient';
export type { PagedResponse20 } from './BitbucketCloudClient';
export type { PagedResponse } from './BitbucketServerClient';
export type { BitbucketRepository, BitbucketRepository20 } from './types';