From 403837cbac894f8aeb4299bada1defb660e72f3b Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Mon, 7 Mar 2022 15:10:27 +0100 Subject: [PATCH 1/5] 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 --- .changeset/new-maps-complain.md | 5 + docs/integrations/gerrit/locations.md | 36 +++++ packages/integration/config.d.ts | 26 ++++ .../integration/src/ScmIntegrations.test.ts | 12 +- packages/integration/src/ScmIntegrations.ts | 7 + .../src/gerrit/GerritIntegration.test.ts | 100 +++++++++++++ .../src/gerrit/GerritIntegration.ts | 76 ++++++++++ .../integration/src/gerrit/config.test.ts | 138 ++++++++++++++++++ packages/integration/src/gerrit/config.ts | 96 ++++++++++++ packages/integration/src/gerrit/index.ts | 21 +++ packages/integration/src/index.ts | 1 + packages/integration/src/registry.ts | 2 + 12 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 .changeset/new-maps-complain.md create mode 100644 docs/integrations/gerrit/locations.md create mode 100644 packages/integration/src/gerrit/GerritIntegration.test.ts create mode 100644 packages/integration/src/gerrit/GerritIntegration.ts create mode 100644 packages/integration/src/gerrit/config.test.ts create mode 100644 packages/integration/src/gerrit/config.ts create mode 100644 packages/integration/src/gerrit/index.ts diff --git a/.changeset/new-maps-complain.md b/.changeset/new-maps-complain.md new file mode 100644 index 0000000000..199a0c3280 --- /dev/null +++ b/.changeset/new-maps-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Added an integration for Gerrit diff --git a/docs/integrations/gerrit/locations.md b/docs/integrations/gerrit/locations.md new file mode 100644 index 0000000000..3e76d966ad --- /dev/null +++ b/docs/integrations/gerrit/locations.md @@ -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. diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 8873f0f220..06ab359e4a 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -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<{ /** diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index f0a6e5d72d..2e6f2f1472 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -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); }); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index add9e4e016..8638b814ec 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -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; azure: ScmIntegrationsGroup; bitbucket: ScmIntegrationsGroup; + gerrit: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; } @@ -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 { + return this.byType.gerrit; + } + get github(): ScmIntegrationsGroup { return this.byType.github; } diff --git a/packages/integration/src/gerrit/GerritIntegration.test.ts b/packages/integration/src/gerrit/GerritIntegration.test.ts new file mode 100644 index 0000000000..5b31b975f3 --- /dev/null +++ b/packages/integration/src/gerrit/GerritIntegration.test.ts @@ -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'); + }); +}); diff --git a/packages/integration/src/gerrit/GerritIntegration.ts b/packages/integration/src/gerrit/GerritIntegration.ts new file mode 100644 index 0000000000..791adc318d --- /dev/null +++ b/packages/integration/src/gerrit/GerritIntegration.ts @@ -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 = ({ 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; + } +} diff --git a/packages/integration/src/gerrit/config.test.ts b/packages/integration/src/gerrit/config.test.ts new file mode 100644 index 0000000000..3377a3d0ff --- /dev/null +++ b/packages/integration/src/gerrit/config.test.ts @@ -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): Config { + return new ConfigReader(data); + } + + async function buildFrontendConfig( + data: Partial, + ): Promise { + 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[]): 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, + }, + ]); + }); +}); diff --git a/packages/integration/src/gerrit/config.ts b/packages/integration/src/gerrit/config.ts new file mode 100644 index 0000000000..d118e22278 --- /dev/null +++ b/packages/integration/src/gerrit/config.ts @@ -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); +} diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts new file mode 100644 index 0000000000..baad597a22 --- /dev/null +++ b/packages/integration/src/gerrit/index.ts @@ -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'; diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 2f33b76ab3..cf0eeca51a 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -22,6 +22,7 @@ export * from './azure'; export * from './bitbucket'; +export * from './gerrit'; export * from './github'; export * from './gitlab'; export * from './googleGcs'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 5864695e25..5ab5d049fb 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -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; azure: ScmIntegrationsGroup; bitbucket: ScmIntegrationsGroup; + gerrit: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; From ee04b48b8fb72b6a25e6ebd348e88bb20447f42d Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Thu, 10 Mar 2022 08:09:17 +0100 Subject: [PATCH 2/5] packages/integration: Updating "api-report.md" for Gerrit Signed-off-by: Niklas Aronsson --- packages/integration/api-report.md | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 9a6ba03f8e..2b3f99fd2c 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -112,6 +112,35 @@ export function defaultScmResolveUrl(options: { lineNumber?: number; }): string; +// @public +export class GerritIntegration implements ScmIntegration { + constructor(integrationConfig: GerritIntegrationConfig); + // (undocumented) + get config(): GerritIntegrationConfig; + // (undocumented) + static factory: ScmIntegrationsFactory; + // (undocumented) + resolveEditUrl(url: string): string; + // (undocumented) + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number; + }): string; + // (undocumented) + get title(): string; + // (undocumented) + get type(): string; +} + +// @public +export type GerritIntegrationConfig = { + host: string; + apiBaseUrl: string; + username?: string; + password?: string; +}; + // @public export function getAzureCommitsUrl(url: string): string; @@ -293,6 +322,8 @@ export interface IntegrationsByType { // (undocumented) bitbucket: ScmIntegrationsGroup; // (undocumented) + gerrit: ScmIntegrationsGroup; + // (undocumented) github: ScmIntegrationsGroup; // (undocumented) gitlab: ScmIntegrationsGroup; @@ -328,6 +359,16 @@ export function readBitbucketIntegrationConfigs( configs: Config[], ): BitbucketIntegrationConfig[]; +// @public +export function readGerritIntegrationConfig( + config: Config, +): GerritIntegrationConfig; + +// @public +export function readGerritIntegrationConfigs( + configs: Config[], +): GerritIntegrationConfig[]; + // @public export function readGitHubIntegrationConfig( config: Config, @@ -381,6 +422,8 @@ export interface ScmIntegrationRegistry // (undocumented) bitbucket: ScmIntegrationsGroup; // (undocumented) + gerrit: ScmIntegrationsGroup; + // (undocumented) github: ScmIntegrationsGroup; // (undocumented) gitlab: ScmIntegrationsGroup; @@ -408,6 +451,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry { // (undocumented) static fromConfig(config: Config): ScmIntegrations; // (undocumented) + get gerrit(): ScmIntegrationsGroup; + // (undocumented) get github(): ScmIntegrationsGroup; // (undocumented) get gitlab(): ScmIntegrationsGroup; From 02c84fb32f5bdf535c4690d8c58c44fb15d919ad Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Thu, 10 Mar 2022 08:15:12 +0100 Subject: [PATCH 3/5] Added "Gerrit" to "vocab.txt" Signed-off-by: Niklas Aronsson --- .github/styles/vocab.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6ed539530d..7094bf20d3 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -101,6 +101,7 @@ FireHydrant Firekube Firestore Fiverr +Gerrit gitbeaker GitHub GitLab From b0c0fccacf91f28d401099c55aff147d95882f73 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Fri, 11 Mar 2022 13:30:35 +0100 Subject: [PATCH 4/5] Gerrit config: Make the apiBaseUrl optional If the apiBaseUrl is not set assume that the gerrit instance uses https and can be reached on the address specified by the "host" option. Signed-off-by: Niklas Aronsson --- docs/integrations/gerrit/locations.md | 5 +++-- mkdocs.yml | 2 ++ packages/integration/api-report.md | 2 +- packages/integration/config.d.ts | 2 +- packages/integration/src/gerrit/config.test.ts | 17 ++++++++++++++--- packages/integration/src/gerrit/config.ts | 15 ++++++++++----- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/docs/integrations/gerrit/locations.md b/docs/integrations/gerrit/locations.md index 3e76d966ad..747e3f2404 100644 --- a/docs/integrations/gerrit/locations.md +++ b/docs/integrations/gerrit/locations.md @@ -29,8 +29,9 @@ 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. +- `apiBaseUrl` (optional): Needed if the Gerrit instance is not reachable at + the base of the `host` option (e.g. `https://gerrit.company.com`). This is + the address that you would open in a browser. - `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. diff --git a/mkdocs.yml b/mkdocs.yml index 4671a10b7a..05c492a077 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -97,6 +97,8 @@ nav: - Discovery: 'integrations/bitbucket/discovery.md' - Datadog: - Installation: 'integrations/datadog-rum/installation.md' + - Gerrit: + - Locations: 'integrations/gerrit/locations.md' - GitHub: - Locations: 'integrations/github/locations.md' - Discovery: 'integrations/github/discovery.md' diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 2b3f99fd2c..9a93c30316 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -136,7 +136,7 @@ export class GerritIntegration implements ScmIntegration { // @public export type GerritIntegrationConfig = { host: string; - apiBaseUrl: string; + apiBaseUrl?: string; username?: string; password?: string; }; diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 06ab359e4a..90b8b9b759 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -68,7 +68,7 @@ export interface Config { */ host: string; /** - * The base url for the Gerrit API. + * The base url for the Gerrit instance. * @visibility frontend */ apiBaseUrl?: string; diff --git a/packages/integration/src/gerrit/config.test.ts b/packages/integration/src/gerrit/config.test.ts index 3377a3d0ff..f448b76b0b 100644 --- a/packages/integration/src/gerrit/config.test.ts +++ b/packages/integration/src/gerrit/config.test.ts @@ -68,12 +68,23 @@ describe('readGerritIntegrationConfig', () => { }); }); + it('can create a default value if the API base URL is missing', () => { + const output = readGerritIntegrationConfig( + buildConfig({ + host: 'a.com', + }), + ); + expect(output).toEqual({ + host: 'a.com', + apiBaseUrl: 'https://a.com', + username: undefined, + password: undefined, + }); + }); + 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 })), diff --git a/packages/integration/src/gerrit/config.ts b/packages/integration/src/gerrit/config.ts index d118e22278..c376aef98f 100644 --- a/packages/integration/src/gerrit/config.ts +++ b/packages/integration/src/gerrit/config.ts @@ -30,10 +30,13 @@ export type GerritIntegrationConfig = { host: string; /** - * The base URL of the API of this provider, e.g. "https://gerrit-review.com/gerrit", - * with no trailing slash. + * The optional base URL of the Gerrit instance. It is assumed that https + * is used and that the base path is "/" on the host. If that is not the + * case set the complete base url to the gerrit instance, e.g. + * "https://gerrit-review.com/gerrit". This is the url that you would open + * in a browser. */ - apiBaseUrl: string; + apiBaseUrl?: string; /** * The username to use for requests to gerrit. @@ -57,7 +60,7 @@ export function readGerritIntegrationConfig( config: Config, ): GerritIntegrationConfig { const host = config.getString('host'); - let apiBaseUrl = config.getString('apiBaseUrl'); + let apiBaseUrl = config.getOptionalString('apiBaseUrl'); const username = config.getOptionalString('username'); const password = config.getOptionalString('password'); @@ -65,13 +68,15 @@ export function readGerritIntegrationConfig( throw new Error( `Invalid Gerrit integration config, '${host}' is not a valid host`, ); - } else if (!apiBaseUrl || !isValidUrl(apiBaseUrl)) { + } else if (apiBaseUrl && !isValidUrl(apiBaseUrl)) { throw new Error( `Invalid Gerrit integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`, ); } if (apiBaseUrl) { apiBaseUrl = trimEnd(apiBaseUrl, '/'); + } else { + apiBaseUrl = `https://${host}`; } return { From 0885854ff329893b5014e9e8a3ef5d77d1e49a05 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Mon, 14 Mar 2022 10:17:34 +0100 Subject: [PATCH 5/5] Fixed review comments * Rename "apiBaseUrl" to the more suitable "baseUrl". * Removed the Gerrit location doc (will be added later together with the "urlReader" implementation. * Fixed the line number handling in Gerrit/resolveUrl. Signed-off-by: Niklas Aronsson --- docs/integrations/gerrit/locations.md | 37 ------------------- mkdocs.yml | 2 - packages/integration/api-report.md | 2 +- packages/integration/config.d.ts | 3 +- .../src/gerrit/GerritIntegration.test.ts | 23 +++++++++--- .../src/gerrit/GerritIntegration.ts | 10 ++--- .../integration/src/gerrit/config.test.ts | 22 +++++------ packages/integration/src/gerrit/config.ts | 16 ++++---- 8 files changed, 44 insertions(+), 71 deletions(-) delete mode 100644 docs/integrations/gerrit/locations.md diff --git a/docs/integrations/gerrit/locations.md b/docs/integrations/gerrit/locations.md deleted file mode 100644 index 747e3f2404..0000000000 --- a/docs/integrations/gerrit/locations.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -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` (optional): Needed if the Gerrit instance is not reachable at - the base of the `host` option (e.g. `https://gerrit.company.com`). This is - the address that you would open in a browser. -- `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. diff --git a/mkdocs.yml b/mkdocs.yml index 05c492a077..4671a10b7a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -97,8 +97,6 @@ nav: - Discovery: 'integrations/bitbucket/discovery.md' - Datadog: - Installation: 'integrations/datadog-rum/installation.md' - - Gerrit: - - Locations: 'integrations/gerrit/locations.md' - GitHub: - Locations: 'integrations/github/locations.md' - Discovery: 'integrations/github/discovery.md' diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 9a93c30316..84bca54a90 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -136,7 +136,7 @@ export class GerritIntegration implements ScmIntegration { // @public export type GerritIntegrationConfig = { host: string; - apiBaseUrl?: string; + baseUrl?: string; username?: string; password?: string; }; diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 90b8b9b759..dc62695385 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -71,7 +71,7 @@ export interface Config { * The base url for the Gerrit instance. * @visibility frontend */ - apiBaseUrl?: string; + baseUrl?: string; /** * The username to use for authenticated requests. * @visibility secret @@ -80,7 +80,6 @@ export interface Config { /** * Gerrit password used to authenticate requests. This can be either a password * or a generated access token. - * . * @visibility secret */ password?: string; diff --git a/packages/integration/src/gerrit/GerritIntegration.test.ts b/packages/integration/src/gerrit/GerritIntegration.test.ts index 5b31b975f3..6b5e228bb6 100644 --- a/packages/integration/src/gerrit/GerritIntegration.test.ts +++ b/packages/integration/src/gerrit/GerritIntegration.test.ts @@ -26,7 +26,7 @@ describe('GerritIntegration', () => { { host: 'gerrit-review.example.com', username: 'gerrituser', - apiBaseUrl: 'https://gerrit-review.example.com/gerrit', + baseUrl: 'https://gerrit-review.example.com/gerrit', password: '1234', }, ], @@ -37,12 +37,14 @@ describe('GerritIntegration', () => { expect(integrations.list()[0].config.host).toBe( 'gerrit-review.example.com', ); + expect(integrations.list()[0].config.baseUrl).toBe( + 'https://gerrit-review.example.com/gerrit', + ); }); 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'); @@ -52,7 +54,6 @@ describe('GerritIntegration', () => { it('works for valid urls', () => { const integration = new GerritIntegration({ host: 'gerrit-review.example.com', - apiBaseUrl: 'https://gerrit-review.example.com/gerrit', } as any); expect( @@ -63,13 +64,26 @@ describe('GerritIntegration', () => { }), ).toBe('https://gerrit-review.example.com/catalog-info.yaml#9'); }); + + it('handles line numbers', () => { + const integration = new GerritIntegration({ + host: 'gerrit-review.example.com', + } as any); + + expect( + integration.resolveUrl({ + url: '', + base: 'https://gerrit-review.example.com/catalog-info.yaml#4', + 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( @@ -86,7 +100,6 @@ describe('GerritIntegration', () => { 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 diff --git a/packages/integration/src/gerrit/GerritIntegration.ts b/packages/integration/src/gerrit/GerritIntegration.ts index 791adc318d..97eb9372c0 100644 --- a/packages/integration/src/gerrit/GerritIntegration.ts +++ b/packages/integration/src/gerrit/GerritIntegration.ts @@ -33,7 +33,7 @@ export class GerritIntegration implements ScmIntegration { ); return basicIntegrations( configs.map(c => new GerritIntegration(c)), - i => i.config.host ?? '', + i => i.config.host, ); }; @@ -59,14 +59,14 @@ export class GerritIntegration implements ScmIntegration { const { url, base, lineNumber } = options; let updated; if (url) { - updated = new URL(url, base).toString(); + updated = new URL(url, base); } else { - updated = base; + updated = new URL(base); } if (lineNumber) { - return `${updated}#${lineNumber}`; + updated.hash = lineNumber.toString(); } - return updated; + return updated.toString(); } resolveEditUrl(url: string): string { diff --git a/packages/integration/src/gerrit/config.test.ts b/packages/integration/src/gerrit/config.test.ts index f448b76b0b..ce1ddccd75 100644 --- a/packages/integration/src/gerrit/config.test.ts +++ b/packages/integration/src/gerrit/config.test.ts @@ -55,14 +55,14 @@ describe('readGerritIntegrationConfig', () => { const output = readGerritIntegrationConfig( buildConfig({ host: 'a.com', - apiBaseUrl: 'https://a.com/api', + baseUrl: 'https://a.com/api', username: 'u', password: 'p', }), ); expect(output).toEqual({ host: 'a.com', - apiBaseUrl: 'https://a.com/api', + baseUrl: 'https://a.com/api', username: 'u', password: 'p', }); @@ -76,7 +76,7 @@ describe('readGerritIntegrationConfig', () => { ); expect(output).toEqual({ host: 'a.com', - apiBaseUrl: 'https://a.com', + baseUrl: 'https://a.com', username: undefined, password: undefined, }); @@ -90,8 +90,8 @@ describe('readGerritIntegrationConfig', () => { readGerritIntegrationConfig(buildConfig({ ...valid, host: 2 })), ).toThrow(/host/); expect(() => - readGerritIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 2 })), - ).toThrow(/apiBaseUrl/); + readGerritIntegrationConfig(buildConfig({ ...valid, baseUrl: 2 })), + ).toThrow(/baseUrl/); }); it('works on the frontend', async () => { @@ -99,14 +99,14 @@ describe('readGerritIntegrationConfig', () => { readGerritIntegrationConfig( await buildFrontendConfig({ host: 'a.com', - apiBaseUrl: 'https://a.com/gerrit', + baseUrl: 'https://a.com/gerrit', username: 'u', password: 'p', }), ), ).toEqual({ host: 'a.com', - apiBaseUrl: 'https://a.com/gerrit', + baseUrl: 'https://a.com/gerrit', }); }); }); @@ -121,26 +121,26 @@ describe('readGerritIntegrationConfigs', () => { buildConfig([ { host: 'a.com', - apiBaseUrl: 'https://a.com/api', + baseUrl: 'https://a.com/api', username: 'u', password: 'p', }, { host: 'b.com', - apiBaseUrl: 'https://b.com/api', + baseUrl: 'https://b.com/api', }, ]), ); expect(output).toEqual([ { host: 'a.com', - apiBaseUrl: 'https://a.com/api', + baseUrl: 'https://a.com/api', username: 'u', password: 'p', }, { host: 'b.com', - apiBaseUrl: 'https://b.com/api', + baseUrl: 'https://b.com/api', username: undefined, password: undefined, }, diff --git a/packages/integration/src/gerrit/config.ts b/packages/integration/src/gerrit/config.ts index c376aef98f..339b590c11 100644 --- a/packages/integration/src/gerrit/config.ts +++ b/packages/integration/src/gerrit/config.ts @@ -36,7 +36,7 @@ export type GerritIntegrationConfig = { * "https://gerrit-review.com/gerrit". This is the url that you would open * in a browser. */ - apiBaseUrl?: string; + baseUrl?: string; /** * The username to use for requests to gerrit. @@ -60,7 +60,7 @@ export function readGerritIntegrationConfig( config: Config, ): GerritIntegrationConfig { const host = config.getString('host'); - let apiBaseUrl = config.getOptionalString('apiBaseUrl'); + let baseUrl = config.getOptionalString('baseUrl'); const username = config.getOptionalString('username'); const password = config.getOptionalString('password'); @@ -68,20 +68,20 @@ export function readGerritIntegrationConfig( throw new Error( `Invalid Gerrit integration config, '${host}' is not a valid host`, ); - } else if (apiBaseUrl && !isValidUrl(apiBaseUrl)) { + } else if (baseUrl && !isValidUrl(baseUrl)) { throw new Error( - `Invalid Gerrit integration config, '${apiBaseUrl}' is not a valid apiBaseUrl`, + `Invalid Gerrit integration config, '${baseUrl}' is not a valid baseUrl`, ); } - if (apiBaseUrl) { - apiBaseUrl = trimEnd(apiBaseUrl, '/'); + if (baseUrl) { + baseUrl = trimEnd(baseUrl, '/'); } else { - apiBaseUrl = `https://${host}`; + baseUrl = `https://${host}`; } return { host, - apiBaseUrl, + baseUrl, username, password, };