From 010aed7848277b1ff096dbcb20ade89125ba2c7b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 11 Mar 2021 18:18:30 +0100 Subject: [PATCH 1/3] Add `AnnotateScmSlugEntityProcessor` Signed-off-by: Oliver Sand --- .changeset/angry-boats-hunt.md | 15 +++ .../AnnotateScmSlugEntityProcessor.test.ts | 106 ++++++++++++++++++ .../AnnotateScmSlugEntityProcessor.ts | 54 +++++++++ .../src/ingestion/processors/index.ts | 1 + 4 files changed, 176 insertions(+) create mode 100644 .changeset/angry-boats-hunt.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts diff --git a/.changeset/angry-boats-hunt.md b/.changeset/angry-boats-hunt.md new file mode 100644 index 0000000000..32933579ca --- /dev/null +++ b/.changeset/angry-boats-hunt.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add `AnnotateScmSlugEntityProcessor` that automatically adds the +`github.com/project-slug` annotation for components coming from GitHub. + +The processor is optional and not automatically registered in the catalog +builder. To add it to your instance, add it to your `CatalogBuilder` using +`addProcessor()`: + +```typescript +const builder = new CatalogBuilder(env); +builder.addProcessor(new AnnotateScmSlugEntityProcessor()); +``` diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts new file mode 100644 index 0000000000..d31122b6f7 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; + +describe('AnnotateScmSlugEntityProcessor', () => { + describe('github', () => { + it('adds annotation', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/backstage', + }, + }, + }); + }); + + it('does not override existing annotation', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/community', + }, + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'github.com/project-slug': 'backstage/community', + }, + }, + }); + }); + + it('should not add annotation for other providers', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + const location: LocationSpec = { + type: 'url', + target: + 'https://gitlab.com/backstage/backstage/-/blob/master/catalog-info.yaml', + }; + + const processor = new AnnotateScmSlugEntityProcessor(); + + expect(await processor.preProcessEntity(entity, location)).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: {}, + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts new file mode 100644 index 0000000000..d74e9521e0 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity, LocationSpec } from '@backstage/catalog-model'; +import parseGitUrl from 'git-url-parse'; +import { identity, merge, pickBy } from 'lodash'; +import { CatalogProcessor } from './types'; + +const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; + +export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { + async preProcessEntity( + entity: Entity, + location: LocationSpec, + ): Promise { + if (entity.kind !== 'Component' || location.type !== 'url') { + return entity; + } + + const gitUrl = parseGitUrl(location.target); + let githubProjectSlug = + entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]; + + if (gitUrl.source === 'github.com' && !githubProjectSlug) { + githubProjectSlug = `${gitUrl.owner}/${gitUrl.name}`; + } + + return merge( + { + metadata: { + annotations: pickBy( + { + [GITHUB_ACTIONS_ANNOTATION]: githubProjectSlug, + }, + identity, + ), + }, + }, + entity, + ); + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 736c64ea39..fe4fe1c1dd 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -17,6 +17,7 @@ import * as results from './results'; export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; +export { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; From 7c15a489aafb7b564ac5bb32927ac30c13ab70d2 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 15 Mar 2021 10:20:15 +0100 Subject: [PATCH 2/3] Use scm integration Signed-off-by: Oliver Sand --- .changeset/angry-boats-hunt.md | 2 +- .../AnnotateScmSlugEntityProcessor.test.ts | 18 ++++++++++--- .../AnnotateScmSlugEntityProcessor.ts | 25 ++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.changeset/angry-boats-hunt.md b/.changeset/angry-boats-hunt.md index 32933579ca..81f5460933 100644 --- a/.changeset/angry-boats-hunt.md +++ b/.changeset/angry-boats-hunt.md @@ -11,5 +11,5 @@ builder. To add it to your instance, add it to your `CatalogBuilder` using ```typescript const builder = new CatalogBuilder(env); -builder.addProcessor(new AnnotateScmSlugEntityProcessor()); +builder.addProcessor(AnnotateScmSlugEntityProcessor.fromConfig(env.config)); ``` diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts index d31122b6f7..f74fd9c779 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts @@ -14,8 +14,19 @@ * limitations under the License. */ import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; +const config = new ConfigReader({ + /* integrations: { + github: [ + { + host: 'github.com', + }, + ], + },*/ +}); + describe('AnnotateScmSlugEntityProcessor', () => { describe('github', () => { it('adds annotation', async () => { @@ -32,7 +43,7 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }; - const processor = new AnnotateScmSlugEntityProcessor(); + const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1', @@ -63,7 +74,7 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }; - const processor = new AnnotateScmSlugEntityProcessor(); + const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1', @@ -91,14 +102,13 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://gitlab.com/backstage/backstage/-/blob/master/catalog-info.yaml', }; - const processor = new AnnotateScmSlugEntityProcessor(); + const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'my-component', - annotations: {}, }, }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts index d74e9521e0..312693cc07 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts @@ -14,6 +14,11 @@ * limitations under the License. */ import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { identity, merge, pickBy } from 'lodash'; import { CatalogProcessor } from './types'; @@ -21,6 +26,16 @@ import { CatalogProcessor } from './types'; const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { + constructor( + private readonly opts: { scmIntegrationRegistry: ScmIntegrationRegistry }, + ) {} + + static fromConfig(config: Config): AnnotateScmSlugEntityProcessor { + return new AnnotateScmSlugEntityProcessor({ + scmIntegrationRegistry: ScmIntegrations.fromConfig(config), + }); + } + async preProcessEntity( entity: Entity, location: LocationSpec, @@ -29,11 +44,19 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { return entity; } + const scmIntegration = this.opts.scmIntegrationRegistry.byUrl( + location.target, + ); + + if (!scmIntegration || scmIntegration.type !== 'github') { + return entity; + } + const gitUrl = parseGitUrl(location.target); let githubProjectSlug = entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]; - if (gitUrl.source === 'github.com' && !githubProjectSlug) { + if (!githubProjectSlug) { githubProjectSlug = `${gitUrl.owner}/${gitUrl.name}`; } From a21ca837a94db307354875be2eba48411c29aa2a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 15 Mar 2021 14:27:35 +0100 Subject: [PATCH 3/3] Remove comment Signed-off-by: Oliver Sand --- .../AnnotateScmSlugEntityProcessor.test.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts index f74fd9c779..24a5687e0d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts @@ -17,16 +17,6 @@ import { Entity, LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -const config = new ConfigReader({ - /* integrations: { - github: [ - { - host: 'github.com', - }, - ], - },*/ -}); - describe('AnnotateScmSlugEntityProcessor', () => { describe('github', () => { it('adds annotation', async () => { @@ -43,7 +33,9 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }; - const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); + const processor = AnnotateScmSlugEntityProcessor.fromConfig( + new ConfigReader({}), + ); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1', @@ -74,7 +66,9 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }; - const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); + const processor = AnnotateScmSlugEntityProcessor.fromConfig( + new ConfigReader({}), + ); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1', @@ -102,7 +96,9 @@ describe('AnnotateScmSlugEntityProcessor', () => { 'https://gitlab.com/backstage/backstage/-/blob/master/catalog-info.yaml', }; - const processor = AnnotateScmSlugEntityProcessor.fromConfig(config); + const processor = AnnotateScmSlugEntityProcessor.fromConfig( + new ConfigReader({}), + ); expect(await processor.preProcessEntity(entity, location)).toEqual({ apiVersion: 'backstage.io/v1alpha1',