backend-common: port AzureUrlReader

This commit is contained in:
Patrik Oldsberg
2020-10-02 15:02:06 +02:00
parent 4cdac4624e
commit ce32eb3c69
4 changed files with 91 additions and 55 deletions
@@ -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);
}
}
@@ -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<Options>();
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<boolean> {
if (location.type !== 'azure/api') {
return false;
}
async read(url: string): Promise<Buffer> {
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
@@ -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,
);
}
+2 -1
View File
@@ -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';