diff --git a/.changeset/seven-swans-glow.md b/.changeset/seven-swans-glow.md new file mode 100644 index 0000000000..2d41b94082 --- /dev/null +++ b/.changeset/seven-swans-glow.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-tech-insights-common': patch +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +adding new operation to run checks for multiple entities in one request 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 aef09d5434..8866b5badb 100644 --- a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -35,6 +35,7 @@ import { pick } from 'lodash'; import Ajv, { SchemaObject } from 'ajv'; import * as validationSchema from './validation-schema.json'; import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; +import { isError } from '@backstage/errors'; const noopEvent = { type: 'noop', @@ -117,7 +118,7 @@ export class JsonRulesEngineFactChecker if (hasAllFacts) { engine.addRule({ ...techInsightCheck.rule, event: noopEvent }); } else { - this.logger.warn( + this.logger.debug( `Skipping ${ rule.name } due to missing facts: ${techInsightCheck.factIds @@ -139,7 +140,10 @@ export class JsonRulesEngineFactChecker Object.values(facts), ); } catch (e) { - throw new Error(`Failed to run rules engine, ${e.message}`); + if (isError(e)) { + throw new Error(`Failed to run rules engine, ${e.message}`); + } + throw e; } } diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index 80902c307c..22a80c2d79 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -27,10 +27,12 @@ import { Logger } from 'winston'; import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/persistenceContext'; import { + EntityName, EntityRef, parseEntityName, stringifyEntityRef, } from '@backstage/catalog-model'; +import { errorHandler } from '@backstage/backend-common'; /** * @public @@ -91,14 +93,26 @@ export async function createRouter< router.post('/checks/run/:namespace/:kind/:name', async (req, res) => { const { namespace, kind, name } = req.params; - try { - const { checks }: { checks: string[] } = req.body; - const entityTriplet = stringifyEntityRef({ namespace, kind, name }); - const checkResult = await factChecker.runChecks(entityTriplet, checks); - return res.send(checkResult); - } catch (e) { - return res.status(500).json({ message: e.message }).send(); - } + const { checks }: { checks: string[] } = req.body; + const entityTriplet = stringifyEntityRef({ namespace, kind, name }); + const checkResult = await factChecker.runChecks(entityTriplet, checks); + return res.send(checkResult); + }); + + router.post('/checks/run', async (req, res) => { + const { checks, entities }: { checks: string[]; entities: EntityName[] } = + req.body; + const tasks = entities.map(async entity => { + const entityTriplet = + typeof entity === 'string' ? entity : stringifyEntityRef(entity); + const results = await factChecker.runChecks(entityTriplet, checks); + return { + entity: entityTriplet, + results, + }; + }); + const results = await Promise.all(tasks); + return res.send(results); }); } else { logger.info( @@ -153,5 +167,7 @@ export async function createRouter< ), ); }); + + router.use(errorHandler()); return router; } diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index 8618f6266b..0236c0619a 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -12,6 +12,12 @@ export interface BooleanCheckResult extends CheckResult { result: boolean; } +// @public +export type BulkCheckResponse = Array<{ + entity: string; + results: CheckResult[]; +}>; + // @public export interface CheckResponse { description: string; diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts index ecdaa298c1..66af5c2f93 100644 --- a/plugins/tech-insights-common/src/index.ts +++ b/plugins/tech-insights-common/src/index.ts @@ -124,3 +124,13 @@ export type CheckResult = { export interface BooleanCheckResult extends CheckResult { result: boolean; } + +/** + * Response type for bulk check opretation. Contains a list of entities and their respective check results. + * + * @public + */ +export type BulkCheckResponse = Array<{ + entity: string; + results: CheckResult[]; +}>; diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 52712b14a4..79854e505f 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { EntityName } from '@backstage/catalog-model'; import { default as React_2 } from 'react'; @@ -50,6 +51,11 @@ export interface TechInsightsApi { description?: string, ) => CheckResultRenderer | undefined; // (undocumented) + runBulkChecks( + entities: EntityName[], + checks?: Check[], + ): Promise; + // (undocumented) runChecks(entityParams: EntityName, checks?: Check[]): Promise; } diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index 9f2d4f14c4..b1287a62dc 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -15,7 +15,10 @@ */ import { createApiRef } from '@backstage/core-plugin-api'; -import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { + CheckResult, + BulkCheckResponse, +} from '@backstage/plugin-tech-insights-common'; import { Check } from './types'; import { CheckResultRenderer } from '../components/CheckResultRenderer'; import { EntityName } from '@backstage/catalog-model'; @@ -43,4 +46,8 @@ export interface TechInsightsApi { ) => CheckResultRenderer | undefined; getAllChecks(): Promise; runChecks(entityParams: EntityName, checks?: Check[]): Promise; + runBulkChecks( + entities: EntityName[], + checks?: Check[], + ): Promise; } diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 8678ec13c8..692169e80e 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -15,7 +15,10 @@ */ import { TechInsightsApi } from './TechInsightsApi'; -import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { + BulkCheckResponse, + CheckResult, +} from '@backstage/plugin-tech-insights-common'; import { Check } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; @@ -97,4 +100,29 @@ export class TechInsightsClient implements TechInsightsApi { } return await response.json(); } + + async runBulkChecks( + entities: EntityName[], + checks?: Check[], + ): Promise { + const url = await this.discoveryApi.getBaseUrl('tech-insights'); + const token = await this.identityApi.getIdToken(); + const checkIds = checks ? checks.map(check => check.id) : []; + const requestBody = { + entities, + checks: checkIds.length > 0 ? checkIds : undefined, + }; + const response = await fetch(`${url}/checks/run`, { + method: 'POST', + body: JSON.stringify(requestBody), + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + return await response.json(); + } }