diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.test.ts new file mode 100644 index 0000000000..c7c8ecf77a --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { entityFactRetriever } from './entityFactRetriever'; +import { Entity } 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' }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + owner: 'group:team-a', + }, + relations: [ + { + type: 'ownedBy', + target: { name: 'team-a', kind: 'group', namespace: 'default' }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1beta1', + metadata: { + name: 'service-without-owner', + }, + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'test', + }, + }, + ], +}; + +const handlerContext = { + discovery, + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), +}; + +describe('entityFactRetriever', () => { + beforeEach(() => { + getEntitiesMock.mockResolvedValue(defaultEntityListResponse); + }); + afterEach(() => { + getEntitiesMock.mockClear(); + }); + + describe('hasSpecWithOwner', () => { + describe('where the entity has an owner in the spec', () => { + it('returns true for hasSpecWithOwner', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-with-owner'), + ).toMatchObject({ + facts: { + hasSpecWithOwner: true, + }, + }); + }); + }); + describe('where the entity does not have an owner in the spec', () => { + it('returns false for hasSpecWithOwner', async () => { + const facts = await entityFactRetriever.handler(handlerContext); + expect( + facts.find(it => it.entity.name === 'service-without-owner'), + ).toMatchObject({ + facts: { + hasSpecWithOwner: false, + }, + }); + }); + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.ts new file mode 100644 index 0000000000..8bc670b873 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityFactRetriever.ts @@ -0,0 +1,92 @@ +/* + * 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 { FactRetrieverContext } from '@backstage/plugin-tech-insights-node'; +import { CatalogClient } from '@backstage/catalog-client'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; + +const applicableEntityKinds = [ + 'component', + 'api', + 'template', + 'domain', + 'resource', + 'system', +]; + +const entityKindFilter = { field: 'kind', values: applicableEntityKinds }; + +export const entityFactRetriever = { + id: 'entityRetriever', + version: '0.0.1', + entityFilter: [ + { + field: 'kind', + values: applicableEntityKinds, + }, + ], + schema: { + hasSpecWithOwner: { + type: 'boolean', + description: 'The spec.owner field is set', + }, + hasSpecWithGroupOwner: { + type: 'boolean', + description: 'The spec.owner field is set and refers to a group', + }, + hasOwnedByRelation: { + type: 'boolean', + description: 'The entity has an owned_by relation', + }, + hasOwnedByRelationWithGroupTarget: { + type: 'boolean', + description: 'The entity has an owned_by relation which targets a group', + }, + }, + handler: async ({ discovery }: FactRetrieverContext) => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities({ + filter: entityKindFilter, + }); + + return entities.items.map(it => { + return { + entity: { + namespace: it.metadata.namespace!!, + kind: it.kind, + name: it.metadata.name, + }, + facts: { + hasSpecWithOwner: Boolean(it.spec?.owner), + hasSpecWithGroupOwner: Boolean(it.spec?.owner && it.spec?.owner), + hasOwnedByRelation: Boolean( + it.relations && + it.relations.find(({ type }) => type === RELATION_OWNED_BY), + ), + hasOwnedByRelationWithGroupTarget: Boolean( + it.relations && + it.relations.find( + ({ type, target }) => + type === RELATION_OWNED_BY && target.kind === 'group', + ), + ), + }, + }; + }); + }, +};