@red-hat-developer-hub/backstage-plugin-scorecard-backend 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 5d447f1: **BREAKING**: The `supportsEntity` function has been replaced with `getCatalogFilter` for `MetricProvider`. The new function returns a catalog filter instead of taking an entity parameter and returning a boolean. This allows the plugin to query the catalog for entities that support the metric provider.
8
+
9
+ These changes are **required** to your `MyMetricProvider`:
10
+
11
+ ```diff
12
+ export class MyMetricProvider implements MetricProvider {
13
+
14
+ - supportsEntity(entity: Entity): boolean {
15
+ - return entity.metadata.annotations?.['my/annotation'] !== undefined;
16
+ - }
17
+ + getCatalogFilter(): Record<string, string | symbol | (string | symbol)[]> {
18
+ + return {
19
+ + 'metadata.annotations.my/annotation': CATALOG_FILTER_EXISTS,
20
+ + };
21
+ + }
22
+ ```
23
+
24
+ Implemented database support. Implemented scheduler to fetch metrics by provider and to cleanup outdated metrics from database.
25
+
26
+ ### Patch Changes
27
+
28
+ - Updated dependencies [5d447f1]
29
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.0.0
30
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.0.0
31
+
3
32
  ## 1.0.0
4
33
 
5
34
  ### Major Changes
package/README.md CHANGED
@@ -97,6 +97,16 @@ Thresholds are evaluated in order, and the first matching rule determines the ca
97
97
 
98
98
  For comprehensive threshold configuration guide, examples, and best practices, see [thresholds.md](./docs/thresholds.md).
99
99
 
100
+ ## Configuration cleanup Job
101
+
102
+ The plugin has a predefined job that runs every day to check and clean old metrics. By default, metrics are saved for **365 days**, however, this period can be changed in the `app-config.yaml` file. Here is an example of how to do that:
103
+
104
+ ```YAML app-config.yaml
105
+ ---
106
+ scorecard:
107
+ dataRetentionDays: 12
108
+ ```
109
+
100
110
  ## Development
101
111
 
102
112
  This plugin backend can be started in a standalone mode from directly in this
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ class DatabaseMetricValues {
4
+ constructor(dbClient) {
5
+ this.dbClient = dbClient;
6
+ }
7
+ tableName = "metric_values";
8
+ /**
9
+ * Insert multiple metric values
10
+ */
11
+ async createMetricValues(metricValues) {
12
+ await this.dbClient(this.tableName).insert(metricValues);
13
+ }
14
+ /**
15
+ * Get the latest metric values for a specific entity and metrics
16
+ */
17
+ async readLatestEntityMetricValues(catalog_entity_ref, metric_ids) {
18
+ return await this.dbClient(this.tableName).select("*").whereIn(
19
+ "id",
20
+ this.dbClient(this.tableName).max("id").whereIn("metric_id", metric_ids).where("catalog_entity_ref", catalog_entity_ref).groupBy("metric_id")
21
+ );
22
+ }
23
+ /**
24
+ * Delete metric values that are older than the given date
25
+ */
26
+ async cleanupExpiredMetrics(olderThan) {
27
+ return await this.dbClient(this.tableName).where("timestamp", "<", olderThan).del();
28
+ }
29
+ }
30
+
31
+ exports.DatabaseMetricValues = DatabaseMetricValues;
32
+ //# sourceMappingURL=DatabaseMetricValues.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport { DbMetricValue } from './types';\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(\n metricValues: Omit<DbMetricValue, 'id'>[],\n ): Promise<void> {\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n}\n"],"names":[],"mappings":";;AAmBO,MAAM,oBAAqB,CAAA;AAAA,EAGhC,YAA6B,QAA4B,EAAA;AAA5B,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAA6B,EAFzC,SAAY,GAAA,eAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,MAAM,mBACJ,YACe,EAAA;AACf,IAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CAAE,OAAO,YAAY,CAAA;AAAA;AACzD;AAAA;AAAA;AAAA,EAKA,MAAM,4BACJ,CAAA,kBAAA,EACA,UAC0B,EAAA;AAC1B,IAAO,OAAA,MAAM,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CACtC,CAAA,MAAA,CAAO,GAAG,CACV,CAAA,OAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAS,CAAA,IAAA,CAAK,SAAS,CAAA,CACzB,IAAI,IAAI,CAAA,CACR,OAAQ,CAAA,WAAA,EAAa,UAAU,CAC/B,CAAA,KAAA,CAAM,sBAAsB,kBAAkB,CAAA,CAC9C,QAAQ,WAAW;AAAA,KACxB;AAAA;AACJ;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,SAAkC,EAAA;AAC5D,IAAO,OAAA,MAAM,IAAK,CAAA,QAAA,CAAS,IAAK,CAAA,SAAS,CACtC,CAAA,KAAA,CAAM,WAAa,EAAA,GAAA,EAAK,SAAS,CAAA,CACjC,GAAI,EAAA;AAAA;AAEX;;;;"}
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+
5
+ const migrationsDir = backendPluginApi.resolvePackagePath(
6
+ "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
7
+ "migrations"
8
+ );
9
+ async function migrate(databaseManager) {
10
+ const knex = await databaseManager.getClient();
11
+ if (!databaseManager.migrations?.skip) {
12
+ await knex.migrate.latest({
13
+ directory: migrationsDir
14
+ });
15
+ }
16
+ }
17
+
18
+ exports.migrate = migrate;
19
+ //# sourceMappingURL=migration.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.cjs.js","sources":["../../src/database/migration.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n DatabaseService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\n\nconst migrationsDir = resolvePackagePath(\n '@red-hat-developer-hub/backstage-plugin-scorecard-backend',\n 'migrations',\n);\n\nexport async function migrate(databaseManager: DatabaseService) {\n const knex = await databaseManager.getClient();\n\n if (!databaseManager.migrations?.skip) {\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n }\n}\n"],"names":["resolvePackagePath"],"mappings":";;;;AAoBA,MAAM,aAAgB,GAAAA,mCAAA;AAAA,EACpB,2DAAA;AAAA,EACA;AACF,CAAA;AAEA,eAAsB,QAAQ,eAAkC,EAAA;AAC9D,EAAM,MAAA,IAAA,GAAO,MAAM,eAAA,CAAgB,SAAU,EAAA;AAE7C,EAAI,IAAA,CAAC,eAAgB,CAAA,UAAA,EAAY,IAAM,EAAA;AACrC,IAAM,MAAA,IAAA,CAAK,QAAQ,MAAO,CAAA;AAAA,MACxB,SAAW,EAAA;AAAA,KACZ,CAAA;AAAA;AAEL;;;;"}
@@ -6,10 +6,12 @@ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
6
6
  var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plugin-scorecard-node');
7
7
  var MetricProvidersRegistry = require('./providers/MetricProvidersRegistry.cjs.js');
8
8
  var CatalogMetricService = require('./service/CatalogMetricService.cjs.js');
9
- var catalogClient = require('@backstage/catalog-client');
10
9
  var ThresholdEvaluator = require('./threshold/ThresholdEvaluator.cjs.js');
11
10
  var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
12
11
  var rules = require('./permissions/rules.cjs.js');
12
+ var migration = require('./database/migration.cjs.js');
13
+ var DatabaseMetricValues = require('./database/DatabaseMetricValues.cjs.js');
14
+ var index = require('./scheduler/index.cjs.js');
13
15
 
14
16
  const scorecardPlugin = backendPluginApi.createBackendPlugin({
15
17
  pluginId: "scorecard",
@@ -24,34 +26,53 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
24
26
  });
25
27
  env.registerInit({
26
28
  deps: {
27
- discovery: backendPluginApi.coreServices.discovery,
28
29
  auth: backendPluginApi.coreServices.auth,
29
- httpRouter: backendPluginApi.coreServices.httpRouter,
30
30
  catalog: pluginCatalogNode.catalogServiceRef,
31
+ config: backendPluginApi.coreServices.rootConfig,
32
+ database: backendPluginApi.coreServices.database,
33
+ httpRouter: backendPluginApi.coreServices.httpRouter,
31
34
  httpAuth: backendPluginApi.coreServices.httpAuth,
35
+ logger: backendPluginApi.coreServices.logger,
32
36
  permissions: backendPluginApi.coreServices.permissions,
33
- permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry
37
+ permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry,
38
+ scheduler: backendPluginApi.coreServices.scheduler
34
39
  },
35
40
  async init({
36
- discovery,
37
41
  auth,
42
+ catalog,
43
+ config,
44
+ database,
38
45
  httpRouter,
39
46
  httpAuth,
47
+ logger,
40
48
  permissions,
41
- permissionsRegistry
49
+ permissionsRegistry,
50
+ scheduler
42
51
  }) {
43
52
  permissionsRegistry.addResourceType({
44
53
  resourceRef: rules.scorecardMetricPermissionResourceRef,
45
54
  permissions: backstagePluginScorecardCommon.scorecardPermissions,
46
55
  rules: Object.values(rules.rules)
47
56
  });
48
- const catalogClient$1 = new catalogClient.CatalogClient({ discoveryApi: discovery });
57
+ await migration.migrate(database);
58
+ const client = await database.getClient();
59
+ const dbMetricValues = new DatabaseMetricValues.DatabaseMetricValues(client);
49
60
  const catalogMetricService = new CatalogMetricService.CatalogMetricService({
50
- catalogApi: catalogClient$1,
61
+ catalog,
62
+ auth,
51
63
  registry: metricProvidersRegistry,
52
64
  thresholdEvaluator: new ThresholdEvaluator.ThresholdEvaluator(),
53
- auth
65
+ database: dbMetricValues
54
66
  });
67
+ index.Scheduler.create({
68
+ auth,
69
+ catalog,
70
+ config,
71
+ logger,
72
+ scheduler,
73
+ database: dbMetricValues,
74
+ metricProvidersRegistry
75
+ }).start();
55
76
  httpRouter.use(
56
77
  await router.createRouter({
57
78
  metricProvidersRegistry,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n httpRouter: coreServices.httpRouter,\n catalog: catalogServiceRef,\n httpAuth: coreServices.httpAuth,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n async init({\n discovery,\n auth,\n httpRouter,\n httpAuth,\n permissions,\n permissionsRegistry,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n const catalogClient = new CatalogClient({ discoveryApi: discovery });\n const catalogMetricService = new CatalogMetricService({\n catalogApi: catalogClient,\n registry: metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n auth,\n });\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n httpAuth,\n permissions,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardMetricsExtensionPoint","coreServices","catalogServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","catalogClient","CatalogClient","CatalogMetricService","ThresholdEvaluator","createRouter"],"mappings":";;;;;;;;;;;;;AAwCO,MAAM,kBAAkBA,oCAAoB,CAAA;AAAA,EACjD,QAAU,EAAA,WAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,uBAAA,GAA0B,IAAIC,+CAAwB,EAAA;AAE5D,IAAA,GAAA,CAAI,uBAAuBC,2DAAgC,EAAA;AAAA,MACzD,qBAAqB,kBAAsC,EAAA;AACzD,QAAA,kBAAA,CAAmB,QAAQ,CAAkB,cAAA,KAAA;AAC3C,UAAA,uBAAA,CAAwB,SAAS,cAAc,CAAA;AAAA,SAChD,CAAA;AAAA;AACH,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,WAAWC,6BAAa,CAAA,SAAA;AAAA,QACxB,MAAMA,6BAAa,CAAA,IAAA;AAAA,QACnB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,OAAS,EAAAC,mCAAA;AAAA,QACT,UAAUD,6BAAa,CAAA,QAAA;AAAA,QACvB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,qBAAqBA,6BAAa,CAAA;AAAA,OACpC;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,SAAA;AAAA,QACA,IAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACC,EAAA;AACD,QAAA,mBAAA,CAAoB,eAAgB,CAAA;AAAA,UAClC,WAAa,EAAAE,0CAAA;AAAA,UACb,WAAa,EAAAC,mDAAA;AAAA,UACb,KAAA,EAAO,MAAO,CAAA,MAAA,CAAOC,WAAc;AAAA,SACpC,CAAA;AAED,QAAA,MAAMC,kBAAgB,IAAIC,2BAAA,CAAc,EAAE,YAAA,EAAc,WAAW,CAAA;AACnE,QAAM,MAAA,oBAAA,GAAuB,IAAIC,yCAAqB,CAAA;AAAA,UACpD,UAAY,EAAAF,eAAA;AAAA,UACZ,QAAU,EAAA,uBAAA;AAAA,UACV,kBAAA,EAAoB,IAAIG,qCAAmB,EAAA;AAAA,UAC3C;AAAA,SACD,CAAA;AAED,QAAW,UAAA,CAAA,GAAA;AAAA,UACT,MAAMC,mBAAa,CAAA;AAAA,YACjB,uBAAA;AAAA,YACA,oBAAA;AAAA,YACA,QAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAAA;AACF,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\nimport { migrate } from './database/migration';\nimport { DatabaseMetricValues } from './database/DatabaseMetricValues';\nimport { Scheduler } from './scheduler';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n config: coreServices.rootConfig,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n scheduler: coreServices.scheduler,\n },\n async init({\n auth,\n catalog,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n // Run database migrations\n await migrate(database);\n\n const client = await database.getClient();\n const dbMetricValues = new DatabaseMetricValues(client);\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n database: dbMetricValues,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n }).start();\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n httpAuth,\n permissions,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardMetricsExtensionPoint","coreServices","catalogServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","migrate","DatabaseMetricValues","CatalogMetricService","ThresholdEvaluator","Scheduler","createRouter"],"mappings":";;;;;;;;;;;;;;;AA0CO,MAAM,kBAAkBA,oCAAoB,CAAA;AAAA,EACjD,QAAU,EAAA,WAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,uBAAA,GAA0B,IAAIC,+CAAwB,EAAA;AAE5D,IAAA,GAAA,CAAI,uBAAuBC,2DAAgC,EAAA;AAAA,MACzD,qBAAqB,kBAAsC,EAAA;AACzD,QAAA,kBAAA,CAAmB,QAAQ,CAAkB,cAAA,KAAA;AAC3C,UAAA,uBAAA,CAAwB,SAAS,cAAc,CAAA;AAAA,SAChD,CAAA;AAAA;AACH,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,MAAMC,6BAAa,CAAA,IAAA;AAAA,QACnB,OAAS,EAAAC,mCAAA;AAAA,QACT,QAAQD,6BAAa,CAAA,UAAA;AAAA,QACrB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,QAAQA,6BAAa,CAAA,MAAA;AAAA,QACrB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,qBAAqBA,6BAAa,CAAA,mBAAA;AAAA,QAClC,WAAWA,6BAAa,CAAA;AAAA,OAC1B;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,IAAA;AAAA,QACA,OAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,MAAA;AAAA,QACA,WAAA;AAAA,QACA,mBAAA;AAAA,QACA;AAAA,OACC,EAAA;AACD,QAAA,mBAAA,CAAoB,eAAgB,CAAA;AAAA,UAClC,WAAa,EAAAE,0CAAA;AAAA,UACb,WAAa,EAAAC,mDAAA;AAAA,UACb,KAAA,EAAO,MAAO,CAAA,MAAA,CAAOC,WAAc;AAAA,SACpC,CAAA;AAGD,QAAA,MAAMC,kBAAQ,QAAQ,CAAA;AAEtB,QAAM,MAAA,MAAA,GAAS,MAAM,QAAA,CAAS,SAAU,EAAA;AACxC,QAAM,MAAA,cAAA,GAAiB,IAAIC,yCAAA,CAAqB,MAAM,CAAA;AAEtD,QAAM,MAAA,oBAAA,GAAuB,IAAIC,yCAAqB,CAAA;AAAA,UACpD,OAAA;AAAA,UACA,IAAA;AAAA,UACA,QAAU,EAAA,uBAAA;AAAA,UACV,kBAAA,EAAoB,IAAIC,qCAAmB,EAAA;AAAA,UAC3C,QAAU,EAAA;AAAA,SACX,CAAA;AAED,QAAAC,eAAA,CAAU,MAAO,CAAA;AAAA,UACf,IAAA;AAAA,UACA,OAAA;AAAA,UACA,MAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,QAAU,EAAA,cAAA;AAAA,UACV;AAAA,SACD,EAAE,KAAM,EAAA;AAET,QAAW,UAAA,CAAA,GAAA;AAAA,UACT,MAAMC,mBAAa,CAAA;AAAA,YACjB,uBAAA;AAAA,YACA,oBAAA;AAAA,YACA,QAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAAA;AACF,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
@@ -60,6 +60,9 @@ class MetricProvidersRegistry {
60
60
  return { providerId, error: result.reason };
61
61
  });
62
62
  }
63
+ listProviders() {
64
+ return Array.from(this.metricProviders.values());
65
+ }
63
66
  listMetrics() {
64
67
  return [...this.metricProviders.values()].map(
65
68
  (provider) => provider.getMetric()
@@ -1 +1 @@
1
- {"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerId = metricProvider.getProviderId();\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metricId = metricProvider.getMetric().id;\n\n if (providerId !== metricId) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, provider ID must match metric ID '${metricId}'`,\n );\n }\n\n const expectedPrefix = `${providerDatasource}.`;\n if (\n !providerId.startsWith(expectedPrefix) ||\n providerId === expectedPrefix\n ) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(providerId)) {\n throw new ConflictError(\n `Metric provider with ID '${providerId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(providerId, metricProvider);\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(providerId);\n }\n\n getProvider(providerId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(providerId);\n if (!metricProvider) {\n throw new NotFoundError(\n `Metric provider with ID '${providerId}' is not registered.`,\n );\n }\n return metricProvider;\n }\n\n getMetric(providerId: string): Metric {\n return this.getProvider(providerId).getMetric();\n }\n\n async calculateMetric(\n providerId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(providerId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n providerIds: string[],\n entity: Entity,\n ): Promise<{ providerId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n providerIds.map(providerId => this.calculateMetric(providerId, entity)),\n );\n\n return results.map((result, index) => {\n const providerId = providerIds[index];\n if (result.status === 'fulfilled') {\n return { providerId, value: result.value };\n }\n return { providerId, error: result.reason as Error };\n });\n }\n\n listMetrics(): Metric[] {\n return [...this.metricProviders.values()].map(provider =>\n provider.getMetric(),\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n return Array.from(providerIdsOfDatasource)\n .map(providerId => this.metricProviders.get(providerId))\n .filter((provider): provider is MetricProvider => provider !== undefined)\n .map(provider => provider.getMetric());\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}
1
+ {"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerId = metricProvider.getProviderId();\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metricId = metricProvider.getMetric().id;\n\n if (providerId !== metricId) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, provider ID must match metric ID '${metricId}'`,\n );\n }\n\n const expectedPrefix = `${providerDatasource}.`;\n if (\n !providerId.startsWith(expectedPrefix) ||\n providerId === expectedPrefix\n ) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(providerId)) {\n throw new ConflictError(\n `Metric provider with ID '${providerId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(providerId, metricProvider);\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(providerId);\n }\n\n getProvider(providerId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(providerId);\n if (!metricProvider) {\n throw new NotFoundError(\n `Metric provider with ID '${providerId}' is not registered.`,\n );\n }\n return metricProvider;\n }\n\n getMetric(providerId: string): Metric {\n return this.getProvider(providerId).getMetric();\n }\n\n async calculateMetric(\n providerId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(providerId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n providerIds: string[],\n entity: Entity,\n ): Promise<{ providerId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n providerIds.map(providerId => this.calculateMetric(providerId, entity)),\n );\n\n return results.map((result, index) => {\n const providerId = providerIds[index];\n if (result.status === 'fulfilled') {\n return { providerId, value: result.value };\n }\n return { providerId, error: result.reason as Error };\n });\n }\n\n listProviders(): MetricProvider[] {\n return Array.from(this.metricProviders.values());\n }\n\n listMetrics(): Metric[] {\n return [...this.metricProviders.values()].map(provider =>\n provider.getMetric(),\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n return Array.from(providerIdsOfDatasource)\n .map(providerId => this.metricProviders.get(providerId))\n .filter((provider): provider is MetricProvider => provider !== undefined)\n .map(provider => provider.getMetric());\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const CLEANUP_EXPIRED_METRICS_ID = "cleanup-expired-metrics";
4
+
5
+ exports.CLEANUP_EXPIRED_METRICS_ID = CLEANUP_EXPIRED_METRICS_ID;
6
+ //# sourceMappingURL=constants.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.cjs.js","sources":["../../src/scheduler/constants.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const CLEANUP_EXPIRED_METRICS_ID = 'cleanup-expired-metrics' as const;\n"],"names":[],"mappings":";;AAgBO,MAAM,0BAA6B,GAAA;;;;"}
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ var constants = require('./constants.cjs.js');
4
+ var CleanupExpiredMetricsTask = require('./tasks/CleanupExpiredMetricsTask.cjs.js');
5
+ var PullMetricsByProviderTask = require('./tasks/PullMetricsByProviderTask.cjs.js');
6
+
7
+ class Scheduler {
8
+ auth;
9
+ catalog;
10
+ config;
11
+ logger;
12
+ scheduler;
13
+ database;
14
+ metricProvidersRegistry;
15
+ tasks = [];
16
+ constructor(options) {
17
+ this.auth = options.auth;
18
+ this.catalog = options.catalog;
19
+ this.config = options.config;
20
+ this.logger = options.logger;
21
+ this.scheduler = options.scheduler;
22
+ this.database = options.database;
23
+ this.metricProvidersRegistry = options.metricProvidersRegistry;
24
+ }
25
+ static create(options) {
26
+ return new Scheduler(options);
27
+ }
28
+ async start() {
29
+ this.initializeTasks();
30
+ this.initializeTasksByProviders();
31
+ const results = await Promise.allSettled(
32
+ this.tasks.map(({ name, task }) => this.startTask(name, task))
33
+ );
34
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
35
+ let index = 0;
36
+ for (const result of results) {
37
+ if (result.status === "rejected") {
38
+ this.logger.warn(
39
+ `Failed to start task '${this.tasks[index].name}': ${result.reason}`
40
+ );
41
+ }
42
+ index++;
43
+ }
44
+ this.logger.info(`Scheduled: ${successCount}/${this.tasks.length} tasks`);
45
+ }
46
+ initializeTasks() {
47
+ this.tasks = [
48
+ {
49
+ name: constants.CLEANUP_EXPIRED_METRICS_ID,
50
+ task: new CleanupExpiredMetricsTask.CleanupExpiredMetricsTask({
51
+ scheduler: this.scheduler,
52
+ logger: this.logger,
53
+ database: this.database,
54
+ config: this.config
55
+ })
56
+ }
57
+ ];
58
+ }
59
+ initializeTasksByProviders() {
60
+ const providers = this.metricProvidersRegistry.listProviders();
61
+ for (const provider of providers) {
62
+ this.tasks.push({
63
+ name: provider.getProviderId(),
64
+ task: new PullMetricsByProviderTask.PullMetricsByProviderTask(
65
+ {
66
+ scheduler: this.scheduler,
67
+ logger: this.logger,
68
+ database: this.database,
69
+ config: this.config,
70
+ catalog: this.catalog,
71
+ auth: this.auth
72
+ },
73
+ provider
74
+ )
75
+ });
76
+ }
77
+ }
78
+ async startTask(name, task) {
79
+ try {
80
+ await task.start();
81
+ } catch (error) {
82
+ this.logger.error(`Failed to start task '${name}': ${error}`, error);
83
+ throw error;
84
+ }
85
+ }
86
+ }
87
+
88
+ exports.Scheduler = Scheduler;
89
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../../src/scheduler/index.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n LoggerService,\n SchedulerService,\n} from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport type { Config } from '@backstage/config';\nimport { CLEANUP_EXPIRED_METRICS_ID } from './constants';\nimport { CleanupExpiredMetricsTask } from './tasks/CleanupExpiredMetricsTask';\nimport { PullMetricsByProviderTask } from './tasks/PullMetricsByProviderTask';\nimport { SchedulerOptions, SchedulerTask } from './types';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\n\nexport class Scheduler {\n private readonly auth: AuthService;\n private readonly catalog: CatalogService;\n private readonly config: Config;\n private readonly logger: LoggerService;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n private readonly metricProvidersRegistry: MetricProvidersRegistry;\n\n private tasks: Array<{ name: string; task: SchedulerTask }> = [];\n\n private constructor(options: SchedulerOptions) {\n this.auth = options.auth;\n this.catalog = options.catalog;\n this.config = options.config;\n this.logger = options.logger;\n this.scheduler = options.scheduler;\n this.database = options.database;\n this.metricProvidersRegistry = options.metricProvidersRegistry;\n }\n\n static create(options: SchedulerOptions): Scheduler {\n return new Scheduler(options);\n }\n\n async start(): Promise<void> {\n this.initializeTasks();\n this.initializeTasksByProviders();\n\n const results = await Promise.allSettled(\n this.tasks.map(({ name, task }) => this.startTask(name, task)),\n );\n\n const successCount = results.filter(r => r.status === 'fulfilled').length;\n\n let index = 0;\n\n for (const result of results) {\n if (result.status === 'rejected') {\n this.logger.warn(\n `Failed to start task '${this.tasks[index].name}': ${result.reason}`,\n );\n }\n index++;\n }\n\n this.logger.info(`Scheduled: ${successCount}/${this.tasks.length} tasks`);\n }\n\n private initializeTasks(): void {\n this.tasks = [\n {\n name: CLEANUP_EXPIRED_METRICS_ID,\n task: new CleanupExpiredMetricsTask({\n scheduler: this.scheduler,\n logger: this.logger,\n database: this.database,\n config: this.config,\n }),\n },\n ];\n }\n\n private initializeTasksByProviders(): void {\n const providers = this.metricProvidersRegistry.listProviders();\n\n for (const provider of providers) {\n this.tasks.push({\n name: provider.getProviderId(),\n task: new PullMetricsByProviderTask(\n {\n scheduler: this.scheduler,\n logger: this.logger,\n database: this.database,\n config: this.config,\n catalog: this.catalog,\n auth: this.auth,\n },\n provider,\n ),\n });\n }\n }\n\n private async startTask(name: string, task: SchedulerTask): Promise<void> {\n try {\n await task.start();\n } catch (error) {\n this.logger.error(`Failed to start task '${name}': ${error}`, error);\n throw error;\n }\n }\n}\n"],"names":["CLEANUP_EXPIRED_METRICS_ID","CleanupExpiredMetricsTask","PullMetricsByProviderTask"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ var constants = require('../constants.cjs.js');
4
+ var uuid = require('uuid');
5
+ var utils = require('./utils.cjs.js');
6
+
7
+ class CleanupExpiredMetricsTask {
8
+ config;
9
+ logger;
10
+ scheduler;
11
+ database;
12
+ static CLEANUP_SCHEDULE = {
13
+ frequency: { days: 1 },
14
+ timeout: { minutes: 2 },
15
+ initialDelay: { seconds: 3 }
16
+ };
17
+ static DEFAULT_DATA_RETENTION_DAYS = 365;
18
+ constructor(options) {
19
+ this.config = options.config;
20
+ this.logger = options.logger;
21
+ this.scheduler = options.scheduler;
22
+ this.database = options.database;
23
+ }
24
+ async start() {
25
+ const taskRunner = this.scheduler.createScheduledTaskRunner(
26
+ CleanupExpiredMetricsTask.CLEANUP_SCHEDULE
27
+ );
28
+ await taskRunner.run({
29
+ id: constants.CLEANUP_EXPIRED_METRICS_ID,
30
+ fn: async () => {
31
+ const logger = this.logger.child({
32
+ taskId: constants.CLEANUP_EXPIRED_METRICS_ID,
33
+ taskInstanceId: uuid.v4()
34
+ });
35
+ try {
36
+ await this.cleanupExpiredMetrics(logger);
37
+ } catch (error) {
38
+ logger.error("Failed to cleanup expired metrics", error);
39
+ }
40
+ }
41
+ });
42
+ }
43
+ async cleanupExpiredMetrics(logger) {
44
+ const dataRetentionDays = this.getDataRetentionDays();
45
+ const olderThan = new Date(
46
+ Date.now() - utils.daysToMilliseconds(dataRetentionDays)
47
+ );
48
+ const deletedCount = await this.database.cleanupExpiredMetrics(olderThan);
49
+ logger.info(
50
+ `Deleted ${deletedCount} expired metrics older than ${dataRetentionDays} days`
51
+ );
52
+ }
53
+ getDataRetentionDays() {
54
+ return this.config.getOptionalNumber("scorecard.dataRetentionDays") ?? CleanupExpiredMetricsTask.DEFAULT_DATA_RETENTION_DAYS;
55
+ }
56
+ }
57
+
58
+ exports.CleanupExpiredMetricsTask = CleanupExpiredMetricsTask;
59
+ //# sourceMappingURL=CleanupExpiredMetricsTask.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CleanupExpiredMetricsTask.cjs.js","sources":["../../../src/scheduler/tasks/CleanupExpiredMetricsTask.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n LoggerService,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { CLEANUP_EXPIRED_METRICS_ID } from '../constants';\nimport { v4 as uuid } from 'uuid';\nimport { daysToMilliseconds } from './utils';\nimport type { Config } from '@backstage/config';\nimport { SchedulerTask, SchedulerOptions } from '../types';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\n\ntype Options = Pick<\n SchedulerOptions,\n 'scheduler' | 'logger' | 'database' | 'config'\n>;\n\nexport class CleanupExpiredMetricsTask implements SchedulerTask {\n private readonly config: Config;\n private readonly logger: LoggerService;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n\n private static readonly CLEANUP_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { days: 1 },\n timeout: { minutes: 2 },\n initialDelay: { seconds: 3 },\n };\n\n private static readonly DEFAULT_DATA_RETENTION_DAYS = 365;\n\n constructor(options: Options) {\n this.config = options.config;\n this.logger = options.logger;\n this.scheduler = options.scheduler;\n this.database = options.database;\n }\n\n async start(): Promise<void> {\n const taskRunner = this.scheduler.createScheduledTaskRunner(\n CleanupExpiredMetricsTask.CLEANUP_SCHEDULE,\n );\n\n await taskRunner.run({\n id: CLEANUP_EXPIRED_METRICS_ID,\n fn: async () => {\n const logger = this.logger.child({\n taskId: CLEANUP_EXPIRED_METRICS_ID,\n taskInstanceId: uuid(),\n });\n\n try {\n await this.cleanupExpiredMetrics(logger);\n } catch (error) {\n logger.error('Failed to cleanup expired metrics', error);\n }\n },\n });\n }\n\n private async cleanupExpiredMetrics(logger: LoggerService): Promise<void> {\n const dataRetentionDays = this.getDataRetentionDays();\n\n const olderThan = new Date(\n Date.now() - daysToMilliseconds(dataRetentionDays),\n );\n\n const deletedCount = await this.database.cleanupExpiredMetrics(olderThan);\n logger.info(\n `Deleted ${deletedCount} expired metrics older than ${dataRetentionDays} days`,\n );\n }\n\n private getDataRetentionDays(): number {\n return (\n this.config.getOptionalNumber('scorecard.dataRetentionDays') ??\n CleanupExpiredMetricsTask.DEFAULT_DATA_RETENTION_DAYS\n );\n }\n}\n"],"names":["CLEANUP_EXPIRED_METRICS_ID","uuid","daysToMilliseconds"],"mappings":";;;;;;AAiCO,MAAM,yBAAmD,CAAA;AAAA,EAC7C,MAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EAEjB,OAAwB,gBACtB,GAAA;AAAA,IACE,SAAA,EAAW,EAAE,IAAA,EAAM,CAAE,EAAA;AAAA,IACrB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAE,EAAA;AAAA,IACtB,YAAA,EAAc,EAAE,OAAA,EAAS,CAAE;AAAA,GAC7B;AAAA,EAEF,OAAwB,2BAA8B,GAAA,GAAA;AAAA,EAEtD,YAAY,OAAkB,EAAA;AAC5B,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,YAAY,OAAQ,CAAA,SAAA;AACzB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AAAA;AAC1B,EAEA,MAAM,KAAuB,GAAA;AAC3B,IAAM,MAAA,UAAA,GAAa,KAAK,SAAU,CAAA,yBAAA;AAAA,MAChC,yBAA0B,CAAA;AAAA,KAC5B;AAEA,IAAA,MAAM,WAAW,GAAI,CAAA;AAAA,MACnB,EAAI,EAAAA,oCAAA;AAAA,MACJ,IAAI,YAAY;AACd,QAAM,MAAA,MAAA,GAAS,IAAK,CAAA,MAAA,CAAO,KAAM,CAAA;AAAA,UAC/B,MAAQ,EAAAA,oCAAA;AAAA,UACR,gBAAgBC,OAAK;AAAA,SACtB,CAAA;AAED,QAAI,IAAA;AACF,UAAM,MAAA,IAAA,CAAK,sBAAsB,MAAM,CAAA;AAAA,iBAChC,KAAO,EAAA;AACd,UAAO,MAAA,CAAA,KAAA,CAAM,qCAAqC,KAAK,CAAA;AAAA;AACzD;AACF,KACD,CAAA;AAAA;AACH,EAEA,MAAc,sBAAsB,MAAsC,EAAA;AACxE,IAAM,MAAA,iBAAA,GAAoB,KAAK,oBAAqB,EAAA;AAEpD,IAAA,MAAM,YAAY,IAAI,IAAA;AAAA,MACpB,IAAK,CAAA,GAAA,EAAQ,GAAAC,wBAAA,CAAmB,iBAAiB;AAAA,KACnD;AAEA,IAAA,MAAM,YAAe,GAAA,MAAM,IAAK,CAAA,QAAA,CAAS,sBAAsB,SAAS,CAAA;AACxE,IAAO,MAAA,CAAA,IAAA;AAAA,MACL,CAAA,QAAA,EAAW,YAAY,CAAA,4BAAA,EAA+B,iBAAiB,CAAA,KAAA;AAAA,KACzE;AAAA;AACF,EAEQ,oBAA+B,GAAA;AACrC,IAAA,OACE,IAAK,CAAA,MAAA,CAAO,iBAAkB,CAAA,6BAA6B,KAC3D,yBAA0B,CAAA,2BAAA;AAAA;AAGhC;;;;"}
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var uuid = require('uuid');
5
+ var catalogModel = require('@backstage/catalog-model');
6
+
7
+ class PullMetricsByProviderTask {
8
+ config;
9
+ auth;
10
+ providerId;
11
+ logger;
12
+ catalog;
13
+ provider;
14
+ scheduler;
15
+ database;
16
+ static CATALOG_BATCH_SIZE = 50;
17
+ static DEFAULT_SCHEDULE = {
18
+ frequency: { hours: 1 },
19
+ timeout: { minutes: 15 },
20
+ initialDelay: { seconds: 3 }
21
+ };
22
+ constructor(options, provider) {
23
+ this.config = options.config;
24
+ this.auth = options.auth;
25
+ this.providerId = provider.getProviderId();
26
+ this.logger = options.logger;
27
+ this.catalog = options.catalog;
28
+ this.provider = provider;
29
+ this.scheduler = options.scheduler;
30
+ this.database = options.database;
31
+ }
32
+ async start() {
33
+ const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;
34
+ const schedule = this.getScheduleFromConfig(scheduleConfigPath);
35
+ const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);
36
+ await taskRunner.run({
37
+ id: this.providerId,
38
+ fn: async () => {
39
+ const logger = this.logger.child({
40
+ class: this.constructor.name,
41
+ taskId: this.providerId,
42
+ taskInstanceId: uuid.v4()
43
+ });
44
+ try {
45
+ await this.pullProviderMetrics(this.provider, logger);
46
+ } catch (error) {
47
+ logger.error(
48
+ `${this.providerId} pulling metrics failed, ${error}`,
49
+ error
50
+ );
51
+ }
52
+ }
53
+ });
54
+ }
55
+ getScheduleFromConfig(schedulePath) {
56
+ return this.config.has(schedulePath) ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
57
+ this.config.getConfig(schedulePath)
58
+ ) : PullMetricsByProviderTask.DEFAULT_SCHEDULE;
59
+ }
60
+ async pullProviderMetrics(provider, logger) {
61
+ logger.info(`Pulling metrics for ${this.providerId}`);
62
+ let totalProcessed = 0;
63
+ let cursor = void 0;
64
+ try {
65
+ do {
66
+ const entitiesResponse = await this.catalog.queryEntities(
67
+ {
68
+ filter: provider.getCatalogFilter(),
69
+ limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,
70
+ ...cursor ? { cursor } : {}
71
+ },
72
+ { credentials: await this.auth.getOwnServiceCredentials() }
73
+ );
74
+ cursor = entitiesResponse.pageInfo.nextCursor;
75
+ const batchResults = await Promise.allSettled(
76
+ entitiesResponse.items.map(async (entity) => {
77
+ try {
78
+ const value = await provider.calculateMetric(entity);
79
+ return {
80
+ catalog_entity_ref: catalogModel.stringifyEntityRef(entity),
81
+ metric_id: this.providerId,
82
+ value,
83
+ timestamp: /* @__PURE__ */ new Date(),
84
+ error_message: void 0
85
+ };
86
+ } catch (error) {
87
+ return {
88
+ catalog_entity_ref: catalogModel.stringifyEntityRef(entity),
89
+ metric_id: this.providerId,
90
+ value: void 0,
91
+ timestamp: /* @__PURE__ */ new Date(),
92
+ error_message: error instanceof Error ? error.message : String(error)
93
+ };
94
+ }
95
+ })
96
+ ).then(
97
+ (promises) => promises.reduce((acc, curr) => {
98
+ if (curr.status === "fulfilled") {
99
+ return [...acc, curr.value];
100
+ }
101
+ return acc;
102
+ }, [])
103
+ );
104
+ await this.database.createMetricValues(batchResults);
105
+ totalProcessed += entitiesResponse.items.length;
106
+ } while (cursor !== void 0);
107
+ logger.info(
108
+ `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`
109
+ );
110
+ } catch (error) {
111
+ logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);
112
+ throw error;
113
+ }
114
+ }
115
+ }
116
+
117
+ exports.PullMetricsByProviderTask = PullMetricsByProviderTask;
118
+ //# sourceMappingURL=PullMetricsByProviderTask.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PullMetricsByProviderTask.cjs.js","sources":["../../../src/scheduler/tasks/PullMetricsByProviderTask.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport {\n AuthService,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { v4 as uuid } from 'uuid';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { DbMetricValue } from '../../database/types';\nimport { SchedulerOptions, SchedulerTask } from '../types';\n\ntype Options = Pick<\n SchedulerOptions,\n 'scheduler' | 'logger' | 'database' | 'config' | 'catalog' | 'auth'\n>;\n\nexport class PullMetricsByProviderTask implements SchedulerTask {\n private readonly config: Config;\n private readonly auth: AuthService;\n private readonly providerId: string;\n private readonly logger: LoggerService;\n private readonly catalog: CatalogService;\n private readonly provider: MetricProvider;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n\n private static readonly CATALOG_BATCH_SIZE = 50;\n\n private static readonly DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { hours: 1 },\n timeout: { minutes: 15 },\n initialDelay: { seconds: 3 },\n };\n\n constructor(options: Options, provider: MetricProvider) {\n this.config = options.config;\n this.auth = options.auth;\n this.providerId = provider.getProviderId();\n this.logger = options.logger;\n this.catalog = options.catalog;\n this.provider = provider;\n this.scheduler = options.scheduler;\n this.database = options.database;\n }\n\n async start(): Promise<void> {\n const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;\n const schedule = this.getScheduleFromConfig(scheduleConfigPath);\n\n const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);\n\n await taskRunner.run({\n id: this.providerId,\n fn: async () => {\n const logger = this.logger.child({\n class: this.constructor.name,\n taskId: this.providerId,\n taskInstanceId: uuid(),\n });\n\n try {\n await this.pullProviderMetrics(this.provider, logger);\n } catch (error) {\n logger.error(\n `${this.providerId} pulling metrics failed, ${error}`,\n error,\n );\n }\n },\n });\n }\n\n private getScheduleFromConfig(\n schedulePath: string,\n ): SchedulerServiceTaskScheduleDefinition {\n return this.config.has(schedulePath)\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n this.config.getConfig(schedulePath),\n )\n : PullMetricsByProviderTask.DEFAULT_SCHEDULE;\n }\n\n private async pullProviderMetrics(\n provider: MetricProvider,\n logger: LoggerService,\n ): Promise<void> {\n logger.info(`Pulling metrics for ${this.providerId}`);\n\n let totalProcessed = 0;\n let cursor: string | undefined = undefined;\n\n try {\n do {\n const entitiesResponse = await this.catalog.queryEntities(\n {\n filter: provider.getCatalogFilter(),\n limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n cursor = entitiesResponse.pageInfo.nextCursor;\n\n const batchResults = await Promise.allSettled(\n entitiesResponse.items.map(async entity => {\n try {\n const value = await provider.calculateMetric(entity);\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value,\n timestamp: new Date(),\n error_message: undefined,\n };\n } catch (error) {\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value: undefined,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n };\n }\n }),\n ).then(promises =>\n promises.reduce((acc, curr) => {\n if (curr.status === 'fulfilled') {\n return [...acc, curr.value];\n }\n return acc;\n }, [] as Omit<DbMetricValue, 'id'>[]),\n );\n\n await this.database.createMetricValues(batchResults);\n totalProcessed += entitiesResponse.items.length;\n } while (cursor !== undefined);\n\n logger.info(\n `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`,\n );\n } catch (error) {\n logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);\n\n throw error;\n }\n }\n}\n"],"names":["uuid","readSchedulerServiceTaskScheduleDefinitionFromConfig","stringifyEntityRef"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const daysToMilliseconds = (days) => {
4
+ return days * 24 * 60 * 60 * 1e3;
5
+ };
6
+
7
+ exports.daysToMilliseconds = daysToMilliseconds;
8
+ //# sourceMappingURL=utils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.cjs.js","sources":["../../../src/scheduler/tasks/utils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const daysToMilliseconds = (days: number) => {\n return days * 24 * 60 * 60 * 1000;\n};\n"],"names":[],"mappings":";;AAgBa,MAAA,kBAAA,GAAqB,CAAC,IAAiB,KAAA;AAClD,EAAO,OAAA,IAAA,GAAO,EAAK,GAAA,EAAA,GAAK,EAAK,GAAA,GAAA;AAC/B;;;;"}
@@ -6,46 +6,46 @@ var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plu
6
6
  var permissionUtils = require('../permissions/permissionUtils.cjs.js');
7
7
 
8
8
  class CatalogMetricService {
9
- catalogApi;
9
+ catalog;
10
+ auth;
10
11
  registry;
11
12
  thresholdEvaluator;
12
- auth;
13
+ database;
13
14
  constructor(options) {
14
- this.thresholdEvaluator = options.thresholdEvaluator;
15
- this.registry = options.registry;
16
- this.catalogApi = options.catalogApi;
15
+ this.catalog = options.catalog;
17
16
  this.auth = options.auth;
17
+ this.registry = options.registry;
18
+ this.thresholdEvaluator = options.thresholdEvaluator;
19
+ this.database = options.database;
18
20
  }
19
21
  /**
20
- * Calculate metric results for a specific catalog entity.
22
+ * Get latest metric results for a specific catalog entity and metric providers.
21
23
  *
22
24
  * @param entityRef - Entity reference in format "kind:namespace/name"
23
- * @param providerIds - Optional array of provider IDs to calculate.
24
- * If not provided, calculates all available metrics.
25
+ * @param providerIds - Optional array of provider IDs to get latest metrics of.
26
+ * If not provided, gets all available latest metrics.
27
+ * @param filter - Permission filter
25
28
  * @returns Metric results with entity-specific thresholds applied
26
29
  */
27
- async calculateEntityMetrics(entityRef, providerIds, filter) {
28
- const token = await this.getServiceToken();
29
- const entity = await this.catalogApi.getEntityByRef(entityRef, token);
30
+ async getLatestEntityMetrics(entityRef, providerIds, filter) {
31
+ const entity = await this.catalog.getEntityByRef(entityRef, {
32
+ credentials: await this.auth.getOwnServiceCredentials()
33
+ });
30
34
  if (!entity) {
31
35
  throw new errors.NotFoundError(`Entity not found: ${entityRef}`);
32
36
  }
33
- const metricsToCalculate = providerIds ? this.registry.listMetrics().filter((m) => providerIds.includes(m.id)) : this.registry.listMetrics();
34
- const supportedMetricsToCalculate = metricsToCalculate.filter((m) => {
35
- const provider = this.registry.getProvider(m.id);
36
- return provider.supportsEntity(entity);
37
- });
38
- const authorizedMetricsToCalculate = permissionUtils.filterAuthorizedMetrics(
39
- supportedMetricsToCalculate,
37
+ const metricsToFetch = providerIds ? this.registry.listMetrics().filter((m) => providerIds.includes(m.id)) : this.registry.listMetrics();
38
+ const authorizedMetricsToFetch = permissionUtils.filterAuthorizedMetrics(
39
+ metricsToFetch,
40
40
  filter
41
41
  );
42
- const rawResults = await this.registry.calculateMetrics(
43
- authorizedMetricsToCalculate.map((m) => m.id),
44
- entity
42
+ const rawResults = await this.database.readLatestEntityMetricValues(
43
+ entityRef,
44
+ authorizedMetricsToFetch.map((m) => m.id)
45
45
  );
46
- return rawResults.map(({ providerId, value, error }, index) => {
47
- const provider = this.registry.getProvider(providerId);
48
- const metric = authorizedMetricsToCalculate[index];
46
+ return rawResults.map(({ metric_id, value, error_message, timestamp }) => {
47
+ const provider = this.registry.getProvider(metric_id);
48
+ const metric = provider.getMetric();
49
49
  let thresholds;
50
50
  let evaluation;
51
51
  let thresholdError;
@@ -67,7 +67,7 @@ class CatalogMetricService {
67
67
  } catch (e) {
68
68
  thresholdError = errors.stringifyError(e);
69
69
  }
70
- const isMetricCalcError = error || value === void 0;
70
+ const isMetricCalcError = error_message || value === void 0;
71
71
  return {
72
72
  id: metric.id,
73
73
  status: isMetricCalcError ? "error" : "success",
@@ -78,11 +78,11 @@ class CatalogMetricService {
78
78
  history: metric.history
79
79
  },
80
80
  ...isMetricCalcError && {
81
- error: error ? errors.stringifyError(error) : errors.stringifyError(new Error(`Metric value is 'undefined'`))
81
+ error: error_message ?? errors.stringifyError(new Error(`Metric value is 'undefined'`))
82
82
  },
83
83
  result: {
84
84
  value,
85
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
85
+ timestamp: new Date(timestamp).toISOString(),
86
86
  thresholdResult: {
87
87
  definition: thresholds,
88
88
  status: thresholdError ? "error" : "success",
@@ -93,15 +93,6 @@ class CatalogMetricService {
93
93
  };
94
94
  });
95
95
  }
96
- async getServiceToken() {
97
- if (!this.auth) {
98
- return void 0;
99
- }
100
- return await this.auth.getPluginRequestToken({
101
- onBehalfOf: await this.auth.getOwnServiceCredentials(),
102
- targetPluginId: "catalog"
103
- });
104
- }
105
96
  /**
106
97
  * Parse threshold overrides from entity annotations.
107
98
  * Looks for annotations in the format:
@@ -1 +1 @@
1
- {"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CatalogApi } from '@backstage/catalog-client';\nimport { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport {\n MetricResult,\n MetricType,\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { ThresholdEvaluator } from '../threshold/ThresholdEvaluator';\nimport { isError, NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n MetricProvider,\n ThresholdConfigFormatError,\n validateThresholds,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\n\nexport type CatalogMetricServiceOptions = {\n catalogApi: CatalogApi;\n registry: MetricProvidersRegistry;\n thresholdEvaluator: ThresholdEvaluator;\n auth?: AuthService;\n};\n\nexport class CatalogMetricService {\n private readonly catalogApi: CatalogApi;\n private readonly registry: MetricProvidersRegistry;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n private readonly auth?: AuthService;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.thresholdEvaluator = options.thresholdEvaluator;\n this.registry = options.registry;\n this.catalogApi = options.catalogApi;\n this.auth = options.auth;\n }\n\n /**\n * Calculate metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param providerIds - Optional array of provider IDs to calculate.\n * If not provided, calculates all available metrics.\n * @returns Metric results with entity-specific thresholds applied\n */\n async calculateEntityMetrics(\n entityRef: string,\n providerIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const token = await this.getServiceToken();\n const entity = await this.catalogApi.getEntityByRef(entityRef, token);\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToCalculate = providerIds\n ? this.registry.listMetrics().filter(m => providerIds.includes(m.id))\n : this.registry.listMetrics();\n\n const supportedMetricsToCalculate = metricsToCalculate.filter(m => {\n const provider = this.registry.getProvider(m.id);\n return provider.supportsEntity(entity);\n });\n\n const authorizedMetricsToCalculate = filterAuthorizedMetrics(\n supportedMetricsToCalculate,\n filter,\n );\n const rawResults = await this.registry.calculateMetrics(\n authorizedMetricsToCalculate.map(m => m.id),\n entity,\n );\n\n return rawResults.map(({ providerId, value, error }, index) => {\n const provider = this.registry.getProvider(providerId);\n const metric = authorizedMetricsToCalculate[index];\n\n let thresholds: ThresholdConfig | undefined;\n let evaluation: string | undefined;\n let thresholdError: string | undefined;\n try {\n thresholds = this.mergeEntityAndProviderThresholds(\n entity,\n provider,\n metric.type,\n );\n if (value === undefined) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else {\n evaluation = this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metric.type,\n thresholds,\n );\n }\n } catch (e) {\n thresholdError = stringifyError(e);\n }\n\n const isMetricCalcError = error || value === undefined;\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error: error\n ? stringifyError(error)\n : stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date().toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n });\n }\n\n private async getServiceToken(): Promise<{ token: string } | undefined> {\n if (!this.auth) {\n return undefined;\n }\n return await this.auth.getPluginRequestToken({\n onBehalfOf: await this.auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n }\n\n /**\n * Parse threshold overrides from entity annotations.\n * Looks for annotations in the format:\n * scorecard.io/{providerId}.thresholds.rules.{thresholdName}: \"{expression}\"\n *\n * @param entity - The catalog entity\n * @param providerId - The metric provider ID (e.g., 'jira.open_issues')\n * @param metricType - The metric type\n * @returns Threshold rules from entity annotations, or empty rules if none found or invalid\n */\n private parseEntityOverrideThresholds(\n entity: Entity,\n providerId: string,\n metricType: MetricType,\n ): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = `scorecard.io/${providerId}.thresholds.rules.`;\n const overrides: ThresholdRule[] = [];\n\n for (const [annotationKey, expression] of Object.entries(annotations)) {\n if (annotationKey.startsWith(prefix) && expression) {\n const key = annotationKey.substring(prefix.length);\n const entityRule = { key, expression };\n try {\n validateThresholds({ rules: [entityRule] }, metricType);\n overrides.push(entityRule);\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${annotationKey}: ${expression}' in entity '${stringifyEntityRef(\n entity,\n )}': ${e.message}`,\n );\n }\n throw e;\n }\n }\n }\n\n return overrides;\n }\n\n private mergeEntityAndProviderThresholds(\n entity: Entity,\n provider: MetricProvider,\n metricType: MetricType,\n ): ThresholdConfig {\n const providerThresholds = provider.getMetricThresholds();\n const entityOverrideThresholds = this.parseEntityOverrideThresholds(\n entity,\n provider.getProviderId(),\n metricType,\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityOverrideThresholds) {\n const foundKey = mergedRules.findIndex(rule => rule.key === override.key);\n if (foundKey !== -1) {\n mergedRules[foundKey] = override;\n } else {\n throw new ThresholdConfigFormatError(\n `Unable to override ${stringifyEntityRef(\n entity,\n )} thresholds by ${JSON.stringify(\n override,\n )}, metric provider ${provider.getProviderId()} does not support key ${\n override.key\n }`,\n );\n }\n }\n\n return {\n rules: mergedRules,\n };\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","stringifyError","validateThresholds","isError","ThresholdConfigFormatError","stringifyEntityRef"],"mappings":"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"}
1
+ {"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport {\n MetricResult,\n MetricType,\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { ThresholdEvaluator } from '../threshold/ThresholdEvaluator';\nimport { isError, NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n MetricProvider,\n ThresholdConfigFormatError,\n validateThresholds,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\n\nexport type CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n thresholdEvaluator: ThresholdEvaluator;\n database: DatabaseMetricValues;\n};\n\nexport class CatalogMetricService {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n private readonly database: DatabaseMetricValues;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.thresholdEvaluator = options.thresholdEvaluator;\n this.database = options.database;\n }\n\n /**\n * Get latest metric results for a specific catalog entity and metric providers.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param providerIds - Optional array of provider IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n providerIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = providerIds\n ? this.registry.listMetrics().filter(m => providerIds.includes(m.id))\n : this.registry.listMetrics();\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(({ metric_id, value, error_message, timestamp }) => {\n const provider = this.registry.getProvider(metric_id);\n const metric = provider.getMetric();\n\n let thresholds: ThresholdConfig | undefined;\n let evaluation: string | undefined;\n let thresholdError: string | undefined;\n try {\n thresholds = this.mergeEntityAndProviderThresholds(\n entity,\n provider,\n metric.type,\n );\n if (value === undefined) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else {\n evaluation = this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metric.type,\n thresholds,\n );\n }\n } catch (e) {\n thresholdError = stringifyError(e);\n }\n\n const isMetricCalcError = error_message || value === undefined;\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n });\n }\n\n /**\n * Parse threshold overrides from entity annotations.\n * Looks for annotations in the format:\n * scorecard.io/{providerId}.thresholds.rules.{thresholdName}: \"{expression}\"\n *\n * @param entity - The catalog entity\n * @param providerId - The metric provider ID (e.g., 'jira.open_issues')\n * @param metricType - The metric type\n * @returns Threshold rules from entity annotations, or empty rules if none found or invalid\n */\n private parseEntityOverrideThresholds(\n entity: Entity,\n providerId: string,\n metricType: MetricType,\n ): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = `scorecard.io/${providerId}.thresholds.rules.`;\n const overrides: ThresholdRule[] = [];\n\n for (const [annotationKey, expression] of Object.entries(annotations)) {\n if (annotationKey.startsWith(prefix) && expression) {\n const key = annotationKey.substring(prefix.length);\n const entityRule = { key, expression };\n try {\n validateThresholds({ rules: [entityRule] }, metricType);\n overrides.push(entityRule);\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${annotationKey}: ${expression}' in entity '${stringifyEntityRef(\n entity,\n )}': ${e.message}`,\n );\n }\n throw e;\n }\n }\n }\n\n return overrides;\n }\n\n private mergeEntityAndProviderThresholds(\n entity: Entity,\n provider: MetricProvider,\n metricType: MetricType,\n ): ThresholdConfig {\n const providerThresholds = provider.getMetricThresholds();\n const entityOverrideThresholds = this.parseEntityOverrideThresholds(\n entity,\n provider.getProviderId(),\n metricType,\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityOverrideThresholds) {\n const foundKey = mergedRules.findIndex(rule => rule.key === override.key);\n if (foundKey !== -1) {\n mergedRules[foundKey] = override;\n } else {\n throw new ThresholdConfigFormatError(\n `Unable to override ${stringifyEntityRef(\n entity,\n )} thresholds by ${JSON.stringify(\n override,\n )}, metric provider ${provider.getProviderId()} does not support key ${\n override.key\n }`,\n );\n }\n }\n\n return {\n rules: mergedRules,\n };\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","stringifyError","validateThresholds","isError","ThresholdConfigFormatError","stringifyEntityRef"],"mappings":";;;;;;;AAiDO,MAAM,oBAAqB,CAAA;AAAA,EACf,OAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,kBAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAsC,EAAA;AAChD,IAAA,IAAA,CAAK,UAAU,OAAQ,CAAA,OAAA;AACvB,IAAA,IAAA,CAAK,OAAO,OAAQ,CAAA,IAAA;AACpB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AACxB,IAAA,IAAA,CAAK,qBAAqB,OAAQ,CAAA,kBAAA;AAClC,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AAAA;AAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,sBAAA,CACJ,SACA,EAAA,WAAA,EACA,MAGyB,EAAA;AACzB,IAAA,MAAM,MAAS,GAAA,MAAM,IAAK,CAAA,OAAA,CAAQ,eAAe,SAAW,EAAA;AAAA,MAC1D,WAAa,EAAA,MAAM,IAAK,CAAA,IAAA,CAAK,wBAAyB;AAAA,KACvD,CAAA;AACD,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAA,MAAM,IAAIA,oBAAA,CAAc,CAAqB,kBAAA,EAAA,SAAS,CAAE,CAAA,CAAA;AAAA;AAG1D,IAAA,MAAM,iBAAiB,WACnB,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,GAAc,MAAO,CAAA,CAAA,CAAA,KAAK,WAAY,CAAA,QAAA,CAAS,EAAE,EAAE,CAAC,CAClE,GAAA,IAAA,CAAK,SAAS,WAAY,EAAA;AAE9B,IAAA,MAAM,wBAA2B,GAAAC,uCAAA;AAAA,MAC/B,cAAA;AAAA,MACA;AAAA,KACF;AACA,IAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,QAAS,CAAA,4BAAA;AAAA,MACrC,SAAA;AAAA,MACA,wBAAyB,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,EAAE;AAAA,KACxC;AAEA,IAAO,OAAA,UAAA,CAAW,IAAI,CAAC,EAAE,WAAW,KAAO,EAAA,aAAA,EAAe,WAAgB,KAAA;AACxE,MAAA,MAAM,QAAW,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,SAAS,CAAA;AACpD,MAAM,MAAA,MAAA,GAAS,SAAS,SAAU,EAAA;AAElC,MAAI,IAAA,UAAA;AACJ,MAAI,IAAA,UAAA;AACJ,MAAI,IAAA,cAAA;AACJ,MAAI,IAAA;AACF,QAAA,UAAA,GAAa,IAAK,CAAA,gCAAA;AAAA,UAChB,MAAA;AAAA,UACA,QAAA;AAAA,UACA,MAAO,CAAA;AAAA,SACT;AACA,QAAA,IAAI,UAAU,KAAW,CAAA,EAAA;AACvB,UACE,cAAA,GAAA,wDAAA;AAAA,SACG,MAAA;AACL,UAAA,UAAA,GAAa,KAAK,kBAAmB,CAAA,yBAAA;AAAA,YACnC,KAAA;AAAA,YACA,MAAO,CAAA,IAAA;AAAA,YACP;AAAA,WACF;AAAA;AACF,eACO,CAAG,EAAA;AACV,QAAA,cAAA,GAAiBC,sBAAe,CAAC,CAAA;AAAA;AAGnC,MAAM,MAAA,iBAAA,GAAoB,iBAAiB,KAAU,KAAA,MAAA;AACrD,MAAO,OAAA;AAAA,QACL,IAAI,MAAO,CAAA,EAAA;AAAA,QACX,MAAA,EAAQ,oBAAoB,OAAU,GAAA,SAAA;AAAA,QACtC,QAAU,EAAA;AAAA,UACR,OAAO,MAAO,CAAA,KAAA;AAAA,UACd,aAAa,MAAO,CAAA,WAAA;AAAA,UACpB,MAAM,MAAO,CAAA,IAAA;AAAA,UACb,SAAS,MAAO,CAAA;AAAA,SAClB;AAAA,QACA,GAAI,iBAAqB,IAAA;AAAA,UACvB,OACE,aACA,IAAAA,qBAAA,CAAe,IAAI,KAAA,CAAM,6BAA6B,CAAC;AAAA,SAC3D;AAAA,QACA,MAAQ,EAAA;AAAA,UACN,KAAA;AAAA,UACA,SAAW,EAAA,IAAI,IAAK,CAAA,SAAS,EAAE,WAAY,EAAA;AAAA,UAC3C,eAAiB,EAAA;AAAA,YACf,UAAY,EAAA,UAAA;AAAA,YACZ,MAAA,EAAQ,iBAAiB,OAAU,GAAA,SAAA;AAAA,YACnC,UAAA;AAAA,YACA,GAAI,cAAA,IAAkB,EAAE,KAAA,EAAO,cAAe;AAAA;AAChD;AACF,OACF;AAAA,KACD,CAAA;AAAA;AACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,6BAAA,CACN,MACA,EAAA,UAAA,EACA,UACiB,EAAA;AACjB,IAAA,MAAM,WAAc,GAAA,MAAA,CAAO,QAAU,EAAA,WAAA,IAAe,EAAC;AACrD,IAAM,MAAA,MAAA,GAAS,gBAAgB,UAAU,CAAA,kBAAA,CAAA;AACzC,IAAA,MAAM,YAA6B,EAAC;AAEpC,IAAA,KAAA,MAAW,CAAC,aAAe,EAAA,UAAU,KAAK,MAAO,CAAA,OAAA,CAAQ,WAAW,CAAG,EAAA;AACrE,MAAA,IAAI,aAAc,CAAA,UAAA,CAAW,MAAM,CAAA,IAAK,UAAY,EAAA;AAClD,QAAA,MAAM,GAAM,GAAA,aAAA,CAAc,SAAU,CAAA,MAAA,CAAO,MAAM,CAAA;AACjD,QAAM,MAAA,UAAA,GAAa,EAAE,GAAA,EAAK,UAAW,EAAA;AACrC,QAAI,IAAA;AACF,UAAAC,+CAAA,CAAmB,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,UAAU,CAAA;AACtD,UAAA,SAAA,CAAU,KAAK,UAAU,CAAA;AAAA,iBAClB,CAAG,EAAA;AACV,UAAI,IAAAC,cAAA,CAAQ,CAAC,CAAG,EAAA;AACd,YAAA,MAAM,IAAIC,uDAAA;AAAA,cACR,CAAiC,8BAAA,EAAA,aAAa,CAAK,EAAA,EAAA,UAAU,CAAgB,aAAA,EAAAC,+BAAA;AAAA,gBAC3E;AAAA,eACD,CAAM,GAAA,EAAA,CAAA,CAAE,OAAO,CAAA;AAAA,aAClB;AAAA;AAEF,UAAM,MAAA,CAAA;AAAA;AACR;AACF;AAGF,IAAO,OAAA,SAAA;AAAA;AACT,EAEQ,gCAAA,CACN,MACA,EAAA,QAAA,EACA,UACiB,EAAA;AACjB,IAAM,MAAA,kBAAA,GAAqB,SAAS,mBAAoB,EAAA;AACxD,IAAA,MAAM,2BAA2B,IAAK,CAAA,6BAAA;AAAA,MACpC,MAAA;AAAA,MACA,SAAS,aAAc,EAAA;AAAA,MACvB;AAAA,KACF;AAEA,IAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,IAAA,KAAA,MAAW,YAAY,wBAA0B,EAAA;AAC/C,MAAA,MAAM,WAAW,WAAY,CAAA,SAAA,CAAU,UAAQ,IAAK,CAAA,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,MAAA,IAAI,aAAa,EAAI,EAAA;AACnB,QAAA,WAAA,CAAY,QAAQ,CAAI,GAAA,QAAA;AAAA,OACnB,MAAA;AACL,QAAA,MAAM,IAAID,uDAAA;AAAA,UACR,CAAsB,mBAAA,EAAAC,+BAAA;AAAA,YACpB;AAAA,WACD,kBAAkB,IAAK,CAAA,SAAA;AAAA,YACtB;AAAA,WACD,CAAqB,kBAAA,EAAA,QAAA,CAAS,eAAe,CAAA,sBAAA,EAC5C,SAAS,GACX,CAAA;AAAA,SACF;AAAA;AACF;AAGF,IAAO,OAAA;AAAA,MACL,KAAO,EAAA;AAAA,KACT;AAAA;AAEJ;;;;"}
@@ -7,6 +7,7 @@ var Router = require('express-promise-router');
7
7
  var pluginPermissionCommon = require('@backstage/plugin-permission-common');
8
8
  var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
9
9
  var permissionUtils = require('../permissions/permissionUtils.cjs.js');
10
+ var catalogModel = require('@backstage/catalog-model');
10
11
 
11
12
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
13
 
@@ -78,10 +79,10 @@ async function createRouter({
78
79
  if (!parsed.success) {
79
80
  throw new errors.InputError(`Invalid query parameters: ${parsed.error.message}`);
80
81
  }
81
- const entityRef = `${kind}:${namespace}/${name}`;
82
+ const entityRef = catalogModel.stringifyEntityRef({ kind, namespace, name });
82
83
  await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
83
84
  const metricIdArray = metricIds ? metricIds.split(",").map((id) => id.trim()) : void 0;
84
- const results = await catalogMetricService.calculateEntityMetrics(
85
+ const results = await catalogMetricService.getLatestEntityMetrics(
85
86
  entityRef,
86
87
  metricIdArray,
87
88
  conditions
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError, NotAllowedError } from '@backstage/errors';\nimport { z } from 'zod';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const metricsSchema = z.object({\n datasource: z.string().min(1).optional(),\n });\n\n const parsed = metricsSchema.safeParse(req.query);\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n const { datasource } = parsed.data;\n\n let metrics;\n if (datasource) {\n metrics = metricProvidersRegistry.listMetricsByDatasource(datasource);\n } else {\n metrics = metricProvidersRegistry.listMetrics();\n }\n\n res.json({\n metrics: filterAuthorizedMetrics(metrics, conditions),\n });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n const { metricIds } = req.query;\n\n const catalogMetricsSchema = z.object({\n metricIds: z.string().min(1).optional(),\n });\n\n const parsed = catalogMetricsSchema.safeParse(req.query);\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n const entityRef = `${kind}:${namespace}/${name}`;\n\n // Check if user has permission to read this specific catalog entity\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const metricIdArray = metricIds\n ? (metricIds as string).split(',').map(id => id.trim())\n : undefined;\n\n const results = await catalogMetricService.calculateEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n return router;\n}\n"],"names":["Router","express","AuthorizeResult","NotAllowedError","scorecardMetricReadPermission","z","InputError","filterAuthorizedMetrics","checkEntityAccess"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError, NotAllowedError } from '@backstage/errors';\nimport { z } from 'zod';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const metricsSchema = z.object({\n datasource: z.string().min(1).optional(),\n });\n\n const parsed = metricsSchema.safeParse(req.query);\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n const { datasource } = parsed.data;\n\n let metrics;\n if (datasource) {\n metrics = metricProvidersRegistry.listMetricsByDatasource(datasource);\n } else {\n metrics = metricProvidersRegistry.listMetrics();\n }\n\n res.json({\n metrics: filterAuthorizedMetrics(metrics, conditions),\n });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n const { metricIds } = req.query;\n\n const catalogMetricsSchema = z.object({\n metricIds: z.string().min(1).optional(),\n });\n\n const parsed = catalogMetricsSchema.safeParse(req.query);\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n const entityRef = stringifyEntityRef({ kind, namespace, name });\n\n // Check if user has permission to read this specific catalog entity\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const metricIdArray = metricIds\n ? (metricIds as string).split(',').map(id => id.trim())\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n return router;\n}\n"],"names":["Router","express","AuthorizeResult","NotAllowedError","scorecardMetricReadPermission","z","InputError","filterAuthorizedMetrics","stringifyEntityRef","checkEntityAccess"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Copyright Red Hat, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ exports.up = async function up(knex) {
18
+ await knex.schema.createTable('metric_values', table => {
19
+ table.comment('Table containing calculated metric values');
20
+ table
21
+ .bigIncrements('id')
22
+ .primary()
23
+ .comment('Auto-incrementing primary key');
24
+ table.string('metric_id').notNullable();
25
+ table
26
+ .string('catalog_entity_ref')
27
+ .notNullable()
28
+ .comment('The entity ref for which this metric value was calculated');
29
+ table
30
+ .json('value')
31
+ .nullable()
32
+ .comment('Metric value when calculation succeeds');
33
+ table
34
+ .dateTime('timestamp', { precision: 0 })
35
+ .defaultTo(knex.fn.now())
36
+ .notNullable()
37
+ .comment('The timestamp when this entry was created');
38
+ table
39
+ .text('error_message')
40
+ .nullable()
41
+ .comment('Error details when calculation fails');
42
+
43
+ table.index(['catalog_entity_ref', 'metric_id', 'id'], 'idx_entity_metric');
44
+ });
45
+ };
46
+
47
+ exports.down = async function down(knex) {
48
+ await knex.schema.dropTable('metric_results');
49
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,10 +47,12 @@
47
47
  "@backstage/plugin-catalog-node": "^1.18.0",
48
48
  "@backstage/plugin-permission-common": "^0.9.1",
49
49
  "@backstage/plugin-permission-node": "^0.10.3",
50
- "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^1.0.0",
51
- "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^1.0.0",
50
+ "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.0.0",
51
+ "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.0.0",
52
52
  "express": "^4.17.1",
53
53
  "express-promise-router": "^4.1.0",
54
+ "knex": "^3.1.0",
55
+ "uuid": "^9.0.1",
54
56
  "zod": "^3.22.4"
55
57
  },
56
58
  "devDependencies": {
@@ -61,7 +63,8 @@
61
63
  "supertest": "^6.2.4"
62
64
  },
63
65
  "files": [
64
- "dist"
66
+ "dist",
67
+ "migrations"
65
68
  ],
66
69
  "typesVersions": {
67
70
  "*": {