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/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..fd12dfed24 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'; @@ -140,5 +141,9 @@ export interface TechInsightsOptions< 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 50d86931ee..5cadc140bc 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/index.ts b/plugins/tech-insights-backend/src/index.ts index d6b845a78d..2e87a53dfd 100644 --- a/plugins/tech-insights-backend/src/index.ts +++ b/plugins/tech-insights-backend/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { techInsightsPlugin as default } from './plugin'; 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..f443075a74 --- /dev/null +++ b/plugins/tech-insights-backend/src/plugin/plugin.ts @@ -0,0 +1,128 @@ +/* + * 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, + TechInsightCheck, + techInsightsFactCheckerFactoryExtensionPoint, + techInsightsFactRetrieversExtensionPoint, +} 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; + }, + }); + + // 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; + }); + }, + }); + + 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, + factRetrievers, + logger: winstonLogger, + scheduler, + tokenManager, + }); + + httpRouter.use( + await createRouter({ + ...context, + config, + logger: winstonLogger, + }), + ); + }, + }); + }, +}); diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index dbb8a8b674..0d0d835b95 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'; @@ -137,6 +138,29 @@ 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 TechInsightsFactRetrieversExtensionPoint { + // (undocumented) + addFactRetrievers(factRetrievers: Record): void; +} + +// @public +export const techInsightsFactRetrieversExtensionPoint: ExtensionPoint; + // @public export interface TechInsightsStore { getFactsBetweenTimestampsByIds( diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 4cd03c38a7..e8f71558d7 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..527aac7d5d --- /dev/null +++ b/plugins/tech-insights-node/src/extensionPoints.ts @@ -0,0 +1,59 @@ +/* + * 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 { FactRetriever } from './facts'; +import { FactCheckerFactory, TechInsightCheck } from './checks'; + +/** + * @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', + }); 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/yarn.lock b/yarn.lock index 1d31e0fc44..171c5107b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9089,9 +9089,11 @@ __metadata: dependencies: "@backstage/backend-common": "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 @@ -9105,6 +9107,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:^" @@ -9149,6 +9152,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:^"