diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 9b234e3e75..50db27b314 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -14,28 +14,33 @@ * limitations under the License. */ -import { AzureApiReaderProcessor } from './AzureApiReaderProcessor'; +import { AzureUrlReader, readConfig } from './AzureUrlReader'; import { ConfigReader } from '@backstage/config'; -describe('AzureApiReaderProcessor', () => { +const host = 'dev.azure.com'; + +describe('AzureUrlReader', () => { const createConfig = (token: string | undefined) => ConfigReader.fromConfigs([ { context: '', data: { - catalog: { - processors: { - azureApi: { - privateToken: token, + integrations: { + azure: [ + { + host, + token, }, - }, + ], }, }, }, ]); it('should build raw api', () => { - const processor = new AzureApiReaderProcessor(createConfig(undefined)); + const processor = new AzureUrlReader( + readConfig(createConfig(undefined))[0], + ); const tests = [ { target: @@ -98,7 +103,7 @@ describe('AzureApiReaderProcessor', () => { headers: {}, }, err: - "Invalid type in config for key 'catalog.processors.azureApi.privateToken' in '', got empty-string, wanted string", + "Invalid type in config for key 'integrations.azure[0].token' in '', got empty-string, wanted string", }, { token: undefined, @@ -111,10 +116,12 @@ describe('AzureApiReaderProcessor', () => { for (const test of tests) { if (test.err) { expect( - () => new AzureApiReaderProcessor(createConfig(test.token)), + () => new AzureUrlReader(readConfig(createConfig(test.token))[0]), ).toThrowError(test.err); } else { - const processor = new AzureApiReaderProcessor(createConfig(test.token)); + const processor = new AzureUrlReader( + readConfig(createConfig(test.token))[0], + ); expect(processor.getRequestOptions()).toEqual(test.expect); } } diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 70a84d72df..202695eaed 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -14,27 +14,63 @@ * 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 { NotFoundError } from '../errors'; +import { UrlReader } from './types'; +import { ReaderFactory } from './UrlReaders'; -export class AzureApiReaderProcessor implements LocationProcessor { - private privateToken: string; +type Options = { + // TODO: added here for future support, but we only allow dev.azure.com for now + host: string; + token?: string; +}; - constructor(config: Config) { - this.privateToken = - config.getOptionalString('catalog.processors.azureApi.privateToken') ?? - ''; +export function readConfig(config: Config): Options[] { + const optionsArr = Array(); + + const providerConfigs = + config.getOptionalConfigArray('integrations.azure') ?? []; + + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'dev.azure.com'; + const token = providerConfig.getOptionalString('token'); + + optionsArr.push({ host, token }); + } + + // As a convenience we always make sure there's at least an unauthenticated + // reader for public azure repos. + if (!optionsArr.some(p => p.host === 'dev.azure.com')) { + optionsArr.push({ host: 'dev.azure.com' }); + } + + return optionsArr; +} + +export class AzureUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(options => { + const reader = new AzureUrlReader(options); + const predicate = (url: URL) => url.host === options.host; + return { reader, predicate }; + }); + }; + + constructor(private readonly options: Options) { + if (options.host !== 'dev.azure.com') { + throw Error( + `Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`, + ); + } } getRequestOptions(): RequestInit { const headers: HeadersInit = {}; - if (this.privateToken !== '') { + if (this.options.token) { headers.Authorization = `Basic ${Buffer.from( - `:${this.privateToken}`, + `:${this.options.token}`, 'utf8', ).toString('base64')}`; } @@ -46,39 +82,26 @@ export class AzureApiReaderProcessor implements LocationProcessor { return requestOptions; } - async readLocation( - location: LocationSpec, - optional: boolean, - emit: LocationProcessorEmit, - ): Promise { - if (location.type !== 'azure/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()); - - // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html - if (response.ok && response.status !== 203) { - 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; + + // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html + if (response.ok && response.status !== 203) { + 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 1934138d3c..f88c3bfacc 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -16,13 +16,14 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; -import { GithubUrlReader } from './GithubUrlReader'; import { UrlReader } from './types'; import { UrlReaderPredicateMux, UrlReaderPredicateTuple, } from './UrlReaderPredicateMux'; +import { AzureUrlReader } from './AzureUrlReader'; import { BitbucketUrlReader } from './BitbucketUrlReader'; +import { GithubUrlReader } from './GithubUrlReader'; export type ReaderFactoryOptions = { config: Config; @@ -53,7 +54,11 @@ export class UrlReaders { */ static default({ logger }: { logger: Logger }) { return new UrlReaders( - [GithubUrlReader.factory, BitbucketUrlReader.factory], + [ + GithubUrlReader.factory, + BitbucketUrlReader.factory, + AzureUrlReader.factory, + ], logger, ); } diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 286bdd6786..d1dc1e7952 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -16,5 +16,6 @@ export type { UrlReader } from './types'; export { UrlReaders } from './UrlReaders'; -export { GithubUrlReader } from './GithubUrlReader'; +export { AzureUrlReader } from './AzureUrlReader'; export { BitbucketUrlReader } from './BitbucketUrlReader'; +export { GithubUrlReader } from './GithubUrlReader';