diff --git a/.changeset/old-bikes-study.md b/.changeset/old-bikes-study.md new file mode 100644 index 0000000000..f2452fcce3 --- /dev/null +++ b/.changeset/old-bikes-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Gerrit Integration: Added the `getGerritProjectsApiUrl` function diff --git a/.changeset/perfect-penguins-rescue.md b/.changeset/perfect-penguins-rescue.md new file mode 100644 index 0000000000..ab55355121 --- /dev/null +++ b/.changeset/perfect-penguins-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gerrit': minor +--- + +Initial version of the `plugin-catalog-backend-module-gerrit` plugin diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 95e54de3e0..003979a1a7 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -106,6 +106,7 @@ Firekube Firestore Fiverr Francesco +gerrit Gerrit gitbeaker github diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md new file mode 100644 index 0000000000..5cd1f6204b --- /dev/null +++ b/docs/integrations/gerrit/discovery.md @@ -0,0 +1,69 @@ +--- +id: discovery +title: Gerrit Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from Gerrit repositories +--- + +The Gerrit integration has a special entity provider for discovering catalog entities +from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get +a list of repositories and will automatically ingest all `catalog-info.yaml` files +stored in the root of the matching projects. + +## Installation + +As this provider is not one of the default providers, you will first need to install +the Gerrit provider plugin: + +```bash +# From the Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gerrit +``` + +Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: + +```ts +/* packages/backend/src/plugins/catalog.ts */ +import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit'; +import { Duration } from 'luxon'; +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + ...GerritEntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), +); +``` + +## Configuration + +To use the discovery processor, you'll need a Gerrit integration +[set up](locations.md). Then you can add any number of providers. + +```yaml +# app-config.yaml +catalog: + providers: + gerrit: + yourProviderId: # identifies your dataset / provider independent of config changes + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=webapps' + backend: + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=backend' +``` + +The provider configuration is composed of three parts: + +- host, the host of the Gerrit integration to use. +- branch, the branch where we will look for catalog entities (defaults to "master"). +- query, this string is directly used as the argument to the "List Project" API. + Typically you will want to have some filter here to exclude projects that will + never contain any catalog files. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index db6d903086..71911bed2b 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -156,7 +156,10 @@ { "type": "subcategory", "label": "Gerrit", - "ids": ["integrations/gerrit/locations"] + "ids": [ + "integrations/gerrit/locations", + "integrations/gerrit/discovery" + ] }, { "type": "subcategory", diff --git a/mkdocs.yml b/mkdocs.yml index af677a91a8..26e18fbfc0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,6 +101,7 @@ nav: - Installation: 'integrations/datadog-rum/installation.md' - Gerrit: - Locations: 'integrations/gerrit/locations.md' + - Discovery: 'integrations/gerrit/discovery.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 b99a55647c..e208bd9062 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -298,6 +298,11 @@ export function getGerritFileContentsApiUrl( url: string, ): string; +// @public +export function getGerritProjectsApiUrl( + config: GerritIntegrationConfig, +): string; + // @public export function getGerritRequestOptions(config: GerritIntegrationConfig): { headers?: Record; diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 517d27d3ec..7b90df262c 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -97,6 +97,7 @@ export function getAuthenticationPrefix( /** * Return the url to fetch the contents of a file using the Gerrit API. * + * @param config - A Gerrit provider config. * @param url - An url pointing to a file in git. * @public */ @@ -113,6 +114,16 @@ export function getGerritFileContentsApiUrl( )}/branches/${branch}/files/${encodeURIComponent(filePath)}/content`; } +/** + * Return the url to query available projects using the Gerrit API. + * + * @param config - A Gerrit provider config. + * @public + */ +export function getGerritProjectsApiUrl(config: GerritIntegrationConfig) { + return `${config.baseUrl}${getAuthenticationPrefix(config)}projects/`; +} + /** * Return request headers for a Gerrit provider. * diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 75e9779dbc..1c54a270ac 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -20,6 +20,7 @@ export { } from './config'; export { getGerritFileContentsApiUrl, + getGerritProjectsApiUrl, getGerritRequestOptions, parseGerritJsonResponse, } from './core'; diff --git a/plugins/catalog-backend-module-gerrit/.eslintrc.js b/plugins/catalog-backend-module-gerrit/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-gerrit/README.md b/plugins/catalog-backend-module-gerrit/README.md new file mode 100644 index 0000000000..8b0e0f0c69 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for Gerrit + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at Gerrit integrations. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/gerrit/discovery.md) +for details on how to install and configure the plugin. diff --git a/plugins/catalog-backend-module-gerrit/api-report.md b/plugins/catalog-backend-module-gerrit/api-report.md new file mode 100644 index 0000000000..7eae94c2a8 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gerrit" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { TaskRunner } from '@backstage/backend-tasks'; + +// @public (undocumented) +export class GerritEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): GerritEntityProvider[]; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(logger: Logger): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-gerrit/config.d.ts b/plugins/catalog-backend-module-gerrit/config.d.ts new file mode 100644 index 0000000000..14e8f58d8f --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/config.d.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +interface GerritConfig { + /** + * (Required) The host of the Gerrit integration to use. + * @visibility backend + */ + host: string; + /** + * (Required) The query to use for the "List Projects" API call. Used to limit the + * scope of the projects that the provider tries to ingest. + * @visibility backend + */ + query: string; + /** + * (Optional) Branch. + * The branch where the provider will try to find entities. Defaults to "master". + * @visibility backend + */ + branch?: string; +} + +export interface Config { + catalog?: { + /** + * List of provider-specific options and attributes + */ + providers?: { + /** + * GerritEntityProvider configuration + * + * Maps provider id with configuration. + */ + gerrit?: Record; + }; + }; +} diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json new file mode 100644 index 0000000000..03a3eb4d00 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -0,0 +1,54 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-gerrit", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-gerrit" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.2", + "@backstage/backend-tasks": "^0.3.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/config": "^1.0.0", + "@backstage/errors": "^1.0.0", + "@backstage/integration": "^1.1.0", + "@backstage/plugin-catalog-backend": "^1.1.0", + "fs-extra": "10.0.1", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "uuid": "^8.0.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.23", + "@backstage/cli": "^0.17.0", + "@types/fs-extra": "^9.0.1" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/catalog-backend-module-gerrit/src/index.ts b/plugins/catalog-backend-module-gerrit/src/index.ts new file mode 100644 index 0000000000..133772d4b1 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { GerritEntityProvider } from './providers/GerritEntityProvider'; diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts new file mode 100644 index 0000000000..ee65d536c7 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts @@ -0,0 +1,206 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { rest } from 'msw'; +import fs from 'fs-extra'; +import path from 'path'; +import { setupServer } from 'msw/node'; +import { GerritEntityProvider } from './GerritEntityProvider'; + +const server = setupServer(); + +const getJsonFixture = (fileName: string) => + JSON.parse( + fs.readFileSync( + path.resolve(__dirname, `__fixtures__/${fileName}`), + 'utf8', + ), + ); + +class PersistingTaskRunner implements TaskRunner { + private tasks: TaskInvocationDefinition[] = []; + + getTasks() { + return this.tasks; + } + + run(task: TaskInvocationDefinition): Promise { + this.tasks.push(task); + return Promise.resolve(undefined); + } +} + +const logger = getVoidLogger(); + +describe('GerritEntityProvider', () => { + beforeAll(() => server.listen()); + afterEach(() => { + jest.resetAllMocks(); + server.resetHandlers(); + }); + afterAll(() => server.close()); + + const config = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-training': { + host: 'g.com', + query: 'state=ACTIVE&prefix=training', + branch: 'main', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'g.com', + baseUrl: 'https://g.com/gerrit', + gitilesBaseUrl: 'https:/g.com/gitiles', + }, + ], + }, + }); + const schedule = new PersistingTaskRunner(); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + }; + + it('discovers projects from the api.', async () => { + const repoBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/listProjectsBody.txt'), + ); + const expected = getJsonFixture('expectedProviderEntities.json'); + + server.use( + rest.get('https://g.com/gerrit/projects/', (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.body(repoBuffer), + ), + ), + ); + + const provider = GerritEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'gerrit-provider:active-training', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual('gerrit-provider:active-training:refresh'); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toBeCalledWith(expected); + }); + + it('handles api errors.', async () => { + const provider = GerritEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + server.use( + rest.get('https://g.com/gerrit/projects/', (_, res, ctx) => + res(ctx.status(500, 'Error!.')), + ), + ); + + await provider.connect(entityProviderConnection); + const taskDef = schedule.getTasks()[0]; + + await (taskDef.fn as () => Promise)(); + expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled(); + }); + + it('can create multiple providers from config.', async () => { + const configTwoProviders = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-g1': { + host: 'gerrit1.com', + query: 'state=ACTIVE', + }, + 'active-g2': { + host: 'gerrit2.com', + query: 'state=ACTIVE', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'gerrit1.com', + }, + { + host: 'gerrit2.com', + }, + ], + }, + }); + + const providers = GerritEntityProvider.fromConfig(configTwoProviders, { + logger, + schedule, + }); + expect(providers).toHaveLength(2); + expect(providers[0]).toBeInstanceOf(GerritEntityProvider); + expect(providers[0].getProviderName()).toEqual('gerrit-provider:active-g1'); + expect(providers[1]).toBeInstanceOf(GerritEntityProvider); + expect(providers[1].getProviderName()).toEqual('gerrit-provider:active-g2'); + }); + + it('throws if integration is missing.', () => { + const configMissingIntegration = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-g2': { + host: 'gerrit2.com', + query: 'state=ACTIVE', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'gerrit1.com', + }, + ], + }, + }); + + expect(() => + GerritEntityProvider.fromConfig(configMissingIntegration, { + logger, + schedule, + }), + ).toThrow(/No gerrit integration/); + }); +}); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts new file mode 100644 index 0000000000..beb4052073 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts @@ -0,0 +1,165 @@ +/* + * 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 { TaskRunner } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { + EntityProvider, + EntityProviderConnection, + LocationSpec, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-backend'; +import fetch, { Response } from 'node-fetch'; +import { + GerritIntegration, + getGerritProjectsApiUrl, + getGerritRequestOptions, + parseGerritJsonResponse, + ScmIntegrations, +} from '@backstage/integration'; +import * as uuid from 'uuid'; +import { Logger } from 'winston'; + +import { readGerritConfigs } from './config'; +import { GerritProjectQueryResult, GerritProviderConfig } from './types'; + +/** @public */ +export class GerritEntityProvider implements EntityProvider { + private readonly config: GerritProviderConfig; + private readonly integration: GerritIntegration; + private readonly logger: Logger; + private readonly scheduleFn: () => Promise; + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): GerritEntityProvider[] { + const providerConfigs = readGerritConfigs(configRoot); + const integrations = ScmIntegrations.fromConfig(configRoot).gerrit; + const providers: GerritEntityProvider[] = []; + + providerConfigs.forEach(providerConfig => { + const integration = integrations.byHost(providerConfig.host); + if (!integration) { + throw new InputError( + `No gerrit integration found that matches host ${providerConfig.host}`, + ); + } + providers.push( + new GerritEntityProvider( + providerConfig, + integration, + options.logger, + options.schedule, + ), + ); + }); + return providers; + } + + private constructor( + config: GerritProviderConfig, + integration: GerritIntegration, + logger: Logger, + schedule: TaskRunner, + ) { + this.config = config; + this.integration = integration; + this.logger = logger.child({ + target: this.getProviderName(), + }); + this.scheduleFn = this.createScheduleFn(schedule); + } + + getProviderName(): string { + return `gerrit-provider:${this.config.id}`; + } + + async connect(connection: EntityProviderConnection): Promise { + this.connection = connection; + await this.scheduleFn(); + } + + private createScheduleFn(schedule: TaskRunner): () => Promise { + return async () => { + const taskId = `${this.getProviderName()}:refresh`; + return schedule.run({ + id: taskId, + fn: async () => { + const logger = this.logger.child({ + class: GerritEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } + + async refresh(logger: Logger): Promise { + if (!this.connection) { + throw new Error('Gerrit discovery connection not initialized'); + } + + let response: Response; + + const baseProjectApiUrl = getGerritProjectsApiUrl(this.integration.config); + const projectQueryUrl = `${baseProjectApiUrl}?${this.config.query}`; + try { + response = await fetch(projectQueryUrl, { + method: 'GET', + ...getGerritRequestOptions(this.integration.config), + }); + } catch (e) { + throw new Error( + `Failed to list Gerrit projects for query ${this.config.query}, ${e}`, + ); + } + const gerritProjectsResponse = (await parseGerritJsonResponse( + response as any, + )) as GerritProjectQueryResult; + const projects = Object.keys(gerritProjectsResponse); + + const locations = projects.map(project => this.createLocationSpec(project)); + await this.connection.applyMutation({ + type: 'full', + entities: locations.map(location => ({ + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + })), + }); + logger.info(`Found ${locations.length} locations.`); + } + + private createLocationSpec(project: string): LocationSpec { + return { + type: 'url', + target: `${this.integration.config.gitilesBaseUrl}/${project}/+/refs/heads/${this.config.branch}/catalog-info.yaml`, + presence: 'optional', + }; + } +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json new file mode 100644 index 0000000000..f7f707fd1a --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json @@ -0,0 +1,43 @@ +{ + "entities": [ + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Location", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "url:https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml", + "backstage.io/managed-by-origin-location": "url:https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml" + }, + "name": "generated-acbbddb9193be55b61de6a7c1d39b3dcaaf27525" + }, + "spec": { + "presence": "optional", + "target": "https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml", + "type": "url" + } + }, + "locationKey": "gerrit-provider:active-training" + }, + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Location", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "url:https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml", + "backstage.io/managed-by-origin-location": "url:https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml" + }, + "name": "generated-4b8debb1c2ffa8d58c0bdf3d75abcf2252494e65" + }, + "spec": { + "presence": "optional", + "target": "https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml", + "type": "url" + } + }, + "locationKey": "gerrit-provider:active-training" + } + ], + "type": "full" +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt new file mode 100644 index 0000000000..4f7054027c --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt @@ -0,0 +1,2 @@ +)]}' +{"training/gerrit":{"id":"training%2Fgerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"","target":"_blank"}]},"training/sample":{"id":"training%2Fsample","state":"ACTIVE","web_links":[{"name":"browse","url":"","target":"_blank"}]}} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts new file mode 100644 index 0000000000..0422b4e4ea --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { readGerritConfigs } from './config'; + +describe('readGerritConfigs', () => { + it('reads all provider configs', () => { + const provider1 = { + host: 'gerrit1.com', + query: 'state=ACTIVE', + branch: 'main', + }; + const provider2 = { + host: 'gerrit2.com', + query: 'state=ACTIVE', + branch: 'main', + }; + const config = { + catalog: { + providers: { + gerrit: { + 'active-g1': provider1, + 'active-g2': provider2, + }, + }, + }, + }; + + const actual = readGerritConfigs(new ConfigReader(config)); + + expect(actual).toHaveLength(2); + expect(actual[0]).toEqual({ ...provider1, id: 'active-g1' }); + expect(actual[1]).toEqual({ ...provider2, id: 'active-g2' }); + }); + it('provides default values', () => { + const provider = { + host: 'gerrit1.com', + query: 'state=ACTIVE', + }; + const config = { + catalog: { + providers: { + gerrit: { + 'active-g1': provider, + }, + }, + }, + }; + const actual = readGerritConfigs(new ConfigReader(config)); + expect(actual).toHaveLength(1); + expect(actual[0]).toEqual({ + branch: 'master', + id: 'active-g1', + ...provider, + }); + }); +}); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.ts new file mode 100644 index 0000000000..7a28ddd8ea --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.ts @@ -0,0 +1,47 @@ +/* + * 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 { GerritProviderConfig } from './types'; + +function readGerritConfig(id: string, config: Config): GerritProviderConfig { + const branch = config.getOptionalString('branch') ?? 'master'; + const host = config.getString('host'); + const query = config.getString('query'); + + return { + branch, + host, + id, + query, + }; +} + +export function readGerritConfigs(config: Config): GerritProviderConfig[] { + const configs: GerritProviderConfig[] = []; + + const providerConfigs = config.getOptionalConfig('catalog.providers.gerrit'); + + if (!providerConfigs) { + return configs; + } + + for (const id of providerConfigs.keys()) { + configs.push(readGerritConfig(id, providerConfigs.getConfig(id))); + } + + return configs; +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/types.ts b/plugins/catalog-backend-module-gerrit/src/providers/types.ts new file mode 100644 index 0000000000..1e96bd1ee4 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/types.ts @@ -0,0 +1,31 @@ +/* + * 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 type GerritProjectInfo = { + id: string; + name: string; + parent?: string; + state?: string; +}; + +export type GerritProjectQueryResult = Record; + +export type GerritProviderConfig = { + host: string; + query: string; + id: string; + branch?: string; +}; diff --git a/plugins/catalog-backend-module-gerrit/src/setupTests.ts b/plugins/catalog-backend-module-gerrit/src/setupTests.ts new file mode 100644 index 0000000000..8b9b6bd586 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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 {};