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]));