fix(catalog-backend): support Bitbucket servers with custom BaseURLs

Previously, the `BitbucketDiscoveryProcessor` assumed that all Bitbucket servers would have paths formed in the pattern of `/projects/{project}/repos/{repo}`.  However, since Bitbucket servers have the ability to configure custom baseUrls, this assumption does not always hold true. Instead of evaluating project and repo terms from the full target path, the processor will find the `/projects/` url segment, and start path evaluations from its index instead. This path substring is then used to determine the `projectSearchPath` and the `repoSearchPath`. Support is also added to accept search params for the `catalogPath` as well.

Fixes #8771

Signed-off-by: Dhruval Darji <dhruvaldarji@gmail.com>
This commit is contained in:
Dhruval Darji
2022-01-07 11:19:10 -05:00
parent 3cf9ff0ba4
commit 6fd70f8bc8
3 changed files with 222 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Provide support for Bitbucket servers with custom BaseURLs.
@@ -29,7 +29,10 @@ import { setupServer } from 'msw/node';
const server = setupServer();
function setupStubs(projects: any[]) {
function setupStubs(
projects: any[],
bitbucketBaseUrl = `https://bitbucket.mycompany.com`,
) {
function pagedResponse(values: any): PagedResponse<any> {
return {
values: values,
@@ -42,7 +45,7 @@ function setupStubs(projects: any[]) {
repos: string[],
): RequestHandler<any, any> {
return rest.get(
`https://bitbucket.mycompany.com/api/rest/1.0/projects/${project}/repos`,
`${bitbucketBaseUrl}/api/rest/1.0/projects/${project}/repos`,
(_, res, ctx) => {
const response = [];
for (const repo of repos) {
@@ -51,7 +54,7 @@ function setupStubs(projects: any[]) {
links: {
self: [
{
href: `https://bitbucket.mycompany.com/projects/${project}/repos/${repo}/browse`,
href: `${bitbucketBaseUrl}/projects/${project}/repos/${repo}/browse`,
},
],
},
@@ -63,20 +66,17 @@ function setupStubs(projects: any[]) {
}
server.use(
rest.get(
`https://bitbucket.mycompany.com/api/rest/1.0/projects`,
(_, res, ctx) => {
return res(
ctx.json(
pagedResponse(
projects.map(p => {
return { key: p.key };
}),
),
rest.get(`${bitbucketBaseUrl}/api/rest/1.0/projects`, (_, res, ctx) => {
return res(
ctx.json(
pagedResponse(
projects.map(p => {
return { key: p.key };
}),
),
);
},
),
),
);
}),
);
for (const project of projects) {
@@ -249,6 +249,30 @@ describe('BitbucketDiscoveryProcessor', () => {
optional: true,
});
});
it('output repositories by target search ref', async () => {
setupStubs([{ key: 'demo', repos: ['demo'] }]);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target:
'https://bitbucket.mycompany.com/projects/demo/repos/demo/catalog.yaml?ref=branch-name',
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml?ref=branch-name',
presence: 'optional',
},
optional: true,
});
});
it('filter unrelated repositories', async () => {
setupStubs([{ key: 'backstage', repos: ['test', 'abctest', 'testxyz'] }]);
const location: LocationSpec = {
@@ -302,6 +326,177 @@ describe('BitbucketDiscoveryProcessor', () => {
});
});
describe('handles organisation repositories with a custom baseURL', () => {
const processor = BitbucketDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
bitbucket: [
{
host: 'bitbucket.mycompany.com',
token: 'blob',
apiBaseUrl:
'https://bitbucket.mycompany.com/custom-path/api/rest/1.0',
},
],
},
}),
{ logger: getVoidLogger() },
);
it('output all repositories', async () => {
setupStubs(
[
{ key: 'backstage', repos: ['backstage'] },
{ key: 'demo', repos: ['demo'] },
],
'https://bitbucket.mycompany.com/custom-path',
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target:
'https://bitbucket.mycompany.com/custom-path/projects/*/repos/*/catalog.yaml',
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/backstage/browse/catalog.yaml',
presence: 'optional',
},
optional: true,
});
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/demo/repos/demo/browse/catalog.yaml',
presence: 'optional',
},
optional: true,
});
});
it('output repositories with wildcards', async () => {
setupStubs(
[
{ key: 'backstage', repos: ['backstage', 'techdocs-cli'] },
{ key: 'demo', repos: ['demo'] },
],
'https://bitbucket.mycompany.com/custom-path',
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-*/catalog.yaml',
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-cli/browse/catalog.yaml',
presence: 'optional',
},
optional: true,
});
});
it('output repositories by target search ref', async () => {
setupStubs(
[{ key: 'demo', repos: ['demo'] }],
'https://bitbucket.mycompany.com/custom-path',
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target:
'https://bitbucket.mycompany.com/custom-path/projects/demo/repos/demo/catalog.yaml?ref=branch-name',
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/demo/repos/demo/browse/catalog.yaml?ref=branch-name',
presence: 'optional',
},
optional: true,
});
});
it('filter unrelated repositories', async () => {
setupStubs(
[{ key: 'backstage', repos: ['test', 'abctest', 'testxyz'] }],
'https://bitbucket.mycompany.com/custom-path',
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/test/catalog.yaml',
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/test/browse/catalog.yaml',
presence: 'optional',
},
optional: true,
});
});
it.each`
target
${'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/*'}
${'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/*/'}
${'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-*/'}
`("target '$target' adds default path to catalog", async ({ target }) => {
setupStubs(
[{ key: 'backstage', repos: ['techdocs-cli'] }],
'https://bitbucket.mycompany.com/custom-path',
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target: target,
};
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(emitter).toHaveBeenCalledTimes(1);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://bitbucket.mycompany.com/custom-path/projects/backstage/repos/techdocs-cli/browse/catalog-info.yaml',
presence: 'optional',
},
optional: true,
});
});
});
describe('handles cloud repositories', () => {
const processor = BitbucketDiscoveryProcessor.fromConfig(
new ConfigReader({
@@ -230,14 +230,18 @@ function parseUrl(urlString: string): {
catalogPath: string;
} {
const url = new URL(urlString);
const path = url.pathname.substr(1).split('/');
const indexOfProjectSegment =
url.pathname.toLowerCase().indexOf('/projects/') + 1;
const path = url.pathname.substr(indexOfProjectSegment).split('/');
// /projects/backstage/repos/techdocs-*/catalog-info.yaml
if (path.length > 3 && path[1].length && path[3].length) {
return {
projectSearchPath: escapeRegExp(decodeURIComponent(path[1])),
repoSearchPath: escapeRegExp(decodeURIComponent(path[3])),
catalogPath: `/${decodeURIComponent(path.slice(4).join('/'))}`,
catalogPath: `/${decodeURIComponent(
path.slice(4).join('/') + url.search,
)}`,
};
}