Merge pull request #10515 from Bonial-International-GmbH/PJ_bitbucket_split-client
feat: split `BitbucketClient` into `BitbucketCloudClient`, `BitbucketServerClient`
This commit is contained in:
@@ -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;
|
||||
|
||||
+5
-76
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user