backend-common: port BitbucketUrlReader
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user