feat(tech-insights): add plugin for new backend system

A new backend plugin for the tech-insights backend
was added and exported as `default`
as well as extension points to be used by modules.

The plugin can be used like

```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-tech-insights-backend'));
```

Fact retrievers will be added
(built-in or through the extension point),
but only registered if the user adds a configuration for it
(cadence, etc.).

Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2024-01-08 23:34:43 +01:00
parent bc72782dd6
commit 7201af3445
16 changed files with 541 additions and 1 deletions
+15
View File
@@ -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'));
```
+25
View File
@@ -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.
@@ -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)
```
+37
View File
@@ -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;
};
};
};
}
+4 -1
View File
@@ -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"
}
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { techInsightsPlugin as default } from './plugin';
export * from './service';
@@ -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,
});
});
});
});
@@ -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;
}
@@ -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';
@@ -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);
});
});
@@ -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<TechInsightCheck, CheckResult>
| undefined = undefined;
env.registerExtensionPoint(techInsightsFactCheckerFactoryExtensionPoint, {
setFactCheckerFactory<
CheckType extends TechInsightCheck,
CheckResultType extends CheckResult,
>(factory: FactCheckerFactory<CheckType, CheckResultType>): void {
factCheckerFactory = factory;
},
});
// initialized with built-in fact retrievers
// only added as registration if there is config for them
const addedFactRetrievers: Record<string, FactRetriever> = {
entityMetadataFactRetriever,
entityOwnershipFactRetriever,
techdocsFactRetriever,
};
env.registerExtensionPoint(techInsightsFactRetrieversExtensionPoint, {
addFactRetrievers(factRetrievers: Record<string, FactRetriever>): 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,
}),
);
},
});
},
});
+24
View File
@@ -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<CheckType, CheckResultType>,
): void;
}
// @public
export const techInsightsFactCheckerFactoryExtensionPoint: ExtensionPoint<TechInsightsFactCheckerFactoryExtensionPoint>;
// @public (undocumented)
export interface TechInsightsFactRetrieversExtensionPoint {
// (undocumented)
addFactRetrievers(factRetrievers: Record<string, FactRetriever>): void;
}
// @public
export const techInsightsFactRetrieversExtensionPoint: ExtensionPoint<TechInsightsFactRetrieversExtensionPoint>;
// @public
export interface TechInsightsStore {
getFactsBetweenTimestampsByIds(
+1
View File
@@ -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:^",
@@ -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<string, FactRetriever>): void;
}
/**
* An extension point that allows other plugins or modules to add fact retrievers.
*
* @public
*/
export const techInsightsFactRetrieversExtensionPoint =
createExtensionPoint<TechInsightsFactRetrieversExtensionPoint>({
id: 'tech-insights.fact-retrievers',
});
/**
* @public
*/
export interface TechInsightsFactCheckerFactoryExtensionPoint {
setFactCheckerFactory<
CheckType extends TechInsightCheck,
CheckResultType extends CheckResult,
>(
factory: FactCheckerFactory<CheckType, CheckResultType>,
): void;
}
/**
* An extension point that allows other plugins or modules to set a FactCheckerFactory.
*
* @public
*/
export const techInsightsFactCheckerFactoryExtensionPoint =
createExtensionPoint<TechInsightsFactCheckerFactoryExtensionPoint>({
id: 'tech-insights.fact-checker-factory',
});
+1
View File
@@ -15,5 +15,6 @@
*/
export * from './checks';
export * from './extensionPoints';
export * from './facts';
export * from './persistence';
+4
View File
@@ -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:^"