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 <iain@roadie.io>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -24,14 +24,6 @@ export const buildTechInsightsContext: <
|
||||
options: TechInsightsOptions<CheckType, CheckResultType>,
|
||||
) => Promise<TechInsightsContext<CheckType, CheckResultType>>;
|
||||
|
||||
// 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<CheckType, CheckResultType>): Promise<express.Router>;
|
||||
|
||||
// @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,
|
||||
|
||||
+181
@@ -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<PluginEndpointDiscovery> = {
|
||||
getBaseUrl: jest.fn(),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultEntityListResponse: CatalogListResponse<Entity> = {
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+68
@@ -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),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
+2
-95
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+14
-40
@@ -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]),
|
||||
),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
+147
@@ -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<PluginEndpointDiscovery> = {
|
||||
getBaseUrl: jest.fn(),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultEntityListResponse: CatalogListResponse<Entity> = {
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+65
@@ -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,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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]));
|
||||
Reference in New Issue
Block a user