diff --git a/.changeset/tasty-beans-clap.md b/.changeset/tasty-beans-clap.md new file mode 100644 index 0000000000..f171617860 --- /dev/null +++ b/.changeset/tasty-beans-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket': patch +--- + +split BitbucketClient into BitbucketCloudClient, BitbucketServerClient diff --git a/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts index 5553de7f02..a025eda962 100644 --- a/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-bitbucket/src/BitbucketDiscoveryProcessor.ts @@ -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 { - 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 { - 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> { const { projectSearchPath, repoSearchPath } = parseUrl(target); @@ -199,7 +204,7 @@ export async function readBitbucketOrg( } export async function searchBitbucketCloudLocations( - client: BitbucketClient, + client: BitbucketCloudClient, target: string, ): Promise> { const { @@ -252,7 +257,7 @@ export async function searchBitbucketCloudLocations( } export async function readBitbucketCloudLocations( - client: BitbucketClient, + client: BitbucketCloudClient, target: string, ): Promise> { const { catalogPath: requestedCatalogPath } = parseBitbucketCloudUrl(target); @@ -274,7 +279,7 @@ export async function readBitbucketCloudLocations( } export async function readBitbucketCloud( - client: BitbucketClient, + client: BitbucketCloudClient, target: string, ): Promise> { 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; diff --git a/plugins/catalog-backend-module-bitbucket/src/lib/client.ts b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketCloudClient.ts similarity index 65% rename from plugins/catalog-backend-module-bitbucket/src/lib/client.ts rename to plugins/catalog-backend-module-bitbucket/src/lib/BitbucketCloudClient.ts index a3ee4ab8a6..80826ce154 100644 --- a/plugins/catalog-backend-module-bitbucket/src/lib/client.ts +++ b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketCloudClient.ts @@ -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( + return this.pagedRequest( `${this.config.apiBaseUrl}/workspaces/${encodeURIComponent( workspace, )}/search/code`, @@ -61,58 +61,17 @@ export class BitbucketClient { ); } - async listProjects(options?: ListOptions): Promise> { - return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); - } - - async listRepositoriesByWorkspace20( + async listRepositoriesByWorkspace( workspace: string, options?: ListOptions20, ): Promise> { - return this.pagedRequest20( + return this.pagedRequest( `${this.config.apiBaseUrl}/repositories/${encodeURIComponent(workspace)}`, options, ); } - async listRepositories( - projectKey: string, - options?: ListOptions, - ): Promise> { - return this.pagedRequest( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent( - projectKey, - )}/repos`, - options, - ); - } - - private async pagedRequest( - endpoint: string, - options?: ListOptions, - ): Promise> { - 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>; - } - - private async pagedRequest20( + private async pagedRequest( endpoint: string, options?: ListOptions20, ): Promise> { @@ -154,21 +113,6 @@ export type CodeSearchResultItem = { }; }; -export type ListOptions = { - [key: string]: number | undefined; - limit?: number | undefined; - start?: number | undefined; -}; - -export type PagedResponse = { - 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 = { next: string; }; -export async function* paginated( - request: (options: ListOptions) => Promise>, - 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( request: (options: ListOptions20) => Promise>, options?: ListOptions20, diff --git a/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketServerClient.ts new file mode 100644 index 0000000000..bcebb1713b --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketServerClient.ts @@ -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> { + return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); + } + + async listRepositories( + projectKey: string, + options?: ListOptions, + ): Promise> { + return this.pagedRequest( + `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + projectKey, + )}/repos`, + options, + ); + } + + private async pagedRequest( + endpoint: string, + options?: ListOptions, + ): Promise> { + 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>; + } +} + +export type ListOptions = { + [key: string]: number | undefined; + limit?: number | undefined; + start?: number | undefined; +}; + +export type PagedResponse = { + size: number; + limit: number; + start: number; + isLastPage: boolean; + values: T[]; + nextPageStart: number; +}; + +export async function* paginated( + request: (options: ListOptions) => Promise>, + 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); +} diff --git a/plugins/catalog-backend-module-bitbucket/src/lib/index.ts b/plugins/catalog-backend-module-bitbucket/src/lib/index.ts index 309ec0bce5..8faf3db08f 100644 --- a/plugins/catalog-backend-module-bitbucket/src/lib/index.ts +++ b/plugins/catalog-backend-module-bitbucket/src/lib/index.ts @@ -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';