diff --git a/.changeset/dirty-plums-fix.md b/.changeset/dirty-plums-fix.md new file mode 100644 index 0000000000..fb0655377c --- /dev/null +++ b/.changeset/dirty-plums-fix.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +Add support for the new backend system. + +A new backend module for the tech-insights backend +was added and exported as `default`. + +The module will register the `JsonRulesEngineFactCheckerFactory` +as `FactCheckerFactory`, loading checks from the config. + +You can use it with the new backend system like + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-tech-insights-backend-module-jsonfc')); +``` diff --git a/.changeset/fuzzy-comics-collect.md b/.changeset/fuzzy-comics-collect.md new file mode 100644 index 0000000000..5ce6b3c967 --- /dev/null +++ b/.changeset/fuzzy-comics-collect.md @@ -0,0 +1,27 @@ +--- +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +Support loading `TechInsightsJsonRuleCheck` instances from config. + +Uses the check `id` as key. + +Example: + +```yaml title="app-config.yaml" +techInsights: + factChecker: + checks: + groupOwnerCheck: + type: json-rules-engine + name: Group Owner Check + description: Verifies that a group has been set as the spec.owner for this entity + factIds: + - entityOwnershipFactRetriever + rule: + conditions: + all: + - fact: hasGroupOwner + operator: equal + value: true +``` diff --git a/.changeset/green-dogs-fold.md b/.changeset/green-dogs-fold.md new file mode 100644 index 0000000000..c23f7c2a29 --- /dev/null +++ b/.changeset/green-dogs-fold.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-tech-insights-node': patch +--- + +Add support for the new backend system. + +A new backend plugin for the tech-insights backend +was added and exported as `default`. + +You can use it with the new backend system like + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-tech-insights-backend')); +``` diff --git a/.changeset/orange-gorillas-clean.md b/.changeset/orange-gorillas-clean.md new file mode 100644 index 0000000000..d346adce6c --- /dev/null +++ b/.changeset/orange-gorillas-clean.md @@ -0,0 +1,26 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-tech-insights-node': patch +--- + +Move `FactRetrieverRegistry` and `PersistenceContext` to `@backstage/plugin-tech-insights-node`. + +Original exports are marked as deprecated and re-export the moved types. + +Please replace uses like + +```ts +import { + FactRetrieverRegistry, + PersistenceContext, +} from '@backstage/plugin-tech-insights-backend'; +``` + +with + +```ts +import { + FactRetrieverRegistry, + PersistenceContext, +} from '@backstage/plugin-tech-insights-node'; +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index 5940d39b15..c405ac34ef 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -13,7 +13,17 @@ To add this FactChecker into your Tech Insights you need to install the module i yarn add --cwd packages/backend @backstage/plugin-tech-insights-backend-module-jsonfc ``` -and modify the `techInsights.ts` file to contain a reference to the FactCheckers implementation. +### Add to the backend + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-tech-insights-backend-module-jsonfc')); +``` + +This setup requires checks to be provided using the config. + +### Add to the backend (old) + +Modify the `techInsights.ts` file to contain a reference to the FactCheckers implementation. ```diff +import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; @@ -34,7 +44,7 @@ and modify the `techInsights.ts` file to contain a reference to the FactCheckers }); ``` -By default this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows +By default, this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows ```diff const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip @@ -46,7 +56,7 @@ By default this implementation comes with an in-memory storage to store checks. ``` -## Adding checks +## Adding checks in code Checks for this FactChecker are constructed as [`json-rules-engine` compatible JSON rules](https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#conditions). A check could look like the following for example: @@ -86,6 +96,28 @@ export const exampleCheck: TechInsightJsonRuleCheck = { }; ``` +## Adding checks in config + +Example: + +```yaml title="app-config.yaml" +techInsights: + factChecker: + checks: + groupOwnerCheck: + type: json-rules-engine + name: Group Owner Check + description: Verifies that a group has been set as the spec.owner for this entity + factIds: + - entityOwnershipFactRetriever + rule: + conditions: + all: + - fact: hasGroupOwner + operator: equal + value: true +``` + ### More than one `factIds` for a check. When more than one is supplied, the requested fact **MUST** be present in at least one of the fact retrievers. diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 5fb29d275d..790aa0b666 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -3,9 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node'; +import { Config } from '@backstage/config'; import { FactChecker } from '@backstage/plugin-tech-insights-node'; import { Logger } from 'winston'; import { Operator } from 'json-rules-engine'; @@ -63,6 +65,11 @@ export class JsonRulesEngineFactCheckerFactory { constructor(options: JsonRulesEngineFactCheckerFactoryOptions); // (undocumented) construct(repository: TechInsightsStore): JsonRulesEngineFactChecker; + // (undocumented) + static fromConfig( + config: Config, + options: Omit, + ): JsonRulesEngineFactCheckerFactory; } // @public @@ -104,5 +111,9 @@ export interface TechInsightJsonRuleCheck extends TechInsightCheck { rule: Rule; } +// @public +const techInsightsModuleJsonRulesEngineFactCheckerFactory: () => BackendFeature; +export default techInsightsModuleJsonRulesEngineFactCheckerFactory; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/tech-insights-backend-module-jsonfc/config.json b/plugins/tech-insights-backend-module-jsonfc/config.json new file mode 100644 index 0000000000..b95b5e52bb --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/config.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://backstage.io/schema/config-v1", + "type": "object", + "properties": { + "techInsights": { + "type": "object", + "properties": { + "factChecker": { + "type": "object", + "properties": { + "checks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/check" + } + } + } + } + } + } + }, + "$defs": { + "check": { + "type": "object", + "required": ["type", "name", "description", "factIds", "rule"], + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "factIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "rule": { + "$ref": "#/$defs/rule" + } + } + }, + "conditionProperties": { + "type": "object", + "required": ["fact", "operator", "value"], + "properties": { + "fact": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "object", + "required": ["fact"], + "properties": { + "fact": { + "type": "string" + } + } + }, + {} + ] + }, + "path": { + "type": "string" + }, + "name": { + "type": "string" + }, + "priority": { + "type": "number" + }, + "params": { + "type": "object", + "additionalProperties": true + } + } + }, + "conditionReference": { + "type": "object", + "required": ["condition"], + "properties": { + "condition": { + "type": "string" + }, + "name": { + "type": "string" + }, + "priority": { + "type": "number" + } + } + }, + "nestedCondition": { + "oneOf": [ + { + "$ref": "#/$defs/conditionProperties" + }, + { + "$ref": "#/$defs/topLevelCondition" + } + ] + }, + "rule": { + "type": "string", + "required": ["conditions"], + "properties": { + "conditions": { + "$ref": "#/$defs/topLevelCondition" + }, + "name": { + "type": "string" + }, + "priority": { + "type": "number" + }, + "successMetadata": { + "type": "object", + "additionalProperties": true + }, + "failureMetadata": { + "type": "object", + "additionalProperties": true + } + } + }, + "topLevelCondition": { + "oneOf": [ + { + "type": "object", + "required": ["all"], + "properties": { + "all": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/nestedCondition" + } + ] + } + } + } + }, + { + "type": "object", + "required": ["any"], + "properties": { + "any": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/nestedCondition" + } + ] + } + } + } + }, + { + "type": "object", + "required": ["not"], + "properties": { + "not": { + "$ref": "#/$defs/nestedCondition" + } + } + }, + { + "$ref": "#/$defs/conditionReference" + } + ] + } + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 0a9b723186..99ea9aca56 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -34,9 +34,12 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-tech-insights-common": "workspace:^", "@backstage/plugin-tech-insights-node": "workspace:^", + "@backstage/types": "workspace:^", "ajv": "^8.10.0", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -44,9 +47,12 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ + "config.json", "dist" - ] + ], + "configSchema": "config.json" } diff --git a/plugins/tech-insights-backend-module-jsonfc/src/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/index.ts index 1239211f73..1c1e8ab77d 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/index.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/index.ts @@ -13,15 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - JsonRulesEngineFactCheckerFactory, - JsonRulesEngineFactChecker, -} from './service/JsonRulesEngineFactChecker'; + export { JSON_RULE_ENGINE_CHECK_TYPE } from './constants'; -export type { - JsonRulesEngineFactCheckerFactoryOptions, - JsonRulesEngineFactCheckerOptions, -} from './service/JsonRulesEngineFactChecker'; +export { techInsightsModuleJsonRulesEngineFactCheckerFactory as default } from './module'; +export * from './service'; export type { JsonRuleCheckResponse, JsonRuleBooleanCheckResult, diff --git a/plugins/tech-insights-backend-module-jsonfc/src/module/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/module/index.ts new file mode 100644 index 0000000000..0fa167e883 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/module/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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. + */ + +export * from './techInsightsModuleJsonRulesEngineFactCheckerFactory'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.test.ts new file mode 100644 index 0000000000..b6df48a8b2 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { techInsightsFactCheckerFactoryExtensionPoint } from '@backstage/plugin-tech-insights-node'; +import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; +import { techInsightsModuleJsonRulesEngineFactCheckerFactory } from './techInsightsModuleJsonRulesEngineFactCheckerFactory'; + +describe('techInsightsModuleJsonRulesEngineFactCheckerFactory', () => { + it('should register the factory', async () => { + const extensionPoint = { + setFactCheckerFactory: jest.fn(), + } satisfies Partial; + + await startTestBackend({ + extensionPoints: [ + [techInsightsFactCheckerFactoryExtensionPoint, extensionPoint], + ], + features: [ + techInsightsModuleJsonRulesEngineFactCheckerFactory(), + mockServices.logger.factory(), + mockServices.rootConfig.factory({ + data: { + techInsights: { + factChecker: { + checks: { + groupOwnerCheck: { + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Group Owner Check', + description: + 'Verifies that a group has been set as the spec.owner for this entity', + factIds: ['entityOwnershipFactRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'hasGroupOwner', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }), + ], + }); + + expect(extensionPoint.setFactCheckerFactory).toHaveBeenCalled(); + }); +}); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.ts b/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.ts new file mode 100644 index 0000000000..8527b91d9d --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/module/techInsightsModuleJsonRulesEngineFactCheckerFactory.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { techInsightsFactCheckerFactoryExtensionPoint } from '@backstage/plugin-tech-insights-node'; +import { JsonRulesEngineFactCheckerFactory } from '../service'; + +/** + * Sets a JsonRulesEngineFactCheckerFactory as FactCheckerFactory + * loading checks from the config. + * + * @public + */ +export const techInsightsModuleJsonRulesEngineFactCheckerFactory = + createBackendModule({ + pluginId: 'tech-insights', + moduleId: 'json-rules-engine-fact-checker-factory', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + techInsights: techInsightsFactCheckerFactoryExtensionPoint, + }, + async init({ config, logger, techInsights }) { + const winstonLogger = loggerToWinstonLogger(logger); + const factory = JsonRulesEngineFactCheckerFactory.fromConfig(config, { + logger: winstonLogger, + }); + techInsights.setFactCheckerFactory(factory); + }, + }); + }, + }); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts index 6c1c559fc4..51609bc916 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; +import { Config } from '@backstage/config'; +import { isError } from '@backstage/errors'; +import { FactResponse } from '@backstage/plugin-tech-insights-common'; import { FactChecker, TechInsightCheckRegistry, @@ -22,20 +24,20 @@ import { TechInsightsStore, CheckValidationResponse, } from '@backstage/plugin-tech-insights-node'; -import { FactResponse } from '@backstage/plugin-tech-insights-common'; +import Ajv, { SchemaObject } from 'ajv'; import { Engine, EngineResult, Operator, TopLevelCondition, } from 'json-rules-engine'; -import { DefaultCheckRegistry } from './CheckRegistry'; -import { Logger } from 'winston'; import { pick } from 'lodash'; -import Ajv, { SchemaObject } from 'ajv'; -import * as validationSchema from './validation-schema.json'; +import { Logger } from 'winston'; import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; -import { isError } from '@backstage/errors'; +import { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; +import { DefaultCheckRegistry } from './CheckRegistry'; +import { readChecksFromConfig } from './config'; +import * as validationSchema from './validation-schema.json'; const noopEvent = { type: 'noop', @@ -340,7 +342,7 @@ export class JsonRulesEngineFactChecker * * Implementation of checkRegistry is optional. * If there is a need to use persistent storage for checks, it is recommended to inject a storage implementation here. - * Otherwise an in-memory option is instantiated and used. + * Otherwise, an in-memory option is instantiated and used. */ export type JsonRulesEngineFactCheckerFactoryOptions = { checks: TechInsightJsonRuleCheck[]; @@ -354,7 +356,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = { * * Factory to construct JsonRulesEngineFactChecker * Can be constructed with optional implementation of CheckInsightCheckRegistry if needed. - * Otherwise defaults to using in-memory CheckRegistry + * Otherwise, defaults to using in-memory CheckRegistry. */ export class JsonRulesEngineFactCheckerFactory { private readonly checks: TechInsightJsonRuleCheck[]; @@ -362,6 +364,18 @@ export class JsonRulesEngineFactCheckerFactory { private readonly checkRegistry?: TechInsightCheckRegistry; private readonly operators?: Operator[]; + static fromConfig( + config: Config, + options: Omit, + ): JsonRulesEngineFactCheckerFactory { + const checks = readChecksFromConfig(config); + + return new JsonRulesEngineFactCheckerFactory({ + ...options, + checks, + }); + } + constructor(options: JsonRulesEngineFactCheckerFactoryOptions) { this.logger = options.logger; this.checks = options.checks; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/config.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/config.test.ts new file mode 100644 index 0000000000..732cc7f731 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/config.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/config'; +import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; +import { readChecksFromConfig } from './config'; + +describe('config', () => { + describe('readChecksFromConfig', () => { + it('no config return empty checks array', () => { + const config = new ConfigReader({}); + const checks = readChecksFromConfig(config); + + expect(checks).toHaveLength(0); + }); + + it('empty checks config return empty checks array', () => { + const config = new ConfigReader({ + techInsights: { + factChecker: {}, + }, + }); + const checks = readChecksFromConfig(config); + + expect(checks).toHaveLength(0); + }); + + it('with checks return parsed checks', () => { + const config = new ConfigReader({ + techInsights: { + factChecker: { + checks: { + fooCheck: { + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Foo Check', + description: 'Verifies foo', + factIds: ['fooFactRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'numFoo', + operator: 'greaterThanInclusive', + value: 1, + }, + { + fact: 'hasFoo', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + barCheck: { + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Bar Check', + description: 'Verifies bar', + factIds: ['barFactRetriever'], + rule: { + conditions: { + any: [ + { + fact: 'barEnabled', + operator: 'equal', + value: false, + }, + { + fact: 'hasBar', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + bazCheck: { + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Baz Check', + description: 'Verifies baz', + factIds: ['bazFactRetriever'], + rule: { + conditions: { + not: { + fact: 'bazConfig', + operator: 'equal', + value: { invalid: true }, + }, + }, + }, + }, + }, + }, + }, + }); + + const checks = readChecksFromConfig(config); + + expect(checks).toHaveLength(3); + expect(checks.map(check => check.id)).toEqual([ + 'fooCheck', + 'barCheck', + 'bazCheck', + ]); + + const fooCheck = checks.find(check => check.id === 'fooCheck')!; + expect(fooCheck.name).toEqual('Foo Check'); + expect(fooCheck.rule.conditions).toEqual({ + all: [ + { + fact: 'numFoo', + operator: 'greaterThanInclusive', + value: 1, + }, + { + fact: 'hasFoo', + operator: 'equal', + value: true, + }, + ], + }); + + const barCheck = checks.find(check => check.id === 'barCheck')!; + expect(barCheck.name).toEqual('Bar Check'); + expect(barCheck.rule.conditions).toEqual({ + any: [ + { + fact: 'barEnabled', + operator: 'equal', + value: false, + }, + { + fact: 'hasBar', + operator: 'equal', + value: true, + }, + ], + }); + + const bazCheck = checks.find(check => check.id === 'bazCheck')!; + expect(bazCheck.name).toEqual('Baz Check'); + expect(bazCheck.rule.conditions).toEqual({ + not: { + fact: 'bazConfig', + operator: 'equal', + value: { invalid: true }, + }, + }); + }); + }); +}); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/config.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/config.ts new file mode 100644 index 0000000000..5c663d0d41 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/config.ts @@ -0,0 +1,162 @@ +/* + * Copyright 2024 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 { Config } from '@backstage/config'; +import { TopLevelCondition } from 'json-rules-engine'; +import { Rule, TechInsightJsonRuleCheck } from '../types'; + +// copy of non-exported `ConditionProperties` from 'json-rules-engine' +interface ConditionProperties { + fact: string; + operator: string; + value: { fact: string } | any; + path?: string; + priority?: number; + params?: Record; + name?: string; +} + +// copy of non-exported `NestedCondition` from 'json-rules-engine' +type NestedCondition = ConditionProperties | TopLevelCondition; + +function readRuleConditionProperties(config: Config): ConditionProperties { + const fact = config.getString('fact'); + const name = config.getOptionalString('name'); + const operator = config.getString('operator'); + const params = config.getOptionalConfig('params')?.get>(); + const path = config.getOptionalString('path'); + const priority = config.getOptionalNumber('priority'); + const value: { fact: string } | any = config.get('value'); + + return { + fact, + name, + operator, + params, + path, + priority, + value, + }; +} + +function readRuleNestedCondition(config: Config): NestedCondition { + if (config.has('fact')) { + return readRuleConditionProperties(config); + } + + return readRuleTopLevelCondition(config); +} + +function readRuleTopLevelCondition(config: Config): TopLevelCondition { + const base = { + name: config.getOptionalString('name'), + priority: config.getOptionalNumber('priority'), + }; + + if (config.has('all')) { + const all = config + .getConfigArray('all') + .map(conditionConfig => readRuleNestedCondition(conditionConfig)); + return { + ...base, + all, + }; + } + + if (config.has('any')) { + const any = config + .getConfigArray('any') + .map(conditionConfig => readRuleNestedCondition(conditionConfig)); + return { + ...base, + any, + }; + } + + if (config.has('not')) { + const not = readRuleNestedCondition(config.getConfig('not')); + return { + ...base, + not, + }; + } + + const condition = config.getString('condition'); + + return { + ...base, + condition, + }; +} + +function readRuleFromRuleConfig(config: Config): Rule { + const conditions = readRuleTopLevelCondition(config.getConfig('conditions')); + const name = config.getOptionalString('name'); + const priority = config.getOptionalNumber('priority'); + + return { + conditions, + name, + priority, + }; +} + +function readCheckFromCheckConfig( + id: string, + config: Config, +): TechInsightJsonRuleCheck { + const type = config.getString('type'); + const name = config.getString('name'); + const description = config.getString('description'); + const factIds = config.getStringArray('factIds'); + const successMetadata = config + .getOptionalConfig('successMetadata') + ?.get>(); + const failureMetadata = config + .getOptionalConfig('failureMetadata') + ?.get>(); + const rule = readRuleFromRuleConfig(config.getConfig('rule')); + + return { + description, + factIds, + failureMetadata, + id, + name, + rule, + successMetadata, + type, + }; +} + +export function readChecksFromConfig( + config: Config, +): TechInsightJsonRuleCheck[] { + const key = 'techInsights.factChecker.checks'; + if (!config.has(key)) { + return []; + } + + const checksConfig = config.getConfig(key); + const checks: TechInsightJsonRuleCheck[] = []; + checksConfig.keys().forEach(checkId => { + const checkConfig = checksConfig.getConfig(checkId); + + checks.push(readCheckFromCheckConfig(checkId, checkConfig)); + }); + + return checks; +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/index.ts new file mode 100644 index 0000000000..5670c138a1 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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. + */ + +export { + JsonRulesEngineFactCheckerFactory, + JsonRulesEngineFactChecker, +} from './JsonRulesEngineFactChecker'; + +export type { + JsonRulesEngineFactCheckerFactoryOptions, + JsonRulesEngineFactCheckerOptions, +} from './JsonRulesEngineFactChecker'; diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 95fa3ccda9..29b374c18c 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -15,6 +15,31 @@ yarn add --cwd packages/backend @backstage/plugin-tech-insights-backend ### Adding the plugin to your `packages/backend` +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-tech-insights-backend')); +``` + +You can use the extension points [@backstage/plugin-tech-insights-node](../tech-insights-node) +to add your `FactRetriever` or set a `FactCheckerFactory`. + +The built-in `FactRetrievers`: + +- `entityMetadataFactRetriever` +- `entityOwnershipFactRetriever` +- `techdocsFactRetriever` + +`FactRetrievers` only get registered if they get configured: + +```yaml title="app-config.yaml" +techInsights: + factRetrievers: + entityOwnershipFactRetriever: + cadence: '*/15 * * * *' + lifecycle: { timeToLive: { weeks: 2 } } +``` + +### Adding the plugin to your `packages/backend` (old) + You'll need to add the plugin to the router in your `backend` package. You can do this by creating a file called `packages/backend/src/plugins/techInsights.ts`. An example content for `techInsights.ts` could be something like this. diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 4b6e5b34dc..631e792141 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; @@ -12,14 +13,14 @@ import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; import { FactLifecycle } from '@backstage/plugin-tech-insights-node'; import { FactRetriever } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; -import { FactSchema } from '@backstage/plugin-tech-insights-common'; +import { FactRetrieverRegistry as FactRetrieverRegistry_2 } from '@backstage/plugin-tech-insights-node'; import { HumanDuration } from '@backstage/types'; import { Logger } from 'winston'; +import { PersistenceContext as PersistenceContext_2 } from '@backstage/plugin-tech-insights-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; import { TokenManager } from '@backstage/backend-common'; // @public @@ -63,30 +64,17 @@ export type FactRetrieverRegistrationOptions = { initialDelay?: Duration | HumanDuration; }; -// @public (undocumented) -export interface FactRetrieverRegistry { - // (undocumented) - get(retrieverReference: string): Promise; - // (undocumented) - getSchemas(): Promise; - // (undocumented) - listRegistrations(): Promise; - // (undocumented) - listRetrievers(): Promise; - // (undocumented) - register(registration: FactRetrieverRegistration): Promise; -} +// @public @deprecated (undocumented) +export type FactRetrieverRegistry = FactRetrieverRegistry_2; // @public export const initializePersistenceContext: ( database: PluginDatabaseManager, options?: PersistenceContextOptions, -) => Promise; +) => Promise; -// @public -export type PersistenceContext = { - techInsightsStore: TechInsightsStore; -}; +// @public @deprecated (undocumented) +export type PersistenceContext = PersistenceContext_2; // @public export type PersistenceContextOptions = { @@ -101,7 +89,7 @@ export interface RouterOptions< config: Config; factChecker?: FactChecker; logger: Logger; - persistenceContext: PersistenceContext; + persistenceContext: PersistenceContext_2; } // @public @@ -113,7 +101,7 @@ export type TechInsightsContext< CheckResultType extends CheckResult, > = { factChecker?: FactChecker; - persistenceContext: PersistenceContext; + persistenceContext: PersistenceContext_2; factRetrieverEngine: FactRetrieverEngine; }; @@ -129,16 +117,20 @@ export interface TechInsightsOptions< // (undocumented) discovery: PluginEndpointDiscovery; factCheckerFactory?: FactCheckerFactory; - factRetrieverRegistry?: FactRetrieverRegistry; + factRetrieverRegistry?: FactRetrieverRegistry_2; factRetrievers?: FactRetrieverRegistration[]; // (undocumented) logger: Logger; - persistenceContext?: PersistenceContext; + persistenceContext?: PersistenceContext_2; // (undocumented) scheduler: PluginTaskScheduler; // (undocumented) tokenManager: TokenManager; } +// @public +const techInsightsPlugin: () => BackendFeature; +export default techInsightsPlugin; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/tech-insights-backend/config.d.ts b/plugins/tech-insights-backend/config.d.ts new file mode 100644 index 0000000000..cb3602dc60 --- /dev/null +++ b/plugins/tech-insights-backend/config.d.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 { HumanDuration } from '@backstage/types'; + +export interface Config { + /** Configuration options for the tech-insights plugin */ + techInsights?: { + /** Configuration options for fact retrievers */ + factRetrievers?: { + /** Configuration for a fact retriever and its registration identified by its name. */ + [name: string]: { + /** A cron expression to indicate when the fact retriever should be triggered. */ + cadence: string; + /** Optional duration of the initial delay. */ + initialDelay?: HumanDuration; + /** Optional lifecycle definition indicating the cleanup logic of facts when this retriever is run. */ + lifecycle?: { timeToLive: HumanDuration } | { maxItems: number }; + /** Optional duration to determine how long the fact retriever should be allowed to run, defaults to 5 minutes. */ + timeout?: HumanDuration; + }; + }; + }; +} diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 2ac6746b3e..74d7f9929f 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", @@ -63,7 +64,9 @@ "wait-for-expect": "^3.0.2" }, "files": [ + "config.d.ts", "dist", "migrations/**/*.{js,d.ts}" - ] + ], + "configSchema": "config.d.ts" } diff --git a/plugins/tech-insights-backend/src/deprecated.ts b/plugins/tech-insights-backend/src/deprecated.ts new file mode 100644 index 0000000000..1074e6f910 --- /dev/null +++ b/plugins/tech-insights-backend/src/deprecated.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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 { + FactRetrieverRegistry as FactRetrieverRegistry_, + PersistenceContext as PersistenceContext_, +} from '@backstage/plugin-tech-insights-node'; + +/** + * @public + * @deprecated Use FactRetrieverRegistry from `@backstage/plugin-tech-insights-node` instead. + */ +export type FactRetrieverRegistry = FactRetrieverRegistry_; + +/** + * @public + * @deprecated Use PersistenceContext from `@backstage/plugin-tech-insights-node` instead. + */ +export type PersistenceContext = PersistenceContext_; diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts index e9e037cce1..a582a04ebf 100644 --- a/plugins/tech-insights-backend/src/index.ts +++ b/plugins/tech-insights-backend/src/index.ts @@ -14,21 +14,6 @@ * limitations under the License. */ -export * from './service/router'; -export type { RouterOptions } from './service/router'; - -export { buildTechInsightsContext } from './service/techInsightsContextBuilder'; -export { initializePersistenceContext } from './service/persistence/persistenceContext'; -export type { - TechInsightsOptions, - TechInsightsContext, -} from './service/techInsightsContextBuilder'; -export type { FactRetrieverEngine } from './service/fact/FactRetrieverEngine'; -export type { - PersistenceContext, - PersistenceContextOptions, -} from './service/persistence/persistenceContext'; -export { createFactRetrieverRegistration } from './service/fact/createFactRetriever'; -export type { FactRetrieverRegistry } from './service/fact/FactRetrieverRegistry'; -export type { FactRetrieverRegistrationOptions } from './service/fact/createFactRetriever'; -export * from './service/fact/factRetrievers'; +export { techInsightsPlugin as default } from './plugin'; +export * from './deprecated'; +export * from './service'; diff --git a/plugins/tech-insights-backend/src/plugin/config.test.ts b/plugins/tech-insights-backend/src/plugin/config.test.ts new file mode 100644 index 0000000000..09b4954583 --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/config.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/config'; +import { FactRetriever, TTL } from '@backstage/plugin-tech-insights-node'; +import { createFactRetrieverRegistrationFromConfig } from './config'; + +describe('config', () => { + const mockFactRetriever = jest.fn() as unknown as FactRetriever; + + describe('createFactRetrieverRegistrationFromConfig', () => { + it('no config return undefined', () => { + const config = new ConfigReader({}); + const registration = createFactRetrieverRegistrationFromConfig( + config, + 'any', + mockFactRetriever, + ); + + expect(registration).toBeUndefined(); + }); + + it('no entry for fact retriever return undefined', () => { + const config = new ConfigReader({ + techInsights: { + factRetrievers: {}, + }, + }); + const registration = createFactRetrieverRegistrationFromConfig( + config, + 'any', + mockFactRetriever, + ); + + expect(registration).toBeUndefined(); + }); + + it('with config for fact retriever return registration', () => { + const config = new ConfigReader({ + techInsights: { + factRetrievers: { + any: { + cadence: '*/15 * * * *', + lifecycle: { timeToLive: { weeks: 2 } }, + }, + }, + }, + }); + + const registration = createFactRetrieverRegistrationFromConfig( + config, + 'any', + mockFactRetriever, + ); + + expect(registration).toBeDefined(); + expect(registration!.cadence).toEqual('*/15 * * * *'); + expect((registration!.lifecycle! as TTL).timeToLive).toEqual({ + weeks: 2, + }); + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/plugin/config.ts b/plugins/tech-insights-backend/src/plugin/config.ts new file mode 100644 index 0000000000..25827fbbc5 --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/config.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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 { Config, readDurationFromConfig } from '@backstage/config'; +import { + FactLifecycle, + FactRetriever, + FactRetrieverRegistration, +} from '@backstage/plugin-tech-insights-node'; +import { + createFactRetrieverRegistration, + FactRetrieverRegistrationOptions, +} from '../service'; + +type FactRetrieverConfig = Omit< + FactRetrieverRegistrationOptions, + 'factRetriever' +>; + +function readLifecycleConfig( + config: Config | undefined, +): FactLifecycle | undefined { + if (!config) { + return undefined; + } + + if (config.has('maxItems')) { + return { + maxItems: config.getNumber('maxItems'), + }; + } + + return { + timeToLive: readDurationFromConfig(config.getConfig('timeToLive')), + }; +} + +function readFactRetrieverConfig( + config: Config, + name: string, +): FactRetrieverConfig | undefined { + const factRetrieverConfig = config.getOptionalConfig( + `techInsights.factRetrievers.${name}`, + ); + if (!factRetrieverConfig) { + return undefined; + } + + const cadence = factRetrieverConfig.getString('cadence'); + const initialDelay = factRetrieverConfig.has('initialDelay') + ? readDurationFromConfig(factRetrieverConfig.getConfig('initialDelay')) + : undefined; + const lifecycle = readLifecycleConfig( + factRetrieverConfig.getOptionalConfig('lifecycle'), + ); + const timeout = factRetrieverConfig.has('timeout') + ? readDurationFromConfig(factRetrieverConfig.getConfig('timeout')) + : undefined; + + return { + cadence, + initialDelay, + lifecycle, + timeout, + }; +} + +export function createFactRetrieverRegistrationFromConfig( + config: Config, + name: string, + factRetriever: FactRetriever, +): FactRetrieverRegistration | undefined { + const factRetrieverConfig = readFactRetrieverConfig(config, name); + + return factRetrieverConfig + ? createFactRetrieverRegistration({ + ...factRetrieverConfig, + factRetriever, + }) + : undefined; +} diff --git a/plugins/tech-insights-backend/src/plugin/index.ts b/plugins/tech-insights-backend/src/plugin/index.ts new file mode 100644 index 0000000000..dd13bde435 --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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. + */ + +export * from './plugin'; diff --git a/plugins/tech-insights-backend/src/plugin/plugin.test.ts b/plugins/tech-insights-backend/src/plugin/plugin.test.ts new file mode 100644 index 0000000000..66d06b827e --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/plugin.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { techInsightsPlugin } from './plugin'; + +describe('techInsightsPlugin', () => { + it('should register tech-insights plugin and its router', async () => { + const httpRouterMock = mockServices.httpRouter.mock(); + + await startTestBackend({ + extensionPoints: [], + features: [ + techInsightsPlugin(), + httpRouterMock.factory, + mockServices.database.factory(), + mockServices.logger.factory(), + mockServices.rootConfig.factory({ + data: { + techInsights: { + factRetrievers: { + entityOwnershipFactRetriever: { + cadence: '*/15 * * * *', + lifecycle: { timeToLive: { weeks: 2 } }, + }, + }, + }, + }, + }), + mockServices.scheduler.factory(), + mockServices.tokenManager.factory(), + ], + }); + + expect(httpRouterMock.use).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/tech-insights-backend/src/plugin/plugin.ts b/plugins/tech-insights-backend/src/plugin/plugin.ts new file mode 100644 index 0000000000..6da1ac1637 --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/plugin.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2024 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { + FactCheckerFactory, + FactRetriever, + FactRetrieverRegistration, + FactRetrieverRegistry, + PersistenceContext, + TechInsightCheck, + techInsightsFactCheckerFactoryExtensionPoint, + techInsightsFactRetrieverRegistryExtensionPoint, + techInsightsFactRetrieversExtensionPoint, + techInsightsPersistenceContextExtensionPoint, +} from '@backstage/plugin-tech-insights-node'; +import { + buildTechInsightsContext, + createRouter, + entityMetadataFactRetriever, + entityOwnershipFactRetriever, + techdocsFactRetriever, +} from '../service'; +import { createFactRetrieverRegistrationFromConfig } from './config'; + +/** + * The tech-insights backend plugin. + * + * @public + */ +export const techInsightsPlugin = createBackendPlugin({ + pluginId: 'tech-insights', + register(env) { + let factCheckerFactory: + | FactCheckerFactory + | undefined = undefined; + env.registerExtensionPoint(techInsightsFactCheckerFactoryExtensionPoint, { + setFactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, + >(factory: FactCheckerFactory): void { + factCheckerFactory = factory; + }, + }); + + let factRetrieverRegistry: FactRetrieverRegistry | undefined = undefined; + env.registerExtensionPoint( + techInsightsFactRetrieverRegistryExtensionPoint, + { + setFactRetrieverRegistry(registry: FactRetrieverRegistry): void { + factRetrieverRegistry = registry; + }, + }, + ); + + // initialized with built-in fact retrievers + // only added as registration if there is config for them + const addedFactRetrievers: Record = { + entityMetadataFactRetriever, + entityOwnershipFactRetriever, + techdocsFactRetriever, + }; + env.registerExtensionPoint(techInsightsFactRetrieversExtensionPoint, { + addFactRetrievers(factRetrievers: Record): void { + Object.entries(factRetrievers).forEach(([key, value]) => { + addedFactRetrievers[key] = value; + }); + }, + }); + + let persistenceContext: PersistenceContext | undefined = undefined; + env.registerExtensionPoint(techInsightsPersistenceContextExtensionPoint, { + setPersistenceContext(context: PersistenceContext): void { + persistenceContext = context; + }, + }); + + env.registerInit({ + deps: { + config: coreServices.rootConfig, + database: coreServices.database, + discovery: coreServices.discovery, + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, + }, + async init({ + config, + database, + discovery, + httpRouter, + logger, + scheduler, + tokenManager, + }) { + const winstonLogger = loggerToWinstonLogger(logger); + const factRetrievers: FactRetrieverRegistration[] = Object.entries( + addedFactRetrievers, + ) + .map(([name, factRetriever]) => + createFactRetrieverRegistrationFromConfig( + config, + name, + factRetriever, + ), + ) + .filter(registration => registration) as FactRetrieverRegistration[]; + + const context = await buildTechInsightsContext({ + config, + database, + discovery, + factCheckerFactory, + factRetrieverRegistry, + factRetrievers, + logger: winstonLogger, + persistenceContext, + scheduler, + tokenManager, + }); + + httpRouter.use( + await createRouter({ + ...context, + config, + logger: winstonLogger, + }), + ); + }, + }); + }, +}); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 677b778b9b..3bbaf9edf5 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -17,11 +17,11 @@ import { FactRetriever, FactRetrieverRegistration, + FactRetrieverRegistry, FactSchemaDefinition, TechInsightFact, TechInsightsStore, } from '@backstage/plugin-tech-insights-node'; -import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { DefaultFactRetrieverEngine, FactRetrieverEngine, diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts index b723fcd7f5..04035ca8c7 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -18,10 +18,10 @@ import { FactRetriever, FactRetrieverContext, FactRetrieverRegistration, + FactRetrieverRegistry, TechInsightFact, TechInsightsStore, } from '@backstage/plugin-tech-insights-node'; -import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { Logger } from 'winston'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Duration } from 'luxon'; diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts index 8796f1511a..42caca6b20 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -17,22 +17,11 @@ import { FactRetriever, FactRetrieverRegistration, + FactRetrieverRegistry, } from '@backstage/plugin-tech-insights-node'; import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { ConflictError, NotFoundError } from '@backstage/errors'; -/** - * @public - * - */ -export interface FactRetrieverRegistry { - register(registration: FactRetrieverRegistration): Promise; - get(retrieverReference: string): Promise; - listRetrievers(): Promise; - listRegistrations(): Promise; - getSchemas(): Promise; -} - /** * A basic in memory fact retriever registry. * 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 587db8c4b9..5983a82e1d 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './entityOwnershipFactRetriever'; + export * from './entityMetadataFactRetriever'; +export * from './entityOwnershipFactRetriever'; export * from './techdocsFactRetriever'; diff --git a/plugins/tech-insights-backend/src/service/fact/index.ts b/plugins/tech-insights-backend/src/service/fact/index.ts new file mode 100644 index 0000000000..b3df648a8e --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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. + */ + +export { createFactRetrieverRegistration } from './createFactRetriever'; +export type { FactRetrieverRegistrationOptions } from './createFactRetriever'; +export type { FactRetrieverEngine } from './FactRetrieverEngine'; +export * from './factRetrievers'; diff --git a/plugins/tech-insights-backend/src/service/index.ts b/plugins/tech-insights-backend/src/service/index.ts new file mode 100644 index 0000000000..5306ad26d6 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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. + */ + +export * from './fact'; +export * from './persistence'; +export * from './router'; +export type { RouterOptions } from './router'; + +export { buildTechInsightsContext } from './techInsightsContextBuilder'; +export type { + TechInsightsOptions, + TechInsightsContext, +} from './techInsightsContextBuilder'; diff --git a/plugins/tech-insights-backend/src/service/persistence/index.ts b/plugins/tech-insights-backend/src/service/persistence/index.ts new file mode 100644 index 0000000000..d87f91e1b0 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 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. + */ + +export { initializePersistenceContext } from './persistenceContext'; +export type { PersistenceContextOptions } from './persistenceContext'; diff --git a/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts index 07badb038c..3ddae3197f 100644 --- a/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts +++ b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts @@ -20,22 +20,13 @@ import { } from '@backstage/backend-common'; import { Logger } from 'winston'; import { TechInsightsDatabase } from './TechInsightsDatabase'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { PersistenceContext } from '@backstage/plugin-tech-insights-node'; const migrationsDir = resolvePackagePath( '@backstage/plugin-tech-insights-backend', 'migrations', ); -/** - * A Container for persistence related components in TechInsights - * - * @public - */ -export type PersistenceContext = { - techInsightsStore: TechInsightsStore; -}; - /** * A Container for persistence context initialization options * diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 6935338a56..bf4313748f 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -24,8 +24,10 @@ import { import { ConfigReader } from '@backstage/config'; import request from 'supertest'; import express from 'express'; -import { PersistenceContext } from './persistence/persistenceContext'; -import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { + PersistenceContext, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-node'; import { DateTime } from 'luxon'; import { Knex } from 'knex'; import { TaskScheduler } from '@backstage/backend-tasks'; diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index 5e58df1b20..53c1351ffa 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -19,13 +19,13 @@ import Router from 'express-promise-router'; import { Config } from '@backstage/config'; import { FactChecker, + PersistenceContext, TechInsightCheck, } from '@backstage/plugin-tech-insights-node'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Logger } from 'winston'; import { DateTime } from 'luxon'; -import { PersistenceContext } from './persistence/persistenceContext'; import { CompoundEntityRef, parseEntityRef, diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 20693d0a7d..4f49e0031b 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -19,10 +19,7 @@ import { FactRetrieverEngine, } from './fact/FactRetrieverEngine'; import { Logger } from 'winston'; -import { - DefaultFactRetrieverRegistry, - FactRetrieverRegistry, -} from './fact/FactRetrieverRegistry'; +import { DefaultFactRetrieverRegistry } from './fact/FactRetrieverRegistry'; import { Config } from '@backstage/config'; import { PluginDatabaseManager, @@ -33,12 +30,11 @@ import { FactChecker, FactCheckerFactory, FactRetrieverRegistration, + FactRetrieverRegistry, + PersistenceContext, TechInsightCheck, } from '@backstage/plugin-tech-insights-node'; -import { - initializePersistenceContext, - PersistenceContext, -} from './persistence/persistenceContext'; +import { initializePersistenceContext } from './persistence'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index dbb8a8b674..13056b5eaa 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -8,6 +8,7 @@ import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; import { Duration } from 'luxon'; import { DurationLike } from 'luxon'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { HumanDuration } from '@backstage/types'; import { JsonValue } from '@backstage/types'; @@ -79,6 +80,20 @@ export type FactRetrieverRegistration = { initialDelay?: Duration | HumanDuration; }; +// @public (undocumented) +export interface FactRetrieverRegistry { + // (undocumented) + get(retrieverReference: string): Promise; + // (undocumented) + getSchemas(): Promise; + // (undocumented) + listRegistrations(): Promise; + // (undocumented) + listRetrievers(): Promise; + // (undocumented) + register(registration: FactRetrieverRegistration): Promise; +} + // @public export type FactSchemaDefinition = Omit; @@ -92,6 +107,11 @@ export type MaxItems = { maxItems: number; }; +// @public +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + // @public export interface TechInsightCheck { description: string; @@ -137,6 +157,47 @@ export type TechInsightFact = { timestamp?: DateTime; }; +// @public (undocumented) +export interface TechInsightsFactCheckerFactoryExtensionPoint { + // (undocumented) + setFactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, + >( + factory: FactCheckerFactory, + ): void; +} + +// @public +export const techInsightsFactCheckerFactoryExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export interface TechInsightsFactRetrieverRegistryExtensionPoint { + // (undocumented) + setFactRetrieverRegistry(registry: FactRetrieverRegistry): void; +} + +// @public +export const techInsightsFactRetrieverRegistryExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export interface TechInsightsFactRetrieversExtensionPoint { + // (undocumented) + addFactRetrievers(factRetrievers: Record): void; +} + +// @public +export const techInsightsFactRetrieversExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export interface TechInsightsPersistenceContextExtensionPoint { + // (undocumented) + setPersistenceContext(context: PersistenceContext): void; +} + +// @public +export const techInsightsPersistenceContextExtensionPoint: ExtensionPoint; + // @public export interface TechInsightsStore { getFactsBetweenTimestampsByIds( diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 04f084b129..ec800b7845 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-tech-insights-common": "workspace:^", "@backstage/types": "workspace:^", diff --git a/plugins/tech-insights-node/src/extensionPoints.ts b/plugins/tech-insights-node/src/extensionPoints.ts new file mode 100644 index 0000000000..e1f581a840 --- /dev/null +++ b/plugins/tech-insights-node/src/extensionPoints.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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 { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { FactCheckerFactory, TechInsightCheck } from './checks'; +import { FactRetriever, FactRetrieverRegistry } from './facts'; +import { PersistenceContext } from './persistence'; + +/** + * @public + */ +export interface TechInsightsFactRetrieversExtensionPoint { + addFactRetrievers(factRetrievers: Record): void; +} + +/** + * An extension point that allows other plugins or modules to add fact retrievers. + * + * @public + */ +export const techInsightsFactRetrieversExtensionPoint = + createExtensionPoint({ + id: 'tech-insights.fact-retrievers', + }); + +/** + * @public + */ +export interface TechInsightsFactCheckerFactoryExtensionPoint { + setFactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, + >( + factory: FactCheckerFactory, + ): void; +} + +/** + * An extension point that allows other plugins or modules to set a FactCheckerFactory. + * + * @public + */ +export const techInsightsFactCheckerFactoryExtensionPoint = + createExtensionPoint({ + id: 'tech-insights.fact-checker-factory', + }); + +/** + * @public + */ +export interface TechInsightsFactRetrieverRegistryExtensionPoint { + setFactRetrieverRegistry(registry: FactRetrieverRegistry): void; +} + +/** + * An extension point that allows other plugins or modules to set a custom FactRetrieverRegistry. + * + * @public + */ +export const techInsightsFactRetrieverRegistryExtensionPoint = + createExtensionPoint({ + id: 'tech-insights.fact-retriever-registry', + }); + +/** + * @public + */ +export interface TechInsightsPersistenceContextExtensionPoint { + setPersistenceContext(context: PersistenceContext): void; +} + +/** + * An extension point that allows other plugins or modules to set a custom PersistenceContext. + * + * @public + */ +export const techInsightsPersistenceContextExtensionPoint = + createExtensionPoint({ + id: 'tech-insights.persistence-context', + }); diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index b35ad756ed..9257ea789e 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -230,3 +230,14 @@ export type FactRetrieverRegistration = { */ initialDelay?: Duration | HumanDuration; }; + +/** + * @public + */ +export interface FactRetrieverRegistry { + register(registration: FactRetrieverRegistration): Promise; + get(retrieverReference: string): Promise; + listRetrievers(): Promise; + listRegistrations(): Promise; + getSchemas(): Promise; +} diff --git a/plugins/tech-insights-node/src/index.ts b/plugins/tech-insights-node/src/index.ts index 70ef27e159..c1c321f925 100644 --- a/plugins/tech-insights-node/src/index.ts +++ b/plugins/tech-insights-node/src/index.ts @@ -15,5 +15,6 @@ */ export * from './checks'; +export * from './extensionPoints'; export * from './facts'; export * from './persistence'; diff --git a/plugins/tech-insights-node/src/persistence.ts b/plugins/tech-insights-node/src/persistence.ts index 73abb6c5af..391d23bae5 100644 --- a/plugins/tech-insights-node/src/persistence.ts +++ b/plugins/tech-insights-node/src/persistence.ts @@ -22,6 +22,15 @@ import { import { DateTime } from 'luxon'; import { FactSchema } from '@backstage/plugin-tech-insights-common'; +/** + * A Container for persistence related components in TechInsights + * + * @public + */ +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + /** * TechInsights Database * diff --git a/yarn.lock b/yarn.lock index 6abff51760..19e38d0aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9235,10 +9235,14 @@ __metadata: resolution: "@backstage/plugin-tech-insights-backend-module-jsonfc@workspace:plugins/tech-insights-backend-module-jsonfc" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-tech-insights-common": "workspace:^" "@backstage/plugin-tech-insights-node": "workspace:^" + "@backstage/types": "workspace:^" ajv: ^8.10.0 json-rules-engine: ^6.1.2 lodash: ^4.17.21 @@ -9252,6 +9256,7 @@ __metadata: resolution: "@backstage/plugin-tech-insights-backend@workspace:plugins/tech-insights-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" @@ -9296,6 +9301,7 @@ __metadata: resolution: "@backstage/plugin-tech-insights-node@workspace:plugins/tech-insights-node" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/plugin-tech-insights-common": "workspace:^"