From ccc8a7ff5d8c4d0d06e5b032ac8c64c1e680a155 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 17 Nov 2021 15:21:18 +0000 Subject: [PATCH] Split fact retrievers Split fact retriever into severeal more specialised retrievers. Ditch the dynamic addition of annotation facts in favour of explicit annotation facts. Having dynamic facts makes the versioning system somewhat meaningless. Signed-off-by: Iain Billett --- packages/backend/src/plugins/techInsights.ts | 35 +++- plugins/tech-insights-backend/api-report.md | 17 +- .../entityMetadataFactRetriever.test.ts | 181 ++++++++++++++++++ .../entityMetadataFactRetriever.ts | 68 +++++++ ...s => entityOwnershipFactRetriever.test.ts} | 97 +--------- ...ver.ts => entityOwnershipFactRetriever.ts} | 54 ++---- .../src/service/fact/factRetrievers/index.ts | 4 +- .../techdocsFactRetriever.test.ts | 147 ++++++++++++++ .../factRetrievers/techdocsFactRetriever.ts | 65 +++++++ .../src/service/fact/factRetrievers/utils.ts | 24 +++ 10 files changed, 542 insertions(+), 150 deletions(-) create mode 100644 plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts rename plugins/tech-insights-backend/src/service/fact/factRetrievers/{catalogFactRetriever.test.ts => entityOwnershipFactRetriever.test.ts} (61%) rename plugins/tech-insights-backend/src/service/fact/factRetrievers/{catalogFactRetriever.ts => entityOwnershipFactRetriever.ts} (58%) create mode 100644 plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts create mode 100644 plugins/tech-insights-backend/src/service/fact/factRetrievers/utils.ts diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index 6194f03410..8ab6342a4f 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -17,7 +17,9 @@ import { createRouter, buildTechInsightsContext, createFactRetrieverRegistration, - createCatalogFactRetriever, + entityOwnershipFactRetriever, + entityMetadataFactRetriever, + techdocsFactRetriever, } from '@backstage/plugin-tech-insights-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -40,8 +42,10 @@ export default async function createPlugin({ factRetrievers: [ createFactRetrieverRegistration( '* * * * *', // Example cron, every minute - createCatalogFactRetriever(), + entityOwnershipFactRetriever, ), + createFactRetrieverRegistration('* * * * *', entityMetadataFactRetriever), + createFactRetrieverRegistration('* * * * *', techdocsFactRetriever), ], factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ checks: [ @@ -50,14 +54,33 @@ export default async function createPlugin({ type: JSON_RULE_ENGINE_CHECK_TYPE, name: 'simpleTestCheck', description: 'Simple Check For Testing', - factIds: ['testRetriever'], + factIds: [ + 'entityMetadataFactRetriever', + 'techdocsFactRetriever', + 'entityOwnershipFactRetriever', + ], rule: { conditions: { all: [ { - fact: 'examplenumberfact', - operator: 'lessThan', - value: 5, + fact: 'hasGroupOwner', + operator: 'equal', + value: true, + }, + { + fact: 'hasTitle', + operator: 'equal', + value: true, + }, + { + fact: 'hasDescription', + operator: 'equal', + value: true, + }, + { + fact: 'hasAnnotationBackstageIoTechdocsRef', + operator: 'equal', + value: true, }, ], }, diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 2ed5919484..ac2d1d1170 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -24,14 +24,6 @@ export const buildTechInsightsContext: < options: TechInsightsOptions, ) => Promise>; -// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createCatalogFactRetriever" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createCatalogFactRetriever: ({ - annotations, -}?: Options) => FactRetriever; - // @public export function createFactRetrieverRegistration( cadence: string, @@ -44,6 +36,12 @@ export function createRouter< CheckResultType extends CheckResult, >(options: RouterOptions): Promise; +// @public +export const entityMetadataFactRetriever: FactRetriever; + +// @public +export const entityOwnershipFactRetriever: FactRetriever; + // @public export type PersistenceContext = { techInsightsStore: TechInsightsStore; @@ -60,6 +58,9 @@ export interface RouterOptions< persistenceContext: PersistenceContext; } +// @public +export const techdocsFactRetriever: FactRetriever; + // @public (undocumented) export type TechInsightsContext< CheckType extends TechInsightCheck, diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts new file mode 100644 index 0000000000..b99f6a1c02 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2021 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 { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + PluginEndpointDiscovery, + getVoidLogger, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { CatalogListResponse } from '@backstage/catalog-client'; +import { entityMetadataFactRetriever } from './entityMetadataFactRetriever'; + +const getEntitiesMock = jest.fn(); +jest.mock('@backstage/catalog-client', () => { + return { + CatalogClient: jest + .fn() + .mockImplementation(() => ({ getEntities: getEntitiesMock })), + }; +}); +const discovery: jest.Mocked = { + getBaseUrl: jest.fn(), + getExternalBaseUrl: jest.fn(), +}; + +const defaultEntityListResponse: CatalogListResponse = { + items: [ + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-owner', + description: 'service with an owner', + tags: ['a-tag'], + annotations: { + 'backstage.io/techdocs-ref': 'dir:.', + }, + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: 'team-a', + }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { name: 'team-a', kind: 'group', namespace: 'default' }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-incomplete-data', + description: '', + tags: [], + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: '', + }, + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-user-owner', + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: 'user:my-user', + }, + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'user-a', + }, + kind: 'User', + spec: { + memberOf: 'group-a', + }, + }, + ], +}; + +const handlerContext = { + discovery, + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), +}; + +const entityFactRetriever = entityMetadataFactRetriever; + +describe('entityMetadataFactRetriever', () => { + beforeEach(() => { + getEntitiesMock.mockResolvedValue(defaultEntityListResponse); + }); + afterEach(() => { + getEntitiesMock.mockClear(); + }); + + describe('hasDescription', () => { + describe('where the entity has a description', () => { + it('returns true for hasDescription', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-owner'), + ).toMatchObject({ + facts: { + hasDescription: true, + }, + }); + }); + }); + describe('where the entity does not have a description', () => { + it('returns false for hasDescription', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect(facts.find(it => it.entity.name === 'user-a')).toMatchObject({ + facts: { + hasDescription: false, + }, + }); + }); + }); + describe('where the entity has an empty value for description', () => { + it('returns false for hasDescription', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-incomplete-data'), + ).toMatchObject({ + facts: { + hasDescription: false, + }, + }); + }); + }); + }); + describe('hasTags', () => { + describe('where the entity has tags', () => { + it('returns true for hasTags', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-owner'), + ).toMatchObject({ + facts: { + hasTags: true, + }, + }); + }); + }); + describe('where the entity has no tags', () => { + it('returns false for hasTags', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-incomplete-data'), + ).toMatchObject({ + facts: { + hasTags: false, + }, + }); + }); + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts new file mode 100644 index 0000000000..d6d4752adb --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2021 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 { + FactRetriever, + FactRetrieverContext, +} from '@backstage/plugin-tech-insights-node'; +import { CatalogClient } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import isEmpty from 'lodash/isEmpty'; + +/** + * Generates facts which indicate the completeness of entity metadata. + * + * @public + */ +export const entityMetadataFactRetriever: FactRetriever = { + id: 'entityMetadataFactRetriever', + version: '0.0.1', + schema: { + hasTitle: { + type: 'boolean', + description: 'The entity has a title in metadata', + }, + hasDescription: { + type: 'boolean', + description: 'The entity has a description in metadata', + }, + hasTags: { + type: 'boolean', + description: 'The entity has tags in metadata', + }, + }, + handler: async ({ discovery }: FactRetrieverContext) => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities(); + + return entities.items.map((entity: Entity) => { + return { + entity: { + namespace: entity.metadata.namespace!!, + kind: entity.kind, + name: entity.metadata.name, + }, + facts: { + hasTitle: Boolean(entity.metadata?.title), + hasDescription: Boolean(entity.metadata?.description), + hasTags: !isEmpty(entity.metadata?.tags), + }, + }; + }); + }, +}; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts similarity index 61% rename from plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.test.ts rename to plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts index 7b762be63f..ea0801f17c 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createCatalogFactRetriever } from './catalogFactRetriever'; +import { entityOwnershipFactRetriever } from './entityOwnershipFactRetriever'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, @@ -106,9 +106,7 @@ const handlerContext = { config: ConfigReader.fromConfigs([]), }; -const entityFactRetriever = createCatalogFactRetriever({ - annotations: ['backstage.io/techdocs-ref'], -}); +const entityFactRetriever = entityOwnershipFactRetriever; describe('entityFactRetriever', () => { beforeEach(() => { @@ -193,95 +191,4 @@ describe('entityFactRetriever', () => { }); }); }); - describe('hasDescription', () => { - describe('where the entity has a description', () => { - it('returns true for hasDescription', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-owner'), - ).toMatchObject({ - facts: { - hasDescription: true, - }, - }); - }); - }); - describe('where the entity does not have a description', () => { - it('returns false for hasDescription', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect(facts.find(it => it.entity.name === 'user-a')).toMatchObject({ - facts: { - hasDescription: false, - }, - }); - }); - }); - describe('where the entity has an empty value for description', () => { - it('returns false for hasDescription', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-incomplete-data'), - ).toMatchObject({ - facts: { - hasDescription: false, - }, - }); - }); - }); - }); - describe('hasTags', () => { - describe('where the entity has tags', () => { - it('returns true for hasTags', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-owner'), - ).toMatchObject({ - facts: { - hasTags: true, - }, - }); - }); - }); - describe('where the entity has no tags', () => { - it('returns false for hasTags', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-incomplete-data'), - ).toMatchObject({ - facts: { - hasTags: false, - }, - }); - }); - }); - }); - - describe('dynamic annotation facts', () => { - describe('where the retriever is configured to check for the techdocs annotation', () => { - describe('where the entity has the techdocs annotation', () => { - it('returns true for hasAnnotationBackstageIoTechdocsRef', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-owner'), - ).toMatchObject({ - facts: { - hasAnnotationBackstageIoTechdocsRef: true, - }, - }); - }); - }); - describe('where the entity is missing the techdocs annotation', () => { - it('returns false for hasAnnotationBackstageIoTechdocsRef', async () => { - const facts = await entityFactRetriever.handler(handlerContext); - expect( - facts.find(it => it.entity.name === 'service-with-incomplete-data'), - ).toMatchObject({ - facts: { - hasAnnotationBackstageIoTechdocsRef: false, - }, - }); - }); - }); - }); - }); }); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts similarity index 58% rename from plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.ts rename to plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts index d4d6e54865..5dd3abfaa0 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/catalogFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts @@ -20,19 +20,21 @@ import { } from '@backstage/plugin-tech-insights-node'; import { CatalogClient } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import isEmpty from 'lodash/isEmpty'; -import camelCase from 'lodash/camelCase'; -import { get } from 'lodash'; -type Options = { - annotations?: string[]; -}; - -export const createCatalogFactRetriever = ( - { annotations = [] }: Options = { annotations: [] }, -): FactRetriever => ({ - id: 'entityRetriever', +/** + * Generates facts which indicate the quality of data in the spec.owner field. + * + * @public + */ +export const entityOwnershipFactRetriever: FactRetriever = { + id: 'entityOwnershipFactRetriever', version: '0.0.1', + entityFilter: [ + { + field: 'kind', + values: ['component', 'domain', 'system', 'api', 'resource', 'template'], + }, + ], schema: { hasOwner: { type: 'boolean', @@ -42,23 +44,6 @@ export const createCatalogFactRetriever = ( type: 'boolean', description: 'The spec.owner field is set and refers to a group', }, - hasDescription: { - type: 'boolean', - description: 'The entity has a description in metadata', - }, - hasTags: { - type: 'boolean', - description: 'The entity has tags in metadata', - }, - ...annotations.reduce((acc: object, annotation: string) => { - return { - ...acc, - [camelCase(`hasAnnotation-${annotation}`)]: { - type: 'boolean', - description: `The entity has the annotation: ${annotation} `, - }, - }; - }, {}), }, handler: async ({ discovery }: FactRetrieverContext) => { const catalogClient = new CatalogClient({ @@ -79,19 +64,8 @@ export const createCatalogFactRetriever = ( entity.spec?.owner && !(entity.spec?.owner as string).startsWith('user:'), ), - hasDescription: Boolean(entity.metadata?.description), - hasTags: !isEmpty(entity.metadata?.tags), - ...annotations.reduce( - (acc: object, annotation: string) => ({ - ...acc, - [camelCase(`hasAnnotation-${annotation}`)]: Boolean( - get(entity, ['metadata', 'annotations', annotation]), - ), - }), - {}, - ), }, }; }); }, -}); +}; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts index 58ee8f8e92..587db8c4b9 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './catalogFactRetriever'; +export * from './entityOwnershipFactRetriever'; +export * from './entityMetadataFactRetriever'; +export * from './techdocsFactRetriever'; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts new file mode 100644 index 0000000000..6ed9b12523 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2021 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 { techdocsFactRetriever } from './techdocsFactRetriever'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + PluginEndpointDiscovery, + getVoidLogger, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { CatalogListResponse } from '@backstage/catalog-client'; + +const getEntitiesMock = jest.fn(); +jest.mock('@backstage/catalog-client', () => { + return { + CatalogClient: jest + .fn() + .mockImplementation(() => ({ getEntities: getEntitiesMock })), + }; +}); +const discovery: jest.Mocked = { + getBaseUrl: jest.fn(), + getExternalBaseUrl: jest.fn(), +}; + +const defaultEntityListResponse: CatalogListResponse = { + items: [ + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-owner', + description: 'service with an owner', + tags: ['a-tag'], + annotations: { + 'backstage.io/techdocs-ref': 'dir:.', + }, + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: 'team-a', + }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { name: 'team-a', kind: 'group', namespace: 'default' }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-incomplete-data', + description: '', + tags: [], + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: '', + }, + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-with-user-owner', + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: 'user:my-user', + }, + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'user-a', + }, + kind: 'User', + spec: { + memberOf: 'group-a', + }, + }, + ], +}; + +const handlerContext = { + discovery, + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), +}; + +const entityFactRetriever = techdocsFactRetriever; + +describe('techdocsFactRetriever', () => { + beforeEach(() => { + getEntitiesMock.mockResolvedValue(defaultEntityListResponse); + }); + afterEach(() => { + getEntitiesMock.mockClear(); + }); + + describe('hasAnnotationBackstageIoTechdocsRef', () => { + describe('where the retriever is configured to check for the techdocs annotation', () => { + describe('where the entity has the techdocs annotation', () => { + it('returns true for hasAnnotationBackstageIoTechdocsRef', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-owner'), + ).toMatchObject({ + facts: { + hasAnnotationBackstageIoTechdocsRef: true, + }, + }); + }); + }); + describe('where the entity is missing the techdocs annotation', () => { + it('returns false for hasAnnotationBackstageIoTechdocsRef', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-incomplete-data'), + ).toMatchObject({ + facts: { + hasAnnotationBackstageIoTechdocsRef: false, + }, + }); + }); + }); + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts new file mode 100644 index 0000000000..e5a872c703 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2021 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 { + FactRetriever, + FactRetrieverContext, +} from '@backstage/plugin-tech-insights-node'; +import { CatalogClient } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { entityHasAnnotation, generateAnnotationFactName } from './utils'; + +const techdocsAnnotation = 'backstage.io/techdocs-ref'; +const techdocsAnnotationFactName = + generateAnnotationFactName(techdocsAnnotation); + +/** + * Generates facts related to the completeness of techdocs configuration for entities. + * + * @public + */ +export const techdocsFactRetriever: FactRetriever = { + id: 'techdocsFactRetriever', + version: '0.0.1', + schema: { + [techdocsAnnotationFactName]: { + type: 'boolean', + description: 'The entity has a title in metadata', + }, + }, + handler: async ({ discovery }: FactRetrieverContext) => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities(); + + return entities.items.map((entity: Entity) => { + return { + entity: { + namespace: entity.metadata.namespace!!, + kind: entity.kind, + name: entity.metadata.name, + }, + facts: { + [techdocsAnnotationFactName]: entityHasAnnotation( + entity, + techdocsAnnotation, + ), + }, + }; + }); + }, +}; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/utils.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/utils.ts new file mode 100644 index 0000000000..7967502a63 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/utils.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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 camelCase from 'lodash/camelCase'; +import { Entity } from '@backstage/catalog-model'; +import { get } from 'lodash'; + +export const generateAnnotationFactName = (annotation: string) => + camelCase(`hasAnnotation-${annotation}`); + +export const entityHasAnnotation = (entity: Entity, annotation: string) => + Boolean(get(entity, ['metadata', 'annotations', annotation]));