backend-common: port BitbucketUrlReader

This commit is contained in:
Patrik Oldsberg
2020-10-02 14:50:59 +02:00
parent 683181837b
commit 4cdac4624e
4 changed files with 98 additions and 61 deletions
@@ -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);
}
@@ -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<Options>();
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<boolean> {
if (location.type !== 'bitbucket/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());
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
@@ -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(
@@ -16,3 +16,5 @@
export type { UrlReader } from './types';
export { UrlReaders } from './UrlReaders';
export { GithubUrlReader } from './GithubUrlReader';
export { BitbucketUrlReader } from './BitbucketUrlReader';