Skip to content

Commit

Permalink
Merge pull request #5149 from goober/feature/bitbucket-discovery
Browse files Browse the repository at this point in the history
Add a Bitbucket Discovery processor
  • Loading branch information
freben committed Mar 30, 2021
2 parents bd90c8f + 13abbc1 commit aa618e5
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-emus-wave.md
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---

Add Bitbucket Server discovery processor.
41 changes: 41 additions & 0 deletions docs/integrations/bitbucket/discovery.md
@@ -0,0 +1,41 @@
---
id: discovery
title: Bitbucket Discovery
sidebar_label: Discovery
description:
Automatically discovering catalog entities from repositories in Bitbucket
---

The Bitbucket integration has a special discovery processor for discovering
catalog entities located in Bitbucket. The processor will crawl your Bitbucket
account and register entities matching the configured path. This can be useful
as an alternative to static locations or manually adding things to the catalog.

> Note: The Bitbucket Discovery Processor currently only supports a self-hosted
> Bitbucket Server, and not the hosted Bitbucket Cloud product.
To use the discovery processor, you'll need a Bitbucket integration
[set up](locations.md) with a `BITBUCKET_TOKEN` and a `BITBUCKET_API_BASE_URL`.
Then you can add a location target to the catalog configuration:

```yaml
catalog:
locations:
- type: bitbucket-discovery
target: https://bitbucket.mycompany.com/projects/my-project/repos/service-*/catalog-info.yaml
```

Note the `bitbucket-discovery` type, as this is not a regular `url` processor.

The target is composed of four parts:

- The base instance URL, `https://bitbucket.mycompany.com` in this case
- The project key to scan, which accepts \* wildcard tokens. This can simply be
`*` to scan repositories from all projects. This example only scans for
repositories in the `my-project` project.
- The repository blob to scan, which accepts \* wildcard tokens. This can simply
be `*` to scan all repositories in the project. This example only looks for
repositories prefixed with `service-`.
- The path within each repository to find the catalog YAML file. This will
usually be `/catalog-info.yaml` or a similar variation for catalog files
stored in the root directory of each repository.
24 changes: 12 additions & 12 deletions docs/integrations/bitbucket/locations.md
@@ -1,12 +1,12 @@
---
id: locations
title: BitBucket Locations
title: Bitbucket Locations
sidebar_label: Locations
description:
Integrating source code stored in BitBucket into the Backstage catalog
Integrating source code stored in Bitbucket into the Backstage catalog
---

The BitBucket integration supports loading catalog entities from bitbucket.com
The Bitbucket integration supports loading catalog entities from bitbucket.org
or a self-hosted BitBucket. Entities can be added to
[static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
Expand All @@ -21,21 +21,21 @@ integrations:
token: ${BITBUCKET_TOKEN}
```

> Note: A public BitBucket provider is added automatically at startup for
> Note: A public Bitbucket provider is added automatically at startup for
> convenience, so you only need to list it if you want to supply a
> [token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html).
Directly under the `bitbucket` key is a list of provider configurations, where
you can list the BitBucket providers you want to fetch data from. Each entry is
you can list the Bitbucket providers you want to fetch data from. Each entry is
a structure with up to four elements:

- `host`: The host of the BitBucket instance, e.g. `bitbucket.company.com`.
- `token` (optional): An personal access token as expected by BitBucket. Either
- `host`: The host of the Bitbucket instance, e.g. `bitbucket.company.com`.
- `token` (optional): An personal access token as expected by Bitbucket. Either
an access token **or** a username + appPassword may be supplied.
- `username`: The BitBucket username to use in API requests. If neither a
- `username`: The Bitbucket username to use in API requests. If neither a
username nor token are supplied, anonymous access will be used.
- `appPassword` (optional): The password for the BitBucket user. Only needed
- `appPassword` (optional): The password for the Bitbucket user. Only needed
when using `username` instead of `token`.
- `apiBaseUrl` (optional): The URL of the GitLab API. For self-hosted
installations, it is commonly at `https://<host>/api/v4`. For gitlab.com, this
configuration is not needed as it can be inferred.
- `apiBaseUrl` (optional): The URL of the Bitbucket API. For self-hosted
installations, it is commonly at `https://<host>/rest/api/1.0`. For
bitbucket.org, this configuration is not needed as it can be inferred.
@@ -0,0 +1,218 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import {
BitbucketDiscoveryProcessor,
readBitbucketOrg,
} from './BitbucketDiscoveryProcessor';
import { ConfigReader } from '@backstage/config';
import { LocationSpec } from '@backstage/catalog-model';
import { BitbucketClient, PagedResponse } from './bitbucket';

function pagedResponse(values: any): PagedResponse<any> {
return {
values: values,
isLastPage: true,
} as PagedResponse<any>;
}

describe('BitbucketDiscoveryProcessor', () => {
const client: jest.Mocked<BitbucketClient> = {
listProjects: jest.fn(),
listRepositories: jest.fn(),
} as any;

afterEach(() => jest.resetAllMocks());

describe('reject unrelated entries', () => {
it('rejects unknown types', async () => {
const processor = BitbucketDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
bitbucket: [{ host: 'bitbucket.mycompany.com', token: 'blob' }],
},
}),
{ logger: getVoidLogger() },
);
const location: LocationSpec = {
type: 'not-bitbucket-discovery',
target: 'https://bitbucket.mycompany.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});

it('rejects unknown targets', async () => {
const processor = BitbucketDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
bitbucket: [
{ host: 'bitbucket.org', token: 'blob' },
{ host: 'bitbucket.mycompany.com', token: 'blob' },
],
},
}),
{ logger: getVoidLogger() },
);
const location: LocationSpec = {
type: 'bitbucket-discovery',
target: 'https://not.bitbucket.mycompany.com/foobar',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no Bitbucket integration that matches https:\/\/not.bitbucket.mycompany.com\/foobar/,
);
});
});

describe('handles repositories', () => {
it('output all repositories', async () => {
const target =
'https://bitbucket.mycompany.com/projects/*/repos/*/catalog.yaml';

client.listProjects.mockResolvedValue(
pagedResponse([{ key: 'backstage' }, { key: 'demo' }]),
);
client.listRepositories.mockResolvedValueOnce(
pagedResponse([
{
slug: 'backstage',
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse',
},
],
},
},
]),
);
client.listRepositories.mockResolvedValueOnce(
pagedResponse([
{
slug: 'demo',
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse',
},
],
},
},
]),
);

const actual = await readBitbucketOrg(client, target);
expect(actual.scanned).toBe(2);
expect(actual.matches).toContainEqual({
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse/catalog.yaml',
});
expect(actual.matches).toContainEqual({
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml',
});
});

it('output repositories with wildcards', async () => {
const target =
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-*/catalog.yaml';

client.listProjects.mockResolvedValue(
pagedResponse([{ key: 'backstage' }]),
);
client.listRepositories.mockResolvedValueOnce(
pagedResponse([
{ slug: 'backstage' },
{
slug: 'techdocs-cli',
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse',
},
],
},
},
{
slug: 'techdocs-container',
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse',
},
],
},
},
]),
);

const actual = await readBitbucketOrg(client, target);
expect(actual.scanned).toBe(3);
expect(actual.matches).toContainEqual({
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog.yaml',
});
expect(actual.matches).toContainEqual({
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse/catalog.yaml',
});
});
it('filter unrelated repositories', async () => {
const target =
'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml';

client.listProjects.mockResolvedValue(
pagedResponse([{ key: 'backstage' }]),
);
client.listRepositories.mockResolvedValue(
pagedResponse([
{ slug: 'abstest' },
{ slug: 'testxyz' },
{
slug: 'test',
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/backstage/repos/test',
},
],
},
},
]),
);

const actual = await readBitbucketOrg(client, target);
expect(actual.scanned).toBe(3);
expect(actual.matches).toContainEqual({
type: 'url',
target:
'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml',
});
});
});
});

0 comments on commit aa618e5

Please sign in to comment.