Added a ScmIntegration for Gerrit

A new ScmIntegration has been added for reading entities from
gits hosted by Gerrit. The UrlReader implementation will be done
in an upcoming patch.

The Gerrit configuration supports the following values:

* host (required) : The host of the gerrit instance to use.
* apiBaseUrl (required): The base url of the gerrit api.
* username (optional): The username to use during authentication.
* password (optional): The password or http token to use for
  authentication.

Signed-off-by: Niklas Aronsson <niklasar@axis.com>
This commit is contained in:
Niklas Aronsson
2022-03-07 15:10:27 +01:00
parent 9dc01b0081
commit 403837cbac
12 changed files with 519 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Added an integration for Gerrit
+36
View File
@@ -0,0 +1,36 @@
---
id: locations
title: Gerrit Locations
sidebar_label: Locations
description: Integrating source code stored in Gerrit into the Backstage catalog
---
The Gerrit integration supports loading catalog entities from Gerrit hosted gits. Entities can
be added to [static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
plugin.
## Configuration
To use this integration, add configuration to your root `app-config.yaml`:
```yaml
integrations:
gerrit:
- host: gerrit.company.com
apiBaseUrl: gerrit.company.com/gerrit
username: ${GERRIT_USERNAME}
password: ${GERRIT_PASSWORD}
```
Directly under the `gerrit` key is a list of provider configurations, where
you can list the Gerrit instances you want to fetch data from. Each entry is
a structure with up to four elements:
- `host`: The host of the Gerrit instance, e.g. `gerrit.company.com`.
- `apiBaseUrl`: The base url of the Gerrit API. This would typically be the address
up to but not including the authentication ("/a/") prefix.
- `username` (optional): The Gerrit username to use in API requests. If
neither a username nor password are supplied, anonymous access will be used.
- `password` (optional): The password or http token for the Gerrit user.
+26
View File
@@ -60,6 +60,32 @@ export interface Config {
appPassword?: string;
}>;
/** Integration configuration for Gerrit */
gerrit?: Array<{
/**
* The hostname of the given Gerrit instance
* @visibility frontend
*/
host: string;
/**
* The base url for the Gerrit API.
* @visibility frontend
*/
apiBaseUrl?: string;
/**
* The username to use for authenticated requests.
* @visibility secret
*/
username?: string;
/**
* Gerrit password used to authenticate requests. This can be either a password
* or a generated access token.
* .
* @visibility secret
*/
password?: string;
}>;
/** Integration configuration for GitHub */
github?: Array<{
/**
@@ -19,6 +19,8 @@ import { AzureIntegrationConfig } from './azure';
import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketIntegrationConfig } from './bitbucket';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
import { GerritIntegrationConfig } from './gerrit';
import { GerritIntegration } from './gerrit/GerritIntegration';
import { GitHubIntegrationConfig } from './github';
import { GitHubIntegration } from './github/GitHubIntegration';
import { GitLabIntegrationConfig } from './gitlab';
@@ -39,6 +41,10 @@ describe('ScmIntegrations', () => {
host: 'bitbucket.local',
} as BitbucketIntegrationConfig);
const gerrit = new GerritIntegration({
host: 'gerrit.local',
} as GerritIntegrationConfig);
const github = new GitHubIntegration({
host: 'github.local',
} as GitHubIntegrationConfig);
@@ -51,6 +57,7 @@ describe('ScmIntegrations', () => {
awsS3: basicIntegrations([awsS3], item => item.config.host),
azure: basicIntegrations([azure], item => item.config.host),
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
gerrit: basicIntegrations([gerrit], item => item.config.host),
github: basicIntegrations([github], item => item.config.host),
gitlab: basicIntegrations([gitlab], item => item.config.host),
});
@@ -59,13 +66,14 @@ describe('ScmIntegrations', () => {
expect(i.awsS3.byUrl('https://awss3.local')).toBe(awsS3);
expect(i.azure.byUrl('https://azure.local')).toBe(azure);
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
expect(i.gerrit.byUrl('https://gerrit.local')).toBe(gerrit);
expect(i.github.byUrl('https://github.local')).toBe(github);
expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab);
});
it('can list', () => {
expect(i.list()).toEqual(
expect.arrayContaining([awsS3, azure, bitbucket, github, gitlab]),
expect.arrayContaining([awsS3, azure, bitbucket, gerrit, github, gitlab]),
);
});
@@ -73,12 +81,14 @@ describe('ScmIntegrations', () => {
expect(i.byUrl('https://awss3.local')).toBe(awsS3);
expect(i.byUrl('https://azure.local')).toBe(azure);
expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket);
expect(i.byUrl('https://gerrit.local')).toBe(gerrit);
expect(i.byUrl('https://github.local')).toBe(github);
expect(i.byUrl('https://gitlab.local')).toBe(gitlab);
expect(i.byHost('awss3.local')).toBe(awsS3);
expect(i.byHost('azure.local')).toBe(azure);
expect(i.byHost('bitbucket.local')).toBe(bitbucket);
expect(i.byHost('gerrit.local')).toBe(gerrit);
expect(i.byHost('github.local')).toBe(github);
expect(i.byHost('gitlab.local')).toBe(gitlab);
});
@@ -18,6 +18,7 @@ import { Config } from '@backstage/config';
import { AwsS3Integration } from './awsS3/AwsS3Integration';
import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
import { GerritIntegration } from './gerrit/GerritIntegration';
import { GitHubIntegration } from './github/GitHubIntegration';
import { GitLabIntegration } from './gitlab/GitLabIntegration';
import { defaultScmResolveUrl } from './helpers';
@@ -33,6 +34,7 @@ export interface IntegrationsByType {
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
azure: ScmIntegrationsGroup<AzureIntegration>;
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
gerrit: ScmIntegrationsGroup<GerritIntegration>;
github: ScmIntegrationsGroup<GitHubIntegration>;
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
}
@@ -50,6 +52,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
awsS3: AwsS3Integration.factory({ config }),
azure: AzureIntegration.factory({ config }),
bitbucket: BitbucketIntegration.factory({ config }),
gerrit: GerritIntegration.factory({ config }),
github: GitHubIntegration.factory({ config }),
gitlab: GitLabIntegration.factory({ config }),
});
@@ -71,6 +74,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
return this.byType.bitbucket;
}
get gerrit(): ScmIntegrationsGroup<GerritIntegration> {
return this.byType.gerrit;
}
get github(): ScmIntegrationsGroup<GitHubIntegration> {
return this.byType.github;
}
@@ -0,0 +1,100 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ConfigReader } from '@backstage/config';
import { GerritIntegration } from './GerritIntegration';
describe('GerritIntegration', () => {
it('has a working factory', () => {
const integrations = GerritIntegration.factory({
config: new ConfigReader({
integrations: {
gerrit: [
{
host: 'gerrit-review.example.com',
username: 'gerrituser',
apiBaseUrl: 'https://gerrit-review.example.com/gerrit',
password: '1234',
},
],
},
}),
});
expect(integrations.list().length).toBe(1);
expect(integrations.list()[0].config.host).toBe(
'gerrit-review.example.com',
);
});
it('returns the basics', () => {
const integration = new GerritIntegration({
host: 'gerrit-review.example.com',
apiBaseUrl: 'https://gerrit-review.example.com/gerrit',
} as any);
expect(integration.type).toBe('gerrit');
expect(integration.title).toBe('gerrit-review.example.com');
});
describe('resolveUrl', () => {
it('works for valid urls', () => {
const integration = new GerritIntegration({
host: 'gerrit-review.example.com',
apiBaseUrl: 'https://gerrit-review.example.com/gerrit',
} as any);
expect(
integration.resolveUrl({
url: 'https://gerrit-review.example.com/catalog-info.yaml',
base: 'https://gerrit-review.example.com/catalog-info.yaml',
lineNumber: 9,
}),
).toBe('https://gerrit-review.example.com/catalog-info.yaml#9');
});
});
describe('resolves with a relative url', () => {
it('works for valid urls', () => {
const integration = new GerritIntegration({
host: 'gerrit-review.example.com',
apiBaseUrl: 'https://gerrit-review.example.com/gerrit',
} as any);
expect(
integration.resolveUrl({
url: './skeleton',
base: 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/heads/master/template.yaml',
}),
).toBe(
'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/heads/master/skeleton',
);
});
});
it('resolve edit URL', () => {
const integration = new GerritIntegration({
host: 'gerrit-review.example.com',
apiBaseUrl: 'https://gerrit-review.example.com/gerrit',
} as any);
// Resolve edit URLs is not applicable for gerrit. Return the input
// url as is.
expect(
integration.resolveEditUrl(
'https://gerrit-review.example.com/catalog-info.yaml',
),
).toBe('https://gerrit-review.example.com/catalog-info.yaml');
});
});
@@ -0,0 +1,76 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { basicIntegrations } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
GerritIntegrationConfig,
readGerritIntegrationConfigs,
} from './config';
/**
* A Gerrit based integration.
*
* @public
*/
export class GerritIntegration implements ScmIntegration {
static factory: ScmIntegrationsFactory<GerritIntegration> = ({ config }) => {
const configs = readGerritIntegrationConfigs(
config.getOptionalConfigArray('integrations.gerrit') ?? [],
);
return basicIntegrations(
configs.map(c => new GerritIntegration(c)),
i => i.config.host ?? '',
);
};
constructor(private readonly integrationConfig: GerritIntegrationConfig) {}
get type(): string {
return 'gerrit';
}
get title(): string {
return this.integrationConfig.host;
}
get config(): GerritIntegrationConfig {
return this.integrationConfig;
}
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
const { url, base, lineNumber } = options;
let updated;
if (url) {
updated = new URL(url, base).toString();
} else {
updated = base;
}
if (lineNumber) {
return `${updated}#${lineNumber}`;
}
return updated;
}
resolveEditUrl(url: string): string {
// Not applicable for gerrit.
return url;
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { Config, ConfigReader } from '@backstage/config';
import { loadConfigSchema } from '@backstage/config-loader';
import {
GerritIntegrationConfig,
readGerritIntegrationConfig,
readGerritIntegrationConfigs,
} from './config';
describe('readGerritIntegrationConfig', () => {
function buildConfig(data: Partial<GerritIntegrationConfig>): Config {
return new ConfigReader(data);
}
async function buildFrontendConfig(
data: Partial<GerritIntegrationConfig>,
): Promise<Config> {
const fullSchema = await loadConfigSchema({
dependencies: ['@backstage/integration'],
});
const serializedSchema = fullSchema.serialize() as {
schemas: { value: { properties?: { integrations?: object } } }[];
};
const schema = await loadConfigSchema({
serialized: {
...serializedSchema, // only include schemas that apply to integrations
schemas: serializedSchema.schemas.filter(
s => s.value?.properties?.integrations,
),
},
});
const processed = schema.process(
[{ data: { integrations: { gerrit: [data] } }, context: 'app' }],
{ visibility: ['frontend'] },
);
return new ConfigReader((processed[0].data as any).integrations.gerrit[0]);
}
it('reads all values', () => {
const output = readGerritIntegrationConfig(
buildConfig({
host: 'a.com',
apiBaseUrl: 'https://a.com/api',
username: 'u',
password: 'p',
}),
);
expect(output).toEqual({
host: 'a.com',
apiBaseUrl: 'https://a.com/api',
username: 'u',
password: 'p',
});
});
it('rejects funky configs', () => {
const valid: any = {
host: 'a.com',
apiBaseUrl: 'https://a.com/api',
username: 'u',
appPassword: 'p',
};
expect(() =>
readGerritIntegrationConfig(buildConfig({ ...valid, host: 2 })),
).toThrow(/host/);
expect(() =>
readGerritIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 2 })),
).toThrow(/apiBaseUrl/);
});
it('works on the frontend', async () => {
expect(
readGerritIntegrationConfig(
await buildFrontendConfig({
host: 'a.com',
apiBaseUrl: 'https://a.com/gerrit',
username: 'u',
password: 'p',
}),
),
).toEqual({
host: 'a.com',
apiBaseUrl: 'https://a.com/gerrit',
});
});
});
describe('readGerritIntegrationConfigs', () => {
function buildConfig(data: Partial<GerritIntegrationConfig>[]): Config[] {
return data.map(item => new ConfigReader(item));
}
it('reads all values', () => {
const output = readGerritIntegrationConfigs(
buildConfig([
{
host: 'a.com',
apiBaseUrl: 'https://a.com/api',
username: 'u',
password: 'p',
},
{
host: 'b.com',
apiBaseUrl: 'https://b.com/api',
},
]),
);
expect(output).toEqual([
{
host: 'a.com',
apiBaseUrl: 'https://a.com/api',
username: 'u',
password: 'p',
},
{
host: 'b.com',
apiBaseUrl: 'https://b.com/api',
username: undefined,
password: undefined,
},
]);
});
});
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { Config } from '@backstage/config';
import { trimEnd } from 'lodash';
import { isValidHost, isValidUrl } from '../helpers';
/**
* The configuration parameters for a single Gerrit API provider.
*
* @public
*/
export type GerritIntegrationConfig = {
/**
* The host of the target that this matches on, e.g. "gerrit-review.com"
*/
host: string;
/**
* The base URL of the API of this provider, e.g. "https://gerrit-review.com/gerrit",
* with no trailing slash.
*/
apiBaseUrl: string;
/**
* The username to use for requests to gerrit.
*/
username?: string;
/**
* The password or http token to use for authentication.
*/
password?: string;
};
/**
* Reads a single Gerrit integration config.
*
* @param config - The config object of a single integration
*
* @public
*/
export function readGerritIntegrationConfig(
config: Config,
): GerritIntegrationConfig {
const host = config.getString('host');
let apiBaseUrl = config.getString('apiBaseUrl');
const username = config.getOptionalString('username');
const password = config.getOptionalString('password');
if (!isValidHost(host)) {
throw new Error(
`Invalid Gerrit integration config, '${host}' is not a valid host`,
);
} else if (!apiBaseUrl || !isValidUrl(apiBaseUrl)) {
throw new Error(
`Invalid Gerrit integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`,
);
}
if (apiBaseUrl) {
apiBaseUrl = trimEnd(apiBaseUrl, '/');
}
return {
host,
apiBaseUrl,
username,
password,
};
}
/**
* Reads a set of Gerrit integration configs.
*
* @param configs - All of the integration config objects
*
* @public
*/
export function readGerritIntegrationConfigs(
configs: Config[],
): GerritIntegrationConfig[] {
return configs.map(readGerritIntegrationConfig);
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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.
*/
export { GerritIntegration } from './GerritIntegration';
export {
readGerritIntegrationConfig,
readGerritIntegrationConfigs,
} from './config';
export type { GerritIntegrationConfig } from './config';
+1
View File
@@ -22,6 +22,7 @@
export * from './azure';
export * from './bitbucket';
export * from './gerrit';
export * from './github';
export * from './gitlab';
export * from './googleGcs';
+2
View File
@@ -18,6 +18,7 @@ import { ScmIntegration, ScmIntegrationsGroup } from './types';
import { AwsS3Integration } from './awsS3/AwsS3Integration';
import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
import { GerritIntegration } from './gerrit/GerritIntegration';
import { GitHubIntegration } from './github/GitHubIntegration';
import { GitLabIntegration } from './gitlab/GitLabIntegration';
@@ -31,6 +32,7 @@ export interface ScmIntegrationRegistry
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
azure: ScmIntegrationsGroup<AzureIntegration>;
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
gerrit: ScmIntegrationsGroup<GerritIntegration>;
github: ScmIntegrationsGroup<GitHubIntegration>;
gitlab: ScmIntegrationsGroup<GitLabIntegration>;