diff --git a/.changeset/dry-tables-sniff.md b/.changeset/dry-tables-sniff.md new file mode 100644 index 0000000000..6dd0914d70 --- /dev/null +++ b/.changeset/dry-tables-sniff.md @@ -0,0 +1,62 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Add a new provider `AzureDevOpsEntityProvider` as replacement for `AzureDevOpsDiscoveryProcessor`. + +In order to migrate from the `AzureDevOpsDiscoveryProcessor` you need to apply +the following changes: + +**Before:** + +```yaml +# app-config.yaml + +catalog: + locations: + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject +``` + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors ... */ +builder.addProcessor(new AzureDevOpsDiscoveryProcessor(env.reader)); +``` + +**After:** + +```yaml +# app-config.yaml + +catalog: + providers: + azureDevOps: + anyProviderId: + organization: myorg + project: myproject +``` + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + ...AzureDevOpsEntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), +); +``` + +Visit [https://backstage.io/docs/integrations/azure/discovery](https://backstage.io/docs/integrations/azure/discovery) for more details and options on configuration. diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 9293b6d9d7..2295d7367f 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -6,25 +6,78 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from repositories in an Azure DevOps organization --- -The Azure DevOps integration has a special discovery processor for discovering -catalog entities within an Azure DevOps. The processor will crawl the Azure +The Azure DevOps integration has a special entity provider for discovering +catalog entities within an Azure DevOps. The provider will crawl your Azure DevOps organization and register entities matching the configured path. This can be useful as an alternative to static locations or manually adding things to the catalog. ## Installation -You will have to add the processors in the catalog initialization code of your -backend. They are not installed by default, therefore you have to add a -dependency to `@backstage/plugin-catalog-backend-module-azure` to your backend -package. +At your configuration, you add one or more provider configs: + +```yaml +# app-config.yaml +catalog: + providers: + azureDevOps: + yourProviderId: # identifies your dataset / provider independent of config changes + organization: myorg + project: myproject + repository: service-* # this will match all repos starting with service-* + path: /catalog-info.yaml + anotherProviderId: # another identifier + organization: myorg + project: myproject + repository: '*' # this will match all repos starting with service-* + path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder + yetAotherProviderId: # guess, what? Another one :) + host: selfhostedazure.yourcompany.com + organization: myorg + project: myproject +``` + +The parameters available are: + +- `host:` Leave empty for Cloud hosted, otherwise set to your self-hosted instance host. +- `organization:` Your organization slug. Required. +- `project:` Your project slug. Required. +- `repository:` The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched. +- `path:` Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml. + +To use the entity provider, you'll need an Azure integration +[set up](locations.md) with `host` and `token`. + +As this provider is not one of the default providers, you will first need to install +the Azure catalog plugin: ```bash # From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-azure ``` -And then add the processors to your catalog builder: +Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: + +```diff +/* packages/backend/src/plugins/catalog.ts */ ++import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ ++builder.addEntityProvider( ++ ...AzureDevOpsEntityProvider.fromConfig(env.config, { ++ logger: env.logger, ++ schedule: env.scheduler.createScheduledTaskRunner({ ++ frequency: Duration.fromObject({ minutes: 30 }), ++ timeout: Duration.fromObject({ minutes: 3 }), ++ }), ++ }), ++); +``` + +## Alternative Processor + +As alternative to the entity provider `AzureDevOpsEntityProvider` you can still use the `AzureDevopsDiscoveryProcessor`. ```diff // In packages/backend/src/plugins/catalog.ts @@ -37,12 +90,6 @@ And then add the processors to your catalog builder: + builder.addProcessor(AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { logger: env.logger })); ``` -## Configuration - -To use the discovery processor, you'll need a Azure integration -[set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target -to the catalog configuration: - ```yaml catalog: locations: diff --git a/plugins/catalog-backend-module-azure/api-report.md b/plugins/catalog-backend-module-azure/api-report.md index 139db6aab5..22f5015c9b 100644 --- a/plugins/catalog-backend-module-azure/api-report.md +++ b/plugins/catalog-backend-module-azure/api-report.md @@ -6,9 +6,12 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { TaskRunner } from '@backstage/backend-tasks'; // @public export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { @@ -32,4 +35,22 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, ): Promise; } + +// @public +export class AzureDevOpsEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): AzureDevOpsEntityProvider[]; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(logger: Logger): Promise; +} ``` diff --git a/plugins/catalog-backend-module-azure/config.d.ts b/plugins/catalog-backend-module-azure/config.d.ts new file mode 100644 index 0000000000..6e423c9415 --- /dev/null +++ b/plugins/catalog-backend-module-azure/config.d.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020 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 AzureDevOpsConfig { + /** + * (Optional) The DevOps host; leave empty for `dev.azure.com`, otherwise set to your self-hosted instance host. + * @visibility backend + */ + host: string; + /** + * (Required) Your organization slug. + * @visibility backend + */ + organization: string; + /** + * (Required) Your project slug. + * @visibility backend + */ + project: string; + /** + * (Optional) The repository name. Wildcards are supported as show on the examples above. + * If not set, all repositories will be searched. + * @visibility backend + */ + repository?: string; + /** + * (Optional) Where to find catalog-info.yaml files. Wildcards are supported. + * If not set, defaults to /catalog-info.yaml. + * @visibility backend + */ + path?: string; +} + +export interface Config { + catalog?: { + /** + * List of provider-specific options and attributes + */ + providers?: { + /** + * AzureDevopsEntityProvider configuration + */ + azureDevOps?: Record; + }; + }; +} diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index ffaa368bff..ae919507a3 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -35,6 +35,7 @@ "dependencies": { "@backstage/backend-common": "^0.13.6-next.1", "@backstage/catalog-model": "^1.0.3-next.0", + "@backstage/backend-tasks": "^0.3.1", "@backstage/config": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/integration": "^1.2.1-next.1", @@ -42,6 +43,7 @@ "@backstage/types": "^1.0.0", "lodash": "^4.17.21", "msw": "^0.42.0", + "uuid": "^8.0.0", "node-fetch": "^2.6.7", "winston": "^3.2.1" }, @@ -51,6 +53,8 @@ "@types/lodash": "^4.14.151" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/catalog-backend-module-azure/src/index.ts b/plugins/catalog-backend-module-azure/src/index.ts index f6c61e52a5..a71c40ee6c 100644 --- a/plugins/catalog-backend-module-azure/src/index.ts +++ b/plugins/catalog-backend-module-azure/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ -export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; +export { AzureDevOpsDiscoveryProcessor } from './processors'; +export { AzureDevOpsEntityProvider } from './providers'; diff --git a/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index e6eed8d976..9000b177fa 100644 --- a/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -21,9 +21,9 @@ import { AzureDevOpsDiscoveryProcessor, parseUrl, } from './AzureDevOpsDiscoveryProcessor'; -import { codeSearch } from './lib'; +import { codeSearch } from '../lib'; -jest.mock('./lib'); +jest.mock('../lib'); const mockCodeSearch = codeSearch as jest.MockedFunction; describe('AzureDevOpsDiscoveryProcessor', () => { diff --git a/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts similarity index 99% rename from plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts rename to plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index bdc7255f44..ad63fef810 100644 --- a/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -26,7 +26,7 @@ import { processingResult, } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; -import { codeSearch } from './lib'; +import { codeSearch } from '../lib'; /** * Extracts repositories out of an Azure DevOps org. diff --git a/plugins/catalog-backend-module-azure/src/processors/index.ts b/plugins/catalog-backend-module-azure/src/processors/index.ts new file mode 100644 index 0000000000..9c76336804 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/processors/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 { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts new file mode 100644 index 0000000000..e25073e2b3 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.test.ts @@ -0,0 +1,193 @@ +/* + * 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 { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; +import { ConfigReader } from '@backstage/config'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { CodeSearchResultItem } from '../lib'; +import { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider'; +import { codeSearch } from '../lib'; + +jest.mock('../lib'); +const mockCodeSearch = codeSearch as jest.MockedFunction; + +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('AzureDevOpsEntityProvider', () => { + afterEach(() => { + mockCodeSearch.mockClear(); + }); + + const expectMutation = async ( + providerId: string, + providerConfig: object, + codeSearchResults: CodeSearchResultItem[], + expectedBaseUrl: string, + names: Record, + integrationConfig?: object, + ) => { + const config = new ConfigReader({ + integrations: { + azure: integrationConfig ? [integrationConfig] : [], + }, + catalog: { + providers: { + azureDevOps: { + [providerId]: providerConfig, + }, + }, + }, + }); + + mockCodeSearch.mockResolvedValueOnce(codeSearchResults); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + }; + + const provider = AzureDevOpsEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + `azureDevOps-provider:${providerId}`, + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual(`azureDevOps-provider:${providerId}:refresh`); + await (taskDef.fn as () => Promise)(); + + const expectedEntities = codeSearchResults.map(item => { + const url = encodeURI( + `${expectedBaseUrl}/_git/${item.repository.name}?path=${item.path}`, + ); + return { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: names[`${item.repository.name}?path=${item.path}`], + }, + spec: { + presence: 'required', + target: `${url}`, + type: 'url', + }, + }, + locationKey: `azureDevOps-provider:${providerId}`, + }; + }); + + expect(entityProviderConnection.applyMutation).toBeCalledWith({ + type: 'full', + entities: expectedEntities, + }); + }; + + // eslint-disable-next-line jest/expect-expect + it('no mutation when repos are empty', async () => { + return expectMutation( + 'allRepos', + { + organization: 'myorganization', + project: 'myproject', + }, + [], + 'https://dev.azure.com/myorganization/myproject', + {}, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('single mutation when repos have 1 file found', async () => { + return expectMutation( + 'allReposSingleFile', + { + organization: 'myorganization', + project: 'myproject', + }, + [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'myrepo', + }, + }, + ], + 'https://dev.azure.com/myorganization/myproject', + { + 'myrepo?path=/catalog-info.yaml': + 'generated-87865246726bb12a8c4fb4f914443f1fbb91648c', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('single mutation when multiple repos have multiple files', async () => { + return expectMutation( + 'allReposMultipleFiles', + { + organization: 'myorganization', + project: 'myproject', + }, + [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'myrepo', + }, + }, + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'myotherrepo', + }, + }, + ], + 'https://dev.azure.com/myorganization/myproject', + { + 'myrepo?path=/catalog-info.yaml': + 'generated-87865246726bb12a8c4fb4f914443f1fbb91648c', + 'myotherrepo?path=/catalog-info.yaml': + 'generated-2deccac384c34d0dca37be0ebb4b1c8cf6913fe1', + }, + ); + }); +}); diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts new file mode 100644 index 0000000000..e2165c887d --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -0,0 +1,167 @@ +/* + * 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 { AzureIntegration, ScmIntegrations } from '@backstage/integration'; +import { + EntityProvider, + EntityProviderConnection, + LocationSpec, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-backend'; +import { readAzureDevOpsConfigs } from './config'; +import { Logger } from 'winston'; +import { AzureDevOpsConfig } from './types'; +import * as uuid from 'uuid'; +import { codeSearch, CodeSearchResultItem } from '../lib'; + +/** + * Provider which discovers catalog files within an Azure DevOps repositories. + * + * Use `AzureDevOpsEntityProvider.fromConfig(...)` to create instances. + * + * @public + */ +export class AzureDevOpsEntityProvider implements EntityProvider { + private readonly logger: Logger; + private readonly scheduleFn: () => Promise; + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): AzureDevOpsEntityProvider[] { + const providerConfigs = readAzureDevOpsConfigs(configRoot); + + return providerConfigs.map(providerConfig => { + const integration = ScmIntegrations.fromConfig(configRoot).azure.byHost( + providerConfig.host, + ); + + if (!integration) { + throw new Error( + `There is no Azure integration for host ${providerConfig.host}. Please add a configuration entry for it under integrations.azure`, + ); + } + + return new AzureDevOpsEntityProvider( + providerConfig, + integration, + options.logger, + options.schedule, + ); + }); + } + + private constructor( + private readonly config: AzureDevOpsConfig, + private readonly integration: AzureIntegration, + logger: Logger, + schedule: TaskRunner, + ) { + this.logger = logger.child({ + target: this.getProviderName(), + }); + + this.scheduleFn = this.createScheduleFn(schedule); + } + + 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: AzureDevOpsEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */ + getProviderName(): string { + return `azureDevOps-provider:${this.config.id}`; + } + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ + async connect(connection: EntityProviderConnection): Promise { + this.connection = connection; + await this.scheduleFn(); + } + + async refresh(logger: Logger) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + logger.info('Discovering Azure DevOps catalog files'); + + const files = await codeSearch( + this.integration.config, + this.config.organization, + this.config.project, + this.config.repository, + this.config.path, + ); + + logger.info(`Discovered ${files.length} catalog files`); + + const locations = files.map(key => this.createLocationSpec(key)); + + await this.connection.applyMutation({ + type: 'full', + entities: locations.map(location => { + return { + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + }; + }), + }); + + logger.info( + `Committed ${locations.length} locations for AzureDevOps catalog files`, + ); + } + + private createLocationSpec(file: CodeSearchResultItem): LocationSpec { + return { + type: 'url', + target: this.createObjectUrl(file), + presence: 'required', + }; + } + + private createObjectUrl(file: CodeSearchResultItem): string { + const baseUrl = `https://${this.config.host}/${this.config.organization}/${this.config.project}`; + return encodeURI( + `${baseUrl}/_git/${file.repository.name}?path=${file.path}`, + ); + } +} diff --git a/plugins/catalog-backend-module-azure/src/providers/config.test.ts b/plugins/catalog-backend-module-azure/src/providers/config.test.ts new file mode 100644 index 0000000000..236c9a48d4 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/config.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2020 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 { readAzureDevOpsConfigs } from './config'; + +describe('readAzureDevOpsConfigs', () => { + it('reads all provider configs and set default values', () => { + const provider1 = { + host: 'azure.mycompany.com', + organization: 'mycompany', + project: 'myproject', + }; + const provider2 = { + organization: 'mycompany', + project: 'myproject', + }; + const provider3 = { + organization: 'mycompany', + project: 'myproject', + repository: 'service-*', + }; + const config = { + catalog: { + providers: { + azureDevOps: { provider1, provider2, provider3 }, + }, + }, + }; + + const actual = readAzureDevOpsConfigs(new ConfigReader(config)); + + expect(actual).toHaveLength(3); + expect(actual[0]).toEqual({ + ...provider1, + path: '/catalog-info.yaml', + repository: '*', + id: 'provider1', + }); + expect(actual[1]).toEqual({ + ...provider2, + host: 'dev.azure.com', + path: '/catalog-info.yaml', + repository: '*', + id: 'provider2', + }); + expect(actual[2]).toEqual({ + ...provider3, + host: 'dev.azure.com', + path: '/catalog-info.yaml', + id: 'provider3', + }); + }); +}); diff --git a/plugins/catalog-backend-module-azure/src/providers/config.ts b/plugins/catalog-backend-module-azure/src/providers/config.ts new file mode 100644 index 0000000000..1d143fc882 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/config.ts @@ -0,0 +1,53 @@ +/* + * 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 { AzureDevOpsConfig } from './types'; + +export function readAzureDevOpsConfigs(config: Config): AzureDevOpsConfig[] { + const configs: AzureDevOpsConfig[] = []; + + const providerConfigs = config.getOptionalConfig( + 'catalog.providers.azureDevOps', + ); + + if (!providerConfigs) { + return configs; + } + + for (const id of providerConfigs.keys()) { + configs.push(readAzureDevOpsConfig(id, providerConfigs.getConfig(id))); + } + + return configs; +} + +function readAzureDevOpsConfig(id: string, config: Config): AzureDevOpsConfig { + const organization = config.getString('organization'); + const project = config.getString('project'); + const host = config.getOptionalString('host') || 'dev.azure.com'; + const repository = config.getOptionalString('repository') || '*'; + const path = config.getOptionalString('path') || '/catalog-info.yaml'; + + return { + id, + host, + organization, + project, + repository, + path, + }; +} diff --git a/plugins/catalog-backend-module-azure/src/providers/index.ts b/plugins/catalog-backend-module-azure/src/providers/index.ts new file mode 100644 index 0000000000..450fc0af27 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/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 { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider'; diff --git a/plugins/catalog-backend-module-azure/src/providers/types.ts b/plugins/catalog-backend-module-azure/src/providers/types.ts new file mode 100644 index 0000000000..15ea00ff13 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/providers/types.ts @@ -0,0 +1,24 @@ +/* + * 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 AzureDevOpsConfig = { + id: string; + host: string; + organization: string; + project: string; + repository: string; + path: string; +};