diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 953f0703be..43f797edd4 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor'; +import { BitbucketUrlReader, readConfig } from './BitbucketUrlReader'; import { ConfigReader } from '@backstage/config'; -describe('BitbucketApiReaderProcessor', () => { +const host = 'bitbucket.org'; + +describe('BitbucketUrlReader', () => { const createConfig = ( username: string | undefined, appPassword: string | undefined, @@ -26,22 +28,21 @@ describe('BitbucketApiReaderProcessor', () => { { context: '', data: { - catalog: { - processors: { - bitbucketApi: { + integrations: { + bitbucket: [ + { + host, username: username, appPassword: appPassword, }, - }, + ], }, }, }, ]); it('should build raw api', () => { - const processor = new BitbucketApiReaderProcessor( - createConfig(undefined, undefined), - ); + const processor = new BitbucketUrlReader({ host }); const tests = [ { @@ -90,7 +91,7 @@ describe('BitbucketApiReaderProcessor', () => { headers: {}, }, err: - "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string", + "Invalid type in config for key 'integrations.bitbucket[0].username' in '', got empty-string, wanted string", }, { username: 'only-user-provided', @@ -99,7 +100,7 @@ describe('BitbucketApiReaderProcessor', () => { headers: {}, }, err: - "Invalid type in config for key 'catalog.processors.bitbucketApi.appPassword' in '', got empty-string, wanted string", + "Invalid type in config for key 'integrations.bitbucket[0].appPassword' in '', got empty-string, wanted string", }, { username: '', @@ -108,7 +109,7 @@ describe('BitbucketApiReaderProcessor', () => { headers: {}, }, err: - "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string", + "Invalid type in config for key 'integrations.bitbucket[0].username' in '', got empty-string, wanted string", }, { username: 'some-user', @@ -132,6 +133,8 @@ describe('BitbucketApiReaderProcessor', () => { expect: { headers: {}, }, + err: + "Missing required config value at 'integrations.bitbucket[0].appPassword'", }, { username: undefined, @@ -146,13 +149,13 @@ describe('BitbucketApiReaderProcessor', () => { if (test.err) { expect( () => - new BitbucketApiReaderProcessor( - createConfig(test.username, test.password), + new BitbucketUrlReader( + readConfig(createConfig(test.username, test.password))[0], ), ).toThrowError(test.err); } else { - const processor = new BitbucketApiReaderProcessor( - createConfig(test.username, test.password), + const processor = new BitbucketUrlReader( + readConfig(createConfig(test.username, test.password))[0], ); expect(processor.getRequestOptions()).toEqual(test.expect); } diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index c46fbd1737..62739ded1c 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -14,31 +14,72 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import fetch, { RequestInit, HeadersInit, Response } from 'node-fetch'; import { Config } from '@backstage/config'; +import { ReaderFactory } from './UrlReaders'; +import { UrlReader } from './types'; +import { NotFoundError } from '../errors'; -export class BitbucketApiReaderProcessor implements LocationProcessor { - private username: string; - private password: string; +type Options = { + // TODO: added here for future support, but we only allow bitbucket.org for now + host: string; + auth?: { + username: string; + appPassword: string; + }; +}; - constructor(config: Config) { - this.username = - config.getOptionalString('catalog.processors.bitbucketApi.username') ?? - ''; - this.password = - config.getOptionalString('catalog.processors.bitbucketApi.appPassword') ?? - ''; +export function readConfig(config: Config): Options[] { + const optionsArr = Array(); + + const providerConfigs = + config.getOptionalConfigArray('integrations.bitbucket') ?? []; + + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'bitbucket.org'; + + let auth; + if (providerConfig.has('username')) { + const username = providerConfig.getString('username'); + const appPassword = providerConfig.getString('appPassword'); + auth = { username, appPassword }; + } + + optionsArr.push({ host, auth }); + } + + // As a convenience we always make sure there's at least an unauthenticated + // reader for public bitbucket repos. + if (!optionsArr.some(p => p.host === 'bitbucket.org')) { + optionsArr.push({ host: 'bitbucket.org' }); + } + + return optionsArr; +} + +export class BitbucketUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(options => { + const reader = new BitbucketUrlReader(options); + const predicate = (url: URL) => url.host === options.host; + return { reader, predicate }; + }); + }; + + constructor(private readonly options: Options) { + if (options.host !== 'bitbucket.org') { + throw Error( + `Bitbucket integration currently only supports 'bitbucket.org', tried to use host '${options.host}'`, + ); + } } getRequestOptions(): RequestInit { const headers: HeadersInit = {}; - if (this.username !== '' && this.password !== '') { + if (this.options.auth) { headers.Authorization = `Basic ${Buffer.from( - `${this.username}:${this.password}`, + `${this.options.auth.username}:${this.options.auth.appPassword}`, 'utf8', ).toString('base64')}`; } @@ -50,38 +91,25 @@ export class BitbucketApiReaderProcessor implements LocationProcessor { return requestOptions; } - async readLocation( - location: LocationSpec, - optional: boolean, - emit: LocationProcessorEmit, - ): Promise { - if (location.type !== 'bitbucket/api') { - return false; - } + async read(url: string): Promise { + const builtUrl = this.buildRawUrl(url); + let response: Response; try { - const url = this.buildRawUrl(location.target); - - const response = await fetch(url.toString(), this.getRequestOptions()); - - if (response.ok) { - const data = await response.buffer(); - emit(result.data(location, data)); - } else { - const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - if (!optional) { - emit(result.notFoundError(location, message)); - } - } else { - emit(result.generalError(location, message)); - } - } + response = await fetch(builtUrl.toString(), this.getRequestOptions()); } catch (e) { - const message = `Unable to read ${location.type} ${location.target}, ${e}`; - emit(result.generalError(location, message)); + throw new Error(`Unable to read ${url}, ${e}`); } - return true; + + if (response.ok) { + return response.buffer(); + } + + const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); } // Converts diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index 6560775e32..1934138d3c 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -22,6 +22,7 @@ import { UrlReaderPredicateMux, UrlReaderPredicateTuple, } from './UrlReaderPredicateMux'; +import { BitbucketUrlReader } from './BitbucketUrlReader'; export type ReaderFactoryOptions = { config: Config; @@ -51,7 +52,10 @@ export class UrlReaders { * Creates a new UrlReaders instance that includes all the default factories from this package */ static default({ logger }: { logger: Logger }) { - return new UrlReaders([GithubUrlReader.factory], logger); + return new UrlReaders( + [GithubUrlReader.factory, BitbucketUrlReader.factory], + logger, + ); } private constructor( diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index ad493c34fd..286bdd6786 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -16,3 +16,5 @@ export type { UrlReader } from './types'; export { UrlReaders } from './UrlReaders'; +export { GithubUrlReader } from './GithubUrlReader'; +export { BitbucketUrlReader } from './BitbucketUrlReader';