From def2307f3367d2143b31ca45dd8bfc15729709b8 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 19 Jan 2021 14:05:52 +0100 Subject: [PATCH] Add a managed-by-origin-location annotation --- .changeset/angry-flowers-yawn.md | 29 +++++++++ .../well-known-annotations.md | 17 +++++ .../catalog-model/src/location/annotation.ts | 2 + packages/catalog-model/src/location/index.ts | 2 +- .../src/ingestion/LocationReaders.ts | 20 +++--- .../AnnotateLocationEntityProcessor.test.ts | 62 +++++++++++++++++++ .../AnnotateLocationEntityProcessor.ts | 14 ++++- .../src/ingestion/processors/types.ts | 4 ++ 8 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 .changeset/angry-flowers-yawn.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts diff --git a/.changeset/angry-flowers-yawn.md b/.changeset/angry-flowers-yawn.md new file mode 100644 index 0000000000..8f478b1eab --- /dev/null +++ b/.changeset/angry-flowers-yawn.md @@ -0,0 +1,29 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Adds a `backstage.io/managed-by-origin-location` annotation to all entities. It links to the +location that was registered to the catalog and which emitted this entity. It has a different +semantic than the existing `backstage.io/managed-by-location` annotation, which tells the direct +parent location that created this entity. + +Consider this example: The Backstage operator adds a location of type `github-org` in the +`app-config.yaml`. This setting will be added to a `bootstrap:boostrap` location. The processor +discovers the entities in the following branch +`Location bootstrap:bootstrap -> Location github-org:… -> User xyz`. The user `xyz` will be: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: xyz + annotations: + # This entity was added by the 'github-org:…' location + backstage.io/managed-by-location: github-org:… + # The entity was added because the 'bootstrap:boostrap' was added to the catalog + backstage.io/managed-by-origin-location: bootstrap:bootstrap + # ... +spec: + # ... +``` diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 3627c74b9a..e3f9e86fae 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -40,6 +40,23 @@ expecting a two-item array out of it. The format of the target part is type-dependent and could conceivably even be an empty string, but the separator colon is always present. +### backstage.io/managed-by-origin-location + +```yaml +# Example: +metadata: + annotations: + backstage.io/managed-by-origin-location: github:http://github.com/backstage/backstage/catalog-info.yaml +``` + +The value of this annotation is a location reference string (see above). It +points to the location, which registration lead to the creation of the entity. +In most cases, the `backstage.io/managed-by-location` and +`backstage.io/managed-by-origin-location` will be equal. It will be different if +the original location delegates to another location. A common case is, that a +location is registered via the `bootstrap:boostrap` which means that is part of +the `app-config.yml` of a backstage installation. + ### backstage.io/techdocs-ref ```yaml diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 371d095685..93f2fabea4 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -15,3 +15,5 @@ */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; +export const ORIGIN_LOCATION_ANNOTATION = + 'backstage.io/managed-by-origin-location'; diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index ce64b988a6..8fd516120a 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -20,4 +20,4 @@ export { locationSpecSchema, analyzeLocationSchema, } from './validation'; -export { LOCATION_ANNOTATION } from './annotation'; +export { LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION } from './annotation'; diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index a9248bb3fb..cb703b8e15 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -78,13 +78,17 @@ export class LocationReaders implements LocationReader { if (rulesEnforcer.isAllowed(item.entity, item.location)) { const relations = Array(); - const entity = await this.handleEntity(item, emitResult => { - if (emitResult.type === 'relation') { - relations.push(emitResult.relation); - return; - } - emit(emitResult); - }); + const entity = await this.handleEntity( + item, + emitResult => { + if (emitResult.type === 'relation') { + relations.push(emitResult.relation); + return; + } + emit(emitResult); + }, + location, + ); if (entity) { output.entities.push({ @@ -165,6 +169,7 @@ export class LocationReaders implements LocationReader { private async handleEntity( item: CatalogProcessorEntityResult, emit: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise { const { processors, logger } = this.options; @@ -185,6 +190,7 @@ export class LocationReaders implements LocationReader { current, item.location, emit, + originLocation, ); } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`; diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts new file mode 100644 index 0000000000..cfd98e9e6a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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 { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; + +describe('AnnotateLocationEntityProcessor', () => { + describe('preProcessEntity', () => { + it('adds annotations', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + + const location: LocationSpec = { + type: 'url', + target: 'my-location', + }; + const originLocation: LocationSpec = { + type: 'url', + target: 'my-origin-location', + }; + + const processor = new AnnotateLocationEntityProcessor(); + + expect( + await processor.preProcessEntity( + entity, + location, + () => {}, + originLocation, + ), + ).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/managed-by-location': 'url:my-location', + 'backstage.io/managed-by-origin-location': 'url:my-origin-location', + }, + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts index ea8afcddc5..b40378226a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts @@ -14,20 +14,28 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { + Entity, + LOCATION_ANNOTATION, + LocationSpec, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; import lodash from 'lodash'; -import { CatalogProcessor } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; export class AnnotateLocationEntityProcessor implements CatalogProcessor { async preProcessEntity( entity: Entity, location: LocationSpec, + _: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise { return lodash.merge( { metadata: { annotations: { - 'backstage.io/managed-by-location': `${location.type}:${location.target}`, + [LOCATION_ANNOTATION]: `${location.type}:${location.target}`, + [ORIGIN_LOCATION_ANNOTATION]: `${originLocation.type}:${originLocation.target}`, }, }, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 5da3510f7a..1bf30567bc 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -46,12 +46,16 @@ export type CatalogProcessor = { * @param entity The (possibly partial) entity to process * @param location The location that the entity came from * @param emit A sink for auxiliary items resulting from the processing + * @param originLocation The location that the entity originally came from. + * While location resolves to the direct parent location, originLocation + * tells which location was used to start the ingestion loop. * @returns The same entity or a modified version of it */ preProcessEntity?( entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit, + originLocation: LocationSpec, ): Promise; /**