@red-hat-developer-hub/backstage-plugin-scorecard-backend 4.4.2 → 4.5.1
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 +25 -0
- package/dist/plugin.cjs.js +0 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +217 -69
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js +1 -1
- package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +21 -14
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +3 -3
- package/dist/utils/getEntitiesOwnedByUser.cjs.js +0 -49
- package/dist/utils/getEntitiesOwnedByUser.cjs.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 4.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7bf7e10: Fetch successive database windows when building drill-down results so catalog authorization and disabled-metric filtering can reach the eligible-result cap instead of stopping after the first 10,000 database rows.
|
|
8
|
+
- 7bf7e10: Exclude metrics disabled via `scorecard.disabledMetrics` or the `scorecard.io/disabled-metrics` entity annotation from homepage aggregations, aggregation time series, drill-down tables, and entity metric time series, so stale stored values are no longer shown after a metric is disabled.
|
|
9
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@4.5.1
|
|
10
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@4.5.1
|
|
11
|
+
|
|
12
|
+
## 4.5.0
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- b8d8662: Correct weighted status score aggregation responses to return the thresholds used to classify
|
|
17
|
+
the aggregate score. Drill-down `/metrics/:metricId/catalog/aggregations/entities` responses
|
|
18
|
+
now include the underlying metric thresholds used to classify entity statuses. The entity rows
|
|
19
|
+
are now consistent with statuses shown on entity scorecard page, while the aggregate card uses
|
|
20
|
+
aggregation thresholds for its own scope. Entity-specific threshold annotation overrides can
|
|
21
|
+
still affect an individual entity's status, but are not represented by the drill-down response's
|
|
22
|
+
shared threshold set.
|
|
23
|
+
- Updated dependencies [dfcbf0d]
|
|
24
|
+
- Updated dependencies [b8d8662]
|
|
25
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@4.5.0
|
|
26
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@4.5.0
|
|
27
|
+
|
|
3
28
|
## 4.4.2
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/dist/plugin.cjs.js
CHANGED
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -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 { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createScorecardActions } from './actions';\nimport {\n type Collector,\n type MetricProvider,\n scorecardCollectorsExtensionPoint,\n scorecardCollectorsServiceRef,\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';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\nimport { AggregationsService } from './service/aggregations/AggregationsService';\nimport { ThresholdResolver } from './threshold/ThresholdResolver';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const collectors: Collector[] = [];\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardCollectorsExtensionPoint, {\n addCollector(...newCollectors: Collector[]) {\n collectors.push(...newCollectors);\n },\n });\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 actionsRegistry: actionsRegistryServiceRef,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n collectorsService: scorecardCollectorsServiceRef,\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 actionsRegistry,\n auth,\n catalog,\n collectorsService,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n collectorsService.init({ collectors });\n\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\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 const thresholdResolver = new ThresholdResolver(\n config,\n metricProvidersRegistry.listProviders(),\n );\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n thresholdResolver,\n config,\n });\n\n const aggregationsService = new AggregationsService({\n config,\n database: dbMetricValues,\n logger: logger,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n thresholdResolver,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n thresholdResolver,\n }).start();\n\n const service = {\n aggregationsService: aggregationsService,\n catalogMetricService: catalogMetricService,\n };\n\n createScorecardActions({\n actionsRegistry,\n auth,\n permissions,\n catalog,\n metricProvidersRegistry,\n catalogMetricService,\n });\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n service,\n
|
|
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 { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createScorecardActions } from './actions';\nimport {\n type Collector,\n type MetricProvider,\n scorecardCollectorsExtensionPoint,\n scorecardCollectorsServiceRef,\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';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\nimport { AggregationsService } from './service/aggregations/AggregationsService';\nimport { ThresholdResolver } from './threshold/ThresholdResolver';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const collectors: Collector[] = [];\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardCollectorsExtensionPoint, {\n addCollector(...newCollectors: Collector[]) {\n collectors.push(...newCollectors);\n },\n });\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 actionsRegistry: actionsRegistryServiceRef,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n collectorsService: scorecardCollectorsServiceRef,\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 actionsRegistry,\n auth,\n catalog,\n collectorsService,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n collectorsService.init({ collectors });\n\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\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 const thresholdResolver = new ThresholdResolver(\n config,\n metricProvidersRegistry.listProviders(),\n );\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n thresholdResolver,\n config,\n });\n\n const aggregationsService = new AggregationsService({\n config,\n database: dbMetricValues,\n logger: logger,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n thresholdResolver,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n thresholdResolver,\n }).start();\n\n const service = {\n aggregationsService: aggregationsService,\n catalogMetricService: catalogMetricService,\n };\n\n createScorecardActions({\n actionsRegistry,\n auth,\n permissions,\n catalog,\n metricProvidersRegistry,\n catalogMetricService,\n });\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n service,\n httpAuth,\n permissions,\n logger,\n thresholdResolver,\n collectorsService,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardCollectorsExtensionPoint","scorecardMetricsExtensionPoint","actionsRegistryServiceRef","coreServices","catalogServiceRef","scorecardCollectorsServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","migrate","DatabaseMetricValues","ThresholdResolver","CatalogMetricService","AggregationsService","validateAggregationConfig","Scheduler","ThresholdEvaluator","createScorecardActions","createRouter"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
|
|
4
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
5
|
var normalizeOwnerRef = require('../utils/normalizeOwnerRef.cjs.js');
|
|
5
6
|
var errors = require('@backstage/errors');
|
|
6
7
|
var permissionUtils = require('../permissions/permissionUtils.cjs.js');
|
|
@@ -27,8 +28,10 @@ class CatalogMetricService {
|
|
|
27
28
|
registry;
|
|
28
29
|
database;
|
|
29
30
|
thresholdResolver;
|
|
31
|
+
globalDisabledMetrics;
|
|
30
32
|
thresholdEvaluator = new ThresholdEvaluator.ThresholdEvaluator();
|
|
31
33
|
static MAX_FETCHABLE_ROWS = 1e4;
|
|
34
|
+
static MAX_SCANNED_DB_ROWS = 3e4;
|
|
32
35
|
static BATCH_SIZE = 100;
|
|
33
36
|
constructor(options) {
|
|
34
37
|
this.catalog = options.catalog;
|
|
@@ -37,6 +40,7 @@ class CatalogMetricService {
|
|
|
37
40
|
this.database = options.database;
|
|
38
41
|
this.logger = options.logger;
|
|
39
42
|
this.thresholdResolver = options.thresholdResolver;
|
|
43
|
+
this.globalDisabledMetrics = options.config.getOptionalStringArray("scorecard.disabledMetrics") ?? [];
|
|
40
44
|
this.config = options.config;
|
|
41
45
|
}
|
|
42
46
|
/**
|
|
@@ -146,12 +150,6 @@ class CatalogMetricService {
|
|
|
146
150
|
`To view the scorecard metrics, your administrator must grant you the required permission.`
|
|
147
151
|
);
|
|
148
152
|
}
|
|
149
|
-
const rows = await this.database.readLatestEntityMetricValuesPerUtcDay(
|
|
150
|
-
entityRef,
|
|
151
|
-
metricId,
|
|
152
|
-
from,
|
|
153
|
-
to
|
|
154
|
-
);
|
|
155
153
|
let thresholds;
|
|
156
154
|
let thresholdsError;
|
|
157
155
|
try {
|
|
@@ -165,6 +163,30 @@ class CatalogMetricService {
|
|
|
165
163
|
`Failed to resolve thresholds for metric '${metric.id}' on entity '${entityRef}': ${thresholdsError}`
|
|
166
164
|
);
|
|
167
165
|
}
|
|
166
|
+
if (metricUtils.isMetricIdDisabled(this.config, metricId, entity, this.logger)) {
|
|
167
|
+
return {
|
|
168
|
+
metricId: metric.id,
|
|
169
|
+
entityRef,
|
|
170
|
+
points: [],
|
|
171
|
+
metadata: {
|
|
172
|
+
title: metric.title,
|
|
173
|
+
description: metric.description,
|
|
174
|
+
type: metric.type,
|
|
175
|
+
unit: metric.unit,
|
|
176
|
+
history: metric.history,
|
|
177
|
+
defaultVisualization: metric.defaultVisualization,
|
|
178
|
+
collectorIds: metric.collectorIds
|
|
179
|
+
},
|
|
180
|
+
...thresholds ? { thresholds } : {},
|
|
181
|
+
...thresholdsError ? { thresholdsError } : {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const rows = await this.database.readLatestEntityMetricValuesPerUtcDay(
|
|
185
|
+
entityRef,
|
|
186
|
+
metricId,
|
|
187
|
+
from,
|
|
188
|
+
to
|
|
189
|
+
);
|
|
168
190
|
const points = rows.map((row) => {
|
|
169
191
|
if (metricCalculationError.isMetricCalculationError(row)) {
|
|
170
192
|
return {
|
|
@@ -267,85 +289,127 @@ class CatalogMetricService {
|
|
|
267
289
|
*/
|
|
268
290
|
async getEntityMetricDetails(metricId, credentials, options) {
|
|
269
291
|
const metric = this.registry.getMetric(metricId);
|
|
292
|
+
const thresholds = this.thresholdResolver.resolveMetricThresholds(metric);
|
|
293
|
+
const emptyResponse = () => ({
|
|
294
|
+
metricId: metric.id,
|
|
295
|
+
thresholds,
|
|
296
|
+
metricMetadata: {
|
|
297
|
+
title: metric.title,
|
|
298
|
+
description: metric.description,
|
|
299
|
+
type: metric.type,
|
|
300
|
+
unit: metric.unit
|
|
301
|
+
},
|
|
302
|
+
entities: [],
|
|
303
|
+
pagination: {
|
|
304
|
+
page: options.page,
|
|
305
|
+
pageSize: options.limit,
|
|
306
|
+
total: 0,
|
|
307
|
+
totalPages: 0,
|
|
308
|
+
isCapped: false
|
|
309
|
+
},
|
|
310
|
+
entityHealth: CatalogMetricService.entityHealthSummary([], false)
|
|
311
|
+
});
|
|
312
|
+
if (this.globalDisabledMetrics.includes(metricId)) {
|
|
313
|
+
this.logger.debug(`Disabled metric by app-config: ${metricId}`);
|
|
314
|
+
return emptyResponse();
|
|
315
|
+
}
|
|
270
316
|
if ((options.page - 1) * options.limit >= CatalogMetricService.MAX_FETCHABLE_ROWS) {
|
|
271
|
-
return
|
|
272
|
-
metricId: metric.id,
|
|
273
|
-
metricMetadata: {
|
|
274
|
-
title: metric.title,
|
|
275
|
-
description: metric.description,
|
|
276
|
-
type: metric.type,
|
|
277
|
-
unit: metric.unit
|
|
278
|
-
},
|
|
279
|
-
entities: [],
|
|
280
|
-
pagination: {
|
|
281
|
-
page: options.page,
|
|
282
|
-
pageSize: options.limit,
|
|
283
|
-
total: 0,
|
|
284
|
-
totalPages: 0,
|
|
285
|
-
isCapped: false
|
|
286
|
-
},
|
|
287
|
-
entityHealth: CatalogMetricService.entityHealthSummary([], false)
|
|
288
|
-
};
|
|
317
|
+
return emptyResponse();
|
|
289
318
|
}
|
|
290
|
-
const
|
|
319
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
320
|
+
const accessibleRows = [];
|
|
321
|
+
let dbOffset = 0;
|
|
322
|
+
let scannedDbRows = 0;
|
|
323
|
+
let moreDbRowsMayExist = false;
|
|
324
|
+
let stoppedAtScanCap = false;
|
|
325
|
+
const dbFilterOptions = {
|
|
291
326
|
status: options.status,
|
|
292
327
|
entityName: options.entityName,
|
|
293
328
|
entityKind: options.kind,
|
|
294
329
|
entityNamespace: options.namespace,
|
|
295
330
|
entityOwner: options.owner,
|
|
296
331
|
sortBy: options.sortBy,
|
|
297
|
-
sortOrder: options.sortOrder
|
|
298
|
-
|
|
299
|
-
limit: CatalogMetricService.MAX_FETCHABLE_ROWS,
|
|
300
|
-
offset: 0
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
const entityMap = /* @__PURE__ */ new Map();
|
|
304
|
-
const accessibleRows = [];
|
|
332
|
+
sortOrder: options.sortOrder
|
|
333
|
+
};
|
|
305
334
|
try {
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
335
|
+
while (accessibleRows.length < CatalogMetricService.MAX_FETCHABLE_ROWS) {
|
|
336
|
+
const rows = await this.database.readEntityMetricsWithFilters(
|
|
337
|
+
metricId,
|
|
309
338
|
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
]
|
|
317
|
-
},
|
|
318
|
-
{ credentials }
|
|
339
|
+
...dbFilterOptions,
|
|
340
|
+
pagination: {
|
|
341
|
+
limit: CatalogMetricService.MAX_FETCHABLE_ROWS,
|
|
342
|
+
offset: dbOffset
|
|
343
|
+
}
|
|
344
|
+
}
|
|
319
345
|
);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
346
|
+
if (rows.length === 0) {
|
|
347
|
+
moreDbRowsMayExist = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
scannedDbRows += rows.length;
|
|
351
|
+
let stoppedAtEligibleCap = false;
|
|
352
|
+
for (let i = 0; i < rows.length && !stoppedAtEligibleCap; i += CatalogMetricService.BATCH_SIZE) {
|
|
353
|
+
const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);
|
|
354
|
+
const response = await this.catalog.getEntitiesByRefs(
|
|
355
|
+
{
|
|
356
|
+
entityRefs: batch.map((row) => row.catalogEntityRef),
|
|
357
|
+
fields: [
|
|
358
|
+
"kind",
|
|
359
|
+
"metadata.name",
|
|
360
|
+
"metadata.namespace",
|
|
361
|
+
"metadata.annotations",
|
|
362
|
+
"spec.owner"
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
{ credentials }
|
|
366
|
+
);
|
|
367
|
+
for (let j = 0; j < batch.length; j++) {
|
|
368
|
+
const entity = response.items[j];
|
|
369
|
+
if (!entity) continue;
|
|
370
|
+
if (metricUtils.isMetricIdDisabled(this.config, metricId, entity, this.logger)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (accessibleRows.length >= CatalogMetricService.MAX_FETCHABLE_ROWS) {
|
|
374
|
+
stoppedAtEligibleCap = true;
|
|
375
|
+
moreDbRowsMayExist = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
entityMap.set(batch[j].catalogEntityRef, entity);
|
|
379
|
+
accessibleRows.push(batch[j]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (stoppedAtEligibleCap) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (rows.length < CatalogMetricService.MAX_FETCHABLE_ROWS) {
|
|
386
|
+
moreDbRowsMayExist = false;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
dbOffset += rows.length;
|
|
390
|
+
moreDbRowsMayExist = true;
|
|
391
|
+
if (accessibleRows.length >= CatalogMetricService.MAX_FETCHABLE_ROWS) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
if (scannedDbRows >= CatalogMetricService.MAX_SCANNED_DB_ROWS) {
|
|
395
|
+
stoppedAtScanCap = true;
|
|
396
|
+
this.logger.warn(
|
|
397
|
+
"Drill-down DB scan budget reached; results may be partial",
|
|
398
|
+
{
|
|
399
|
+
metricId,
|
|
400
|
+
scannedDbRows,
|
|
401
|
+
eligibleCount: accessibleRows.length,
|
|
402
|
+
dbOffset
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
break;
|
|
325
406
|
}
|
|
326
407
|
}
|
|
327
408
|
} catch (error) {
|
|
328
409
|
this.logger.error("Failed to fetch entities from catalog", { error });
|
|
329
|
-
return
|
|
330
|
-
metricId: metric.id,
|
|
331
|
-
metricMetadata: {
|
|
332
|
-
title: metric.title,
|
|
333
|
-
description: metric.description,
|
|
334
|
-
type: metric.type,
|
|
335
|
-
unit: metric.unit
|
|
336
|
-
},
|
|
337
|
-
entities: [],
|
|
338
|
-
pagination: {
|
|
339
|
-
page: options.page,
|
|
340
|
-
pageSize: options.limit,
|
|
341
|
-
total: 0,
|
|
342
|
-
totalPages: 0,
|
|
343
|
-
isCapped: false
|
|
344
|
-
},
|
|
345
|
-
entityHealth: CatalogMetricService.entityHealthSummary([], false)
|
|
346
|
-
};
|
|
410
|
+
return emptyResponse();
|
|
347
411
|
}
|
|
348
|
-
const isCapped =
|
|
412
|
+
const isCapped = moreDbRowsMayExist && (accessibleRows.length === CatalogMetricService.MAX_FETCHABLE_ROWS || stoppedAtScanCap);
|
|
349
413
|
const totalFiltered = accessibleRows.length;
|
|
350
414
|
const pageRows = accessibleRows.slice(
|
|
351
415
|
(options.page - 1) * options.limit,
|
|
@@ -354,6 +418,7 @@ class CatalogMetricService {
|
|
|
354
418
|
if (pageRows.length === 0) {
|
|
355
419
|
return {
|
|
356
420
|
metricId: metric.id,
|
|
421
|
+
thresholds,
|
|
357
422
|
metricMetadata: {
|
|
358
423
|
title: metric.title,
|
|
359
424
|
description: metric.description,
|
|
@@ -391,6 +456,7 @@ class CatalogMetricService {
|
|
|
391
456
|
}
|
|
392
457
|
return {
|
|
393
458
|
metricId: metric.id,
|
|
459
|
+
thresholds,
|
|
394
460
|
metricMetadata: {
|
|
395
461
|
title: metric.title,
|
|
396
462
|
description: metric.description,
|
|
@@ -411,6 +477,88 @@ class CatalogMetricService {
|
|
|
411
477
|
)
|
|
412
478
|
};
|
|
413
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Get the user and its members entities.
|
|
482
|
+
*
|
|
483
|
+
* @param userEntityRef - User entity reference in format "kind:namespace/name"
|
|
484
|
+
* @param metricId - Metric ID to filter entities by
|
|
485
|
+
* @param options - Options for the query
|
|
486
|
+
* @param options.credentials - Backstage credentials
|
|
487
|
+
* @returns Array of entity references in format "kind:namespace/name"
|
|
488
|
+
*/
|
|
489
|
+
async getUserAndGroupOwnerRefs(userEntityRef, metricId, options) {
|
|
490
|
+
const { credentials } = options;
|
|
491
|
+
const userEntity = await this.catalog.getEntityByRef(userEntityRef, {
|
|
492
|
+
credentials
|
|
493
|
+
});
|
|
494
|
+
if (!userEntity) {
|
|
495
|
+
throw new errors.NotFoundError("User entity not found in catalog");
|
|
496
|
+
}
|
|
497
|
+
if (this.globalDisabledMetrics.includes(metricId)) {
|
|
498
|
+
this.logger.debug(`Disabled metric by app-config: ${metricId}`);
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
const ownerRefs = [userEntityRef];
|
|
502
|
+
const memberOfRelations = userEntity.relations?.filter(
|
|
503
|
+
(relation) => relation.type === catalogModel.RELATION_MEMBER_OF
|
|
504
|
+
) ?? [];
|
|
505
|
+
if (memberOfRelations.length > 0) {
|
|
506
|
+
for (const relation of memberOfRelations) {
|
|
507
|
+
ownerRefs.push(relation.targetRef);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return ownerRefs;
|
|
511
|
+
}
|
|
512
|
+
async getEntitiesOwnedByOwnerRef(ownerRef, metricId, options) {
|
|
513
|
+
const QUERY_ENTITIES_BATCH_SIZE = 50;
|
|
514
|
+
let cursor = void 0;
|
|
515
|
+
const { credentials } = options;
|
|
516
|
+
const entitiesOwnedByUserAndGroups = [];
|
|
517
|
+
do {
|
|
518
|
+
const entities = await this.catalog.queryEntities(
|
|
519
|
+
{
|
|
520
|
+
filter: {
|
|
521
|
+
[`relations.${catalogModel.RELATION_OWNED_BY}`]: ownerRef
|
|
522
|
+
},
|
|
523
|
+
fields: ["kind", "metadata"],
|
|
524
|
+
limit: QUERY_ENTITIES_BATCH_SIZE,
|
|
525
|
+
...cursor ? { cursor } : {}
|
|
526
|
+
},
|
|
527
|
+
{ credentials }
|
|
528
|
+
);
|
|
529
|
+
cursor = entities.pageInfo.nextCursor;
|
|
530
|
+
for (const entity of entities.items) {
|
|
531
|
+
if (metricUtils.isMetricIdDisabled(this.config, metricId, entity, this.logger)) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
entitiesOwnedByUserAndGroups.push(catalogModel.stringifyEntityRef(entity));
|
|
535
|
+
}
|
|
536
|
+
} while (cursor !== void 0);
|
|
537
|
+
return entitiesOwnedByUserAndGroups;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get the entities owned by a user and their groups.
|
|
541
|
+
*
|
|
542
|
+
* @param userEntityRef - User entity reference in format "kind:namespace/name"
|
|
543
|
+
* @param metricId - Metric ID to filter entities by
|
|
544
|
+
* @param options - Options for the query
|
|
545
|
+
* @param options.credentials - Backstage credentials
|
|
546
|
+
* @returns Array of entity references in format "kind:namespace/name"
|
|
547
|
+
*/
|
|
548
|
+
async getEntitiesOwnedByUser(userEntityRef, metricId, options) {
|
|
549
|
+
const { credentials } = options;
|
|
550
|
+
const ownerRefs = await this.getUserAndGroupOwnerRefs(
|
|
551
|
+
userEntityRef,
|
|
552
|
+
metricId,
|
|
553
|
+
{ credentials }
|
|
554
|
+
);
|
|
555
|
+
const entitiesOwnedByOwnerRefs = await Promise.all(
|
|
556
|
+
ownerRefs.map(
|
|
557
|
+
(ownerRef) => this.getEntitiesOwnedByOwnerRef(ownerRef, metricId, { credentials })
|
|
558
|
+
)
|
|
559
|
+
);
|
|
560
|
+
return entitiesOwnedByOwnerRefs.flat();
|
|
561
|
+
}
|
|
414
562
|
}
|
|
415
563
|
|
|
416
564
|
exports.CatalogMetricService = CatalogMetricService;
|
|
@@ -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 {\n MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n ScorecardEntityHealthSummary,\n aggregationTypes,\n AggregatedMetric,\n MetricTimeSeriesResponse,\n MetricTimeSeriesPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n NotAllowedError,\n NotFoundError,\n stringifyError,\n} from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\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';\nimport { isMetricCalculationError } from '../utils/metricCalculationError';\nimport { isMetricIdDisabled } from '../utils/metricUtils';\nimport { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\nimport { ThresholdEvaluator } from '../threshold/ThresholdEvaluator';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n config: Config;\n};\n\nexport class CatalogMetricService {\n private static entityHealthSummary(\n accessibleRows: DbMetricValue[],\n countsArePartial: boolean,\n ): ScorecardEntityHealthSummary {\n const calculationErrorCount = accessibleRows.filter(row =>\n isMetricCalculationError(row),\n ).length;\n return {\n totalEntities: accessibleRows.length,\n calculationErrorCount,\n countsArePartial,\n };\n }\n\n private readonly logger: LoggerService;\n private readonly config: Config;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdResolver: ThresholdResolver;\n private readonly thresholdEvaluator = new ThresholdEvaluator();\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n this.thresholdResolver = options.thresholdResolver;\n this.config = options.config;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric 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 metricIds?: 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 = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const metricIdsToFetch = authorizedMetricsToFetch\n .filter(m => !isMetricIdDisabled(this.config, m.id, entity, this.logger))\n .map(m => m.id);\n\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n metricIdsToFetch,\n );\n\n return rawResults.map(\n ({ metricId, value, errorMessage, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const metric = this.registry.getMetric(metricId);\n\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (errorMessage) {\n thresholdError = errorMessage;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = isMetricCalculationError({\n value,\n errorMessage,\n });\n\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 unit: metric.unit,\n history: metric.history,\n defaultVisualization: metric.defaultVisualization,\n collectorIds: metric.collectorIds,\n },\n ...(isMetricCalcError && {\n error:\n errorMessage ??\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: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get a daily time series for one metric on one catalog entity.\n *\n * Returns at most one point per UTC calendar day: the latest sample\n * (`MAX(id)`), whether success or calculation error. Calculation failures\n * use `value: null` and `error`. Threshold evaluation failures also set\n * `error` (with `thresholdEvaluation` null). When entity threshold\n * resolution fails, `thresholdsError` is set on the response and points are\n * not classified (`thresholdEvaluation` null, no per-point `error`).\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricId - Metric ID to fetch\n * @param from - Inclusive range start\n * @param to - Inclusive range end\n * @param filter - Permission filter\n */\n async getEntityMetricTimeSeries(\n entityRef: string,\n metricId: string,\n from: Date,\n to: Date,\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricTimeSeriesResponse> {\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 metric = this.registry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], filter);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const rows = await this.database.readLatestEntityMetricValuesPerUtcDay(\n entityRef,\n metricId,\n from,\n to,\n );\n\n let thresholds: ThresholdConfig | undefined;\n let thresholdsError: string | undefined;\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n } catch (err) {\n thresholdsError = stringifyError(err);\n this.logger.warn(\n `Failed to resolve thresholds for metric '${metric.id}' on entity '${entityRef}': ${thresholdsError}`,\n );\n }\n\n const points: MetricTimeSeriesPoint[] = rows.map(row => {\n if (isMetricCalculationError(row)) {\n return {\n value: null,\n timestamp: row.timestamp.toISOString(),\n error: row.errorMessage!,\n };\n }\n\n let thresholdEvaluation: string | null = null;\n let error: string | undefined;\n if (row.value !== null && thresholds) {\n try {\n thresholdEvaluation =\n this.thresholdEvaluator.getFirstMatchingThreshold(\n row.value,\n metric.type,\n thresholds,\n ) ?? null;\n } catch (err) {\n error = stringifyError(err);\n this.logger.warn(\n `Failed to evaluate thresholds for metric '${metric.id}' on entity '${entityRef}': ${error}`,\n );\n }\n }\n\n return {\n value: row.value,\n timestamp: row.timestamp.toISOString(),\n thresholdEvaluation,\n ...(error ? { error } : {}),\n };\n });\n\n return {\n metricId: metric.id,\n entityRef,\n points,\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n history: metric.history,\n defaultVisualization: metric.defaultVisualization,\n collectorIds: metric.collectorIds,\n },\n ...(thresholds ? { thresholds } : {}),\n ...(thresholdsError ? { thresholdsError } : {}),\n };\n }\n\n /**\n * Get an aggregated metric by status grouped for multiple entities and a single metric ID.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @returns Aggregated metric by status grouped results\n */\n async getStatusGroupedAggregatedMetrics(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n /**\n * Get an aggregated metric by aggregation type.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @param aggregationType - Aggregation type to use.\n * @returns Aggregated metric by aggregation type results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n aggregationType: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n if (aggregationType === aggregationTypes.statusGrouped) {\n return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);\n }\n throw new Error(`Unsupported aggregation type: ${aggregationType}`);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.openPRs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // Query database with all DB filters first\n // At the moment, this is going to be an O(MAX_FETCHABLE_ROWS) cost and is intentional to avoid leaking\n // pre-auth counts. MAX_FETCHABLE_ROWS and BATCH_SIZE are to be used as a method to of adjustment to\n // find the right amount of performance\n const rows = await this.database.readEntityMetricsWithFilters(metricId, {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: 0,\n },\n });\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with user\n // credentials. The catalog enforces auth natively: null = unauthorized or deleted.\n // We also cache the returned Entity objects so we can enrich the page rows without\n // a second catalog round-trip. Sequential processing preserves DB sort order.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n try {\n for (let i = 0; i < rows.length; i += CatalogMetricService.BATCH_SIZE) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalogEntityRef),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n // Filter out the unauthorized entities\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n entityMap.set(batch[j].catalogEntityRef, entity);\n accessibleRows.push(batch[j]);\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // True when DB results were capped; pagination.total may undercount the full dataset.\n const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalogEntityRef);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalogEntityRef,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n}\n"],"names":["isMetricCalculationError","ThresholdEvaluator","NotFoundError","filterAuthorizedMetrics","isMetricIdDisabled","stringifyError","NotAllowedError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef"],"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 {\n MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n ScorecardEntityHealthSummary,\n aggregationTypes,\n AggregatedMetric,\n MetricTimeSeriesResponse,\n MetricTimeSeriesPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n RELATION_MEMBER_OF,\n RELATION_OWNED_BY,\n stringifyEntityRef,\n type Entity,\n} from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n NotAllowedError,\n NotFoundError,\n stringifyError,\n} from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\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';\nimport { isMetricCalculationError } from '../utils/metricCalculationError';\nimport { isMetricIdDisabled } from '../utils/metricUtils';\nimport { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\nimport { ThresholdEvaluator } from '../threshold/ThresholdEvaluator';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n config: Config;\n};\n\nexport class CatalogMetricService {\n private static entityHealthSummary(\n accessibleRows: DbMetricValue[],\n countsArePartial: boolean,\n ): ScorecardEntityHealthSummary {\n const calculationErrorCount = accessibleRows.filter(row =>\n isMetricCalculationError(row),\n ).length;\n return {\n totalEntities: accessibleRows.length,\n calculationErrorCount,\n countsArePartial,\n };\n }\n\n private readonly logger: LoggerService;\n private readonly config: Config;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdResolver: ThresholdResolver;\n private readonly globalDisabledMetrics: string[];\n private readonly thresholdEvaluator = new ThresholdEvaluator();\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly MAX_SCANNED_DB_ROWS = 30_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n this.thresholdResolver = options.thresholdResolver;\n this.globalDisabledMetrics =\n options.config.getOptionalStringArray('scorecard.disabledMetrics') ?? [];\n this.config = options.config;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric 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 metricIds?: 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 = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const metricIdsToFetch = authorizedMetricsToFetch\n .filter(m => !isMetricIdDisabled(this.config, m.id, entity, this.logger))\n .map(m => m.id);\n\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n metricIdsToFetch,\n );\n\n return rawResults.map(\n ({ metricId, value, errorMessage, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const metric = this.registry.getMetric(metricId);\n\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (errorMessage) {\n thresholdError = errorMessage;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = isMetricCalculationError({\n value,\n errorMessage,\n });\n\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 unit: metric.unit,\n history: metric.history,\n defaultVisualization: metric.defaultVisualization,\n collectorIds: metric.collectorIds,\n },\n ...(isMetricCalcError && {\n error:\n errorMessage ??\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: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get a daily time series for one metric on one catalog entity.\n *\n * Returns at most one point per UTC calendar day: the latest sample\n * (`MAX(id)`), whether success or calculation error. Calculation failures\n * use `value: null` and `error`. Threshold evaluation failures also set\n * `error` (with `thresholdEvaluation` null). When entity threshold\n * resolution fails, `thresholdsError` is set on the response and points are\n * not classified (`thresholdEvaluation` null, no per-point `error`).\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricId - Metric ID to fetch\n * @param from - Inclusive range start\n * @param to - Inclusive range end\n * @param filter - Permission filter\n */\n async getEntityMetricTimeSeries(\n entityRef: string,\n metricId: string,\n from: Date,\n to: Date,\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricTimeSeriesResponse> {\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 metric = this.registry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], filter);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n let thresholds: ThresholdConfig | undefined;\n let thresholdsError: string | undefined;\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n } catch (err) {\n thresholdsError = stringifyError(err);\n this.logger.warn(\n `Failed to resolve thresholds for metric '${metric.id}' on entity '${entityRef}': ${thresholdsError}`,\n );\n }\n\n if (isMetricIdDisabled(this.config, metricId, entity, this.logger)) {\n return {\n metricId: metric.id,\n entityRef,\n points: [],\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n history: metric.history,\n defaultVisualization: metric.defaultVisualization,\n collectorIds: metric.collectorIds,\n },\n ...(thresholds ? { thresholds } : {}),\n ...(thresholdsError ? { thresholdsError } : {}),\n };\n }\n\n const rows = await this.database.readLatestEntityMetricValuesPerUtcDay(\n entityRef,\n metricId,\n from,\n to,\n );\n\n const points: MetricTimeSeriesPoint[] = rows.map(row => {\n if (isMetricCalculationError(row)) {\n return {\n value: null,\n timestamp: row.timestamp.toISOString(),\n error: row.errorMessage!,\n };\n }\n\n let thresholdEvaluation: string | null = null;\n let error: string | undefined;\n if (row.value !== null && thresholds) {\n try {\n thresholdEvaluation =\n this.thresholdEvaluator.getFirstMatchingThreshold(\n row.value,\n metric.type,\n thresholds,\n ) ?? null;\n } catch (err) {\n error = stringifyError(err);\n this.logger.warn(\n `Failed to evaluate thresholds for metric '${metric.id}' on entity '${entityRef}': ${error}`,\n );\n }\n }\n\n return {\n value: row.value,\n timestamp: row.timestamp.toISOString(),\n thresholdEvaluation,\n ...(error ? { error } : {}),\n };\n });\n\n return {\n metricId: metric.id,\n entityRef,\n points,\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n history: metric.history,\n defaultVisualization: metric.defaultVisualization,\n collectorIds: metric.collectorIds,\n },\n ...(thresholds ? { thresholds } : {}),\n ...(thresholdsError ? { thresholdsError } : {}),\n };\n }\n\n /**\n * Get an aggregated metric by status grouped for multiple entities and a single metric ID.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @returns Aggregated metric by status grouped results\n */\n async getStatusGroupedAggregatedMetrics(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n /**\n * Get an aggregated metric by aggregation type.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @param aggregationType - Aggregation type to use.\n * @returns Aggregated metric by aggregation type results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n aggregationType: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n if (aggregationType === aggregationTypes.statusGrouped) {\n return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);\n }\n throw new Error(`Unsupported aggregation type: ${aggregationType}`);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.openPRs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n const thresholds = this.thresholdResolver.resolveMetricThresholds(metric);\n\n const emptyResponse = (): EntityMetricDetailResponse => ({\n metricId: metric.id,\n thresholds,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n });\n\n if (this.globalDisabledMetrics.includes(metricId)) {\n this.logger.debug(`Disabled metric by app-config: ${metricId}`);\n return emptyResponse();\n }\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return emptyResponse();\n }\n\n // Fetch successive DB windows in sort order, then apply catalog authorization and\n // disabled-metric filtering until we have MAX_FETCHABLE_ROWS eligible rows, the\n // scan budget (MAX_SCANNED_DB_ROWS) is exhausted, or the database is exhausted.\n // Cost is intentional to avoid leaking pre-auth counts; MAX_FETCHABLE_ROWS,\n // MAX_SCANNED_DB_ROWS, and BATCH_SIZE tune the performance tradeoff.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n let dbOffset = 0;\n let scannedDbRows = 0;\n let moreDbRowsMayExist = false;\n let stoppedAtScanCap = false;\n\n const dbFilterOptions = {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n };\n\n try {\n while (accessibleRows.length < CatalogMetricService.MAX_FETCHABLE_ROWS) {\n const rows = await this.database.readEntityMetricsWithFilters(\n metricId,\n {\n ...dbFilterOptions,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: dbOffset,\n },\n },\n );\n\n if (rows.length === 0) {\n moreDbRowsMayExist = false;\n break;\n }\n\n scannedDbRows += rows.length;\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with\n // user credentials. The catalog enforces auth natively: null = unauthorized or\n // deleted. Cache Entity objects so we can enrich the page without a second\n // catalog round-trip. Sequential processing preserves DB sort order.\n let stoppedAtEligibleCap = false;\n for (\n let i = 0;\n i < rows.length && !stoppedAtEligibleCap;\n i += CatalogMetricService.BATCH_SIZE\n ) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalogEntityRef),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'metadata.annotations',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n if (\n isMetricIdDisabled(this.config, metricId, entity, this.logger)\n ) {\n continue;\n }\n if (\n accessibleRows.length >= CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n // Eligible cap reached with remaining rows in this window (or later\n // windows) that have not been fully evaluated.\n stoppedAtEligibleCap = true;\n moreDbRowsMayExist = true;\n break;\n }\n entityMap.set(batch[j].catalogEntityRef, entity);\n accessibleRows.push(batch[j]);\n }\n }\n\n if (stoppedAtEligibleCap) {\n break;\n }\n\n if (rows.length < CatalogMetricService.MAX_FETCHABLE_ROWS) {\n // Database exhausted for the current filters/sort.\n moreDbRowsMayExist = false;\n break;\n }\n\n // Full DB window consumed; more rows may exist beyond this offset.\n dbOffset += rows.length;\n moreDbRowsMayExist = true;\n\n if (accessibleRows.length >= CatalogMetricService.MAX_FETCHABLE_ROWS) {\n break;\n }\n\n // Scan budget reached: stop before fetching another window so one request\n // cannot walk an unbounded number of DB rows / catalog auth calls.\n if (scannedDbRows >= CatalogMetricService.MAX_SCANNED_DB_ROWS) {\n stoppedAtScanCap = true;\n this.logger.warn(\n 'Drill-down DB scan budget reached; results may be partial',\n {\n metricId,\n scannedDbRows,\n eligibleCount: accessibleRows.length,\n dbOffset,\n },\n );\n break;\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return emptyResponse();\n }\n\n // True when we stopped with more DB rows possibly remaining — either the\n // eligible-result cap or the scan budget. Totals / entityHealth may undercount.\n const isCapped =\n moreDbRowsMayExist &&\n (accessibleRows.length === CatalogMetricService.MAX_FETCHABLE_ROWS ||\n stoppedAtScanCap);\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n thresholds,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalogEntityRef);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalogEntityRef,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n thresholds,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n unit: metric.unit,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n /**\n * Get the user and its members entities.\n *\n * @param userEntityRef - User entity reference in format \"kind:namespace/name\"\n * @param metricId - Metric ID to filter entities by\n * @param options - Options for the query\n * @param options.credentials - Backstage credentials\n * @returns Array of entity references in format \"kind:namespace/name\"\n */\n async getUserAndGroupOwnerRefs(\n userEntityRef: string,\n metricId: string,\n options: {\n credentials: BackstageCredentials;\n },\n ): Promise<string[]> {\n const { credentials } = options;\n\n const userEntity = await this.catalog.getEntityByRef(userEntityRef, {\n credentials,\n });\n\n if (!userEntity) {\n throw new NotFoundError('User entity not found in catalog');\n }\n\n if (this.globalDisabledMetrics.includes(metricId)) {\n this.logger.debug(`Disabled metric by app-config: ${metricId}`);\n return [];\n }\n\n const ownerRefs: string[] = [userEntityRef];\n\n const memberOfRelations =\n userEntity.relations?.filter(\n relation => relation.type === RELATION_MEMBER_OF,\n ) ?? [];\n\n if (memberOfRelations.length > 0) {\n for (const relation of memberOfRelations) {\n ownerRefs.push(relation.targetRef);\n }\n }\n\n return ownerRefs;\n }\n\n async getEntitiesOwnedByOwnerRef(\n ownerRef: string,\n metricId: string,\n options: {\n credentials: BackstageCredentials;\n },\n ): Promise<string[]> {\n const QUERY_ENTITIES_BATCH_SIZE = 50;\n\n let cursor: string | undefined = undefined;\n\n const { credentials } = options;\n const entitiesOwnedByUserAndGroups: string[] = [];\n\n do {\n const entities = await this.catalog.queryEntities(\n {\n filter: {\n [`relations.${RELATION_OWNED_BY}`]: ownerRef,\n },\n fields: ['kind', 'metadata'],\n limit: QUERY_ENTITIES_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials },\n );\n\n cursor = entities.pageInfo.nextCursor;\n\n for (const entity of entities.items) {\n if (isMetricIdDisabled(this.config, metricId, entity, this.logger)) {\n continue;\n }\n entitiesOwnedByUserAndGroups.push(stringifyEntityRef(entity));\n }\n } while (cursor !== undefined);\n\n return entitiesOwnedByUserAndGroups;\n }\n\n /**\n * Get the entities owned by a user and their groups.\n *\n * @param userEntityRef - User entity reference in format \"kind:namespace/name\"\n * @param metricId - Metric ID to filter entities by\n * @param options - Options for the query\n * @param options.credentials - Backstage credentials\n * @returns Array of entity references in format \"kind:namespace/name\"\n */\n async getEntitiesOwnedByUser(\n userEntityRef: string,\n metricId: string,\n options: {\n credentials: BackstageCredentials;\n },\n ): Promise<string[]> {\n const { credentials } = options;\n\n const ownerRefs = await this.getUserAndGroupOwnerRefs(\n userEntityRef,\n metricId,\n { credentials },\n );\n\n const entitiesOwnedByOwnerRefs = await Promise.all(\n ownerRefs.map(ownerRef =>\n this.getEntitiesOwnedByOwnerRef(ownerRef, metricId, { credentials }),\n ),\n );\n\n return entitiesOwnedByOwnerRefs.flat();\n }\n}\n"],"names":["isMetricCalculationError","ThresholdEvaluator","NotFoundError","filterAuthorizedMetrics","isMetricIdDisabled","stringifyError","NotAllowedError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef","RELATION_MEMBER_OF","RELATION_OWNED_BY","stringifyEntityRef"],"mappings":"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"}
|
|
@@ -61,7 +61,7 @@ class WeightedStatusScoreAggregationStrategy {
|
|
|
61
61
|
count: aggregatedMetric.values[rule.key] ?? 0,
|
|
62
62
|
score: statusScores[rule.key] ?? 0
|
|
63
63
|
})),
|
|
64
|
-
thresholds,
|
|
64
|
+
thresholds: headlineThresholds,
|
|
65
65
|
weightedStatusScore,
|
|
66
66
|
weightedStatusSum: weightedSum,
|
|
67
67
|
weightedStatusMaxPossible: maxPossibleScore,
|
package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WeightedStatusScoreAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.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 type AggregatedMetric,\n type WeightedStatusScoreAggregationResult,\n type AggregatedMetricResult,\n ThresholdRule,\n aggregationTypes,\n type StatusScoreAggregationOption,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\nimport { getRequiredAggregationChartDisplayColor } from '../../../utils/aggregation/getAggregationChartDisplayColor';\n\nexport class WeightedStatusScoreAggregationStrategy\n implements AggregationStrategy\n{\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n private readonly thresholdEvaluator: ThresholdEvaluator = new ThresholdEvaluator(),\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n if (aggregationConfig.type !== aggregationTypes.weightedStatusScore) {\n throw new Error(\n `Expected aggregation type \"${aggregationTypes.weightedStatusScore}\" but received \"${aggregationConfig.type}\"`,\n );\n }\n\n const {\n statusScores,\n thresholds:\n headlineThresholds = DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS,\n } = aggregationConfig.options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n statusScores,\n metric.id,\n );\n\n const { weightedStatusScore, maxPossibleScore } =\n this.prepareWeightedStatusScoreValues(\n aggregatedMetric.total,\n statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const aggregationChartDisplayColor =\n aggregatedMetric.total > 0\n ? getRequiredAggregationChartDisplayColor(\n weightedStatusScore,\n headlineThresholds,\n this.thresholdEvaluator,\n `The color for percentage '${weightedStatusScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n )\n : null;\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: statusScores[rule.key] ?? 0,\n })),\n thresholds,\n weightedStatusScore,\n weightedStatusSum: weightedSum,\n weightedStatusMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as WeightedStatusScoreAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: StatusScoreAggregationOption,\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for weightedStatusScore aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private prepareWeightedStatusScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: StatusScoreAggregationOption,\n rules: ThresholdRule[],\n weightedSum: number,\n ): { weightedStatusScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const weightedStatusScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * 1000) / 10\n : 0;\n\n return { weightedStatusScore, maxPossibleScore };\n }\n}\n"],"names":["ThresholdEvaluator","aggregationTypes","DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS","getRequiredAggregationChartDisplayColor","AggregatedMetricMapper"],"mappings":";;;;;;;;AAiCO,MAAM,sCAAA,CAEb;AAAA,EACE,YACmB,MAAA,EACA,MAAA,EACA,kBAAA,GAAyC,IAAIA,uCAAmB,EACjF;AAHiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,kBAAA,GAAA,kBAAA;AAAA,EAChB;AAAA,EAHgB,MAAA;AAAA,EACA,MAAA;AAAA,EACA,kBAAA;AAAA,EAGnB,MAAM,SAAA,CAAU;AAAA,IACd,UAAA;AAAA,IACA,MAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF,EAAwD;AACtD,IAAA,IAAI,iBAAA,CAAkB,IAAA,KAASC,+CAAA,CAAiB,mBAAA,EAAqB;AACnE,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,2BAAA,EAA8BA,+CAAA,CAAiB,mBAAmB,CAAA,gBAAA,EAAmB,kBAAkB,IAAI,CAAA,CAAA;AAAA,OAC7G;AAAA,IACF;AAEA,IAAA,MAAM;AAAA,MACJ,YAAA;AAAA,MACA,YACE,kBAAA,GAAqBC;AAAA,QACrB,iBAAA,CAAkB,OAAA;AAEtB,IAAA,MAAM,gBAAA,GACJ,MAAM,IAAA,CAAK,MAAA,CAAO,mCAAA;AAAA,MAChB,UAAA;AAAA,MACA,MAAA,CAAO;AAAA,KACT;AAEF,IAAA,MAAM,cAAc,IAAA,CAAK,oBAAA;AAAA,MACvB,gBAAA,CAAiB,MAAA;AAAA,MACjB,YAAA;AAAA,MACA,MAAA,CAAO;AAAA,KACT;AAEA,IAAA,MAAM,EAAE,mBAAA,EAAqB,gBAAA,EAAiB,GAC5C,IAAA,CAAK,gCAAA;AAAA,MACH,gBAAA,CAAiB,KAAA;AAAA,MACjB,YAAA;AAAA,MACA,UAAA,CAAW,KAAA;AAAA,MACX;AAAA,KACF;AAEF,IAAA,MAAM,4BAAA,GACJ,gBAAA,CAAiB,KAAA,GAAQ,CAAA,GACrBC,uEAAA;AAAA,MACE,mBAAA;AAAA,MACA,kBAAA;AAAA,MACA,IAAA,CAAK,kBAAA;AAAA,MACL,6BAA6B,mBAAmB,CAAA,UAAA,EAAa,OAAO,EAAE,CAAA,0DAAA,EAA6D,kBAAkB,EAAE,CAAA,mCAAA;AAAA,KACzJ,GACA,IAAA;AAEN,IAAA,MAAM,MAAA,GAAS;AAAA,MACb,OAAO,gBAAA,CAAiB,KAAA;AAAA,MACxB,WAAW,gBAAA,CAAiB,SAAA;AAAA,MAC5B,oBAAoB,gBAAA,CAAiB,kBAAA;AAAA,MACrC,uBAAuB,gBAAA,CAAiB,qBAAA;AAAA,MACxC,MAAA,EAAQ,UAAA,CAAW,KAAA,CAAM,GAAA,CAAI,CAAA,IAAA,MAAS;AAAA,QACpC,MAAM,IAAA,CAAK,GAAA;AAAA,QACX,KAAA,EAAO,gBAAA,CAAiB,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,IAAK,CAAA;AAAA,QAC5C,KAAA,EAAO,YAAA,CAAa,IAAA,CAAK,GAAG,CAAA,IAAK;AAAA,OACnC,CAAE,CAAA;AAAA,MACF,UAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"WeightedStatusScoreAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.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 type AggregatedMetric,\n type WeightedStatusScoreAggregationResult,\n type AggregatedMetricResult,\n ThresholdRule,\n aggregationTypes,\n type StatusScoreAggregationOption,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\nimport { getRequiredAggregationChartDisplayColor } from '../../../utils/aggregation/getAggregationChartDisplayColor';\n\nexport class WeightedStatusScoreAggregationStrategy\n implements AggregationStrategy\n{\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n private readonly thresholdEvaluator: ThresholdEvaluator = new ThresholdEvaluator(),\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n if (aggregationConfig.type !== aggregationTypes.weightedStatusScore) {\n throw new Error(\n `Expected aggregation type \"${aggregationTypes.weightedStatusScore}\" but received \"${aggregationConfig.type}\"`,\n );\n }\n\n const {\n statusScores,\n thresholds:\n headlineThresholds = DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS,\n } = aggregationConfig.options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n statusScores,\n metric.id,\n );\n\n const { weightedStatusScore, maxPossibleScore } =\n this.prepareWeightedStatusScoreValues(\n aggregatedMetric.total,\n statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const aggregationChartDisplayColor =\n aggregatedMetric.total > 0\n ? getRequiredAggregationChartDisplayColor(\n weightedStatusScore,\n headlineThresholds,\n this.thresholdEvaluator,\n `The color for percentage '${weightedStatusScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n )\n : null;\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: statusScores[rule.key] ?? 0,\n })),\n thresholds: headlineThresholds,\n weightedStatusScore,\n weightedStatusSum: weightedSum,\n weightedStatusMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as WeightedStatusScoreAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: StatusScoreAggregationOption,\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for weightedStatusScore aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private prepareWeightedStatusScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: StatusScoreAggregationOption,\n rules: ThresholdRule[],\n weightedSum: number,\n ): { weightedStatusScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const weightedStatusScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * 1000) / 10\n : 0;\n\n return { weightedStatusScore, maxPossibleScore };\n }\n}\n"],"names":["ThresholdEvaluator","aggregationTypes","DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS","getRequiredAggregationChartDisplayColor","AggregatedMetricMapper"],"mappings":"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"}
|
|
@@ -7,7 +7,6 @@ var permissionUtils = require('../permissions/permissionUtils.cjs.js');
|
|
|
7
7
|
var catalogModel = require('@backstage/catalog-model');
|
|
8
8
|
var validateMetricIdsQueryParams = require('../middlewares/validateMetricIdsQueryParams.cjs.js');
|
|
9
9
|
var validateTimeSeriesQueryParams = require('../middlewares/validateTimeSeriesQueryParams.cjs.js');
|
|
10
|
-
var getEntitiesOwnedByUser = require('../utils/getEntitiesOwnedByUser.cjs.js');
|
|
11
10
|
var parseCommaSeparatedString = require('../utils/parseCommaSeparatedString.cjs.js');
|
|
12
11
|
var mappers = require('./mappers.cjs.js');
|
|
13
12
|
var validateDrillDownMetricsSchema = require('../validation/validateDrillDownMetricsSchema.cjs.js');
|
|
@@ -23,7 +22,6 @@ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
|
23
22
|
async function createRouter({
|
|
24
23
|
metricProvidersRegistry,
|
|
25
24
|
service,
|
|
26
|
-
catalog,
|
|
27
25
|
httpAuth,
|
|
28
26
|
permissions,
|
|
29
27
|
logger,
|
|
@@ -162,10 +160,13 @@ async function createRouter({
|
|
|
162
160
|
if (!userEntityRef) {
|
|
163
161
|
throw new errors.AuthenticationError("User entity reference not found");
|
|
164
162
|
}
|
|
165
|
-
const entitiesOwnedByAUser = await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
const entitiesOwnedByAUser = await catalogMetricService.getEntitiesOwnedByUser(
|
|
164
|
+
userEntityRef,
|
|
165
|
+
metric.id,
|
|
166
|
+
{
|
|
167
|
+
credentials
|
|
168
|
+
}
|
|
169
|
+
);
|
|
169
170
|
for (const entityRef of entitiesOwnedByAUser) {
|
|
170
171
|
await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
|
|
171
172
|
}
|
|
@@ -256,10 +257,13 @@ async function createRouter({
|
|
|
256
257
|
const metric = metricProvidersRegistry.getMetric(
|
|
257
258
|
aggregationConfig.metricId
|
|
258
259
|
);
|
|
259
|
-
const entitiesOwnedByAUser = await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
const entitiesOwnedByAUser = await catalogMetricService.getEntitiesOwnedByUser(
|
|
261
|
+
userEntityRef,
|
|
262
|
+
metric.id,
|
|
263
|
+
{
|
|
264
|
+
credentials
|
|
265
|
+
}
|
|
266
|
+
);
|
|
263
267
|
for (const entityRef of entitiesOwnedByAUser) {
|
|
264
268
|
await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
|
|
265
269
|
}
|
|
@@ -318,10 +322,13 @@ async function createRouter({
|
|
|
318
322
|
const metric = metricProvidersRegistry.getMetric(
|
|
319
323
|
aggregationConfig.metricId
|
|
320
324
|
);
|
|
321
|
-
const entitiesOwnedByAUser = await
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
+
const entitiesOwnedByAUser = await catalogMetricService.getEntitiesOwnedByUser(
|
|
326
|
+
userEntityRef,
|
|
327
|
+
metric.id,
|
|
328
|
+
{
|
|
329
|
+
credentials
|
|
330
|
+
}
|
|
331
|
+
);
|
|
325
332
|
for (const entityRef of entitiesOwnedByAUser) {
|
|
326
333
|
await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
|
|
327
334
|
}
|
|
@@ -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 {\n AuthenticationError,\n InputError,\n NotAllowedError,\n NotFoundError,\n} from '@backstage/errors';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n LoggerService,\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n authorizeConditional,\n getUserEntityRef,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateMetricIdsQueryParams } from '../middlewares/validateMetricIdsQueryParams';\nimport {\n validateAggregationTimeSeriesQueryParams,\n validateTimeSeriesQueryParams,\n} from '../middlewares/validateTimeSeriesQueryParams';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\nimport { AggregationsService } from './aggregations/AggregationsService';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\nimport type { ScorecardCollectorsService } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\nexport type ScorecardRouterOptions = {\n service: {\n aggregationsService: AggregationsService;\n catalogMetricService: CatalogMetricService;\n };\n metricProvidersRegistry: MetricProvidersRegistry;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n collectorsService: ScorecardCollectorsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n service,\n catalog,\n httpAuth,\n permissions,\n logger,\n thresholdResolver,\n collectorsService,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const { aggregationsService, catalogMetricService } = service;\n\n router.get(\n '/metrics',\n validateMetricIdsQueryParams,\n validateDatasourceQueryParams,\n async (req, res) => {\n const { metricIds, datasource } = req.query;\n\n if (metricIds && datasource) {\n throw new InputError('Cannot filter by both metricIds and datasource');\n }\n\n if (metricIds) {\n return res.json({\n metrics: metricProvidersRegistry.listMetrics(\n parseCommaSeparatedString(metricIds as string),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(\n datasource as string,\n ),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n },\n );\n\n router.get('/metrics/:metricId/collectors', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const collectors = (metric.collectorIds ?? []).map(collectorId => {\n try {\n return collectorsService.getCollectorMetadata(collectorId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new Error(\n `Metric '${metricId}' is configured to use collector '${collectorId}', but that collector is not registered.`,\n );\n }\n throw error;\n }\n });\n\n res.json({ collectors });\n });\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name',\n validateMetricIdsQueryParams,\n async (req, res) => {\n const { metricIds } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\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 ? parseCommaSeparatedString(metricIds as string)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n },\n );\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name/time-series',\n validateTimeSeriesQueryParams,\n async (req, res) => {\n const { metricId, from, to } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n const entityRef = stringifyEntityRef({ kind, namespace, name });\n\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const result = await catalogMetricService.getEntityMetricTimeSeries(\n entityRef,\n metricId as string,\n new Date(from as string),\n new Date(to as string),\n conditions,\n );\n res.json(result);\n },\n );\n\n // Deprecated (RFC 8594): use GET /aggregations/:aggregationId instead.\n router.get(\n '/metrics/:metricId/catalog/aggregations',\n (req, res, next) => {\n const { metricId } = req.params;\n const successorPath = `${req.baseUrl}/aggregations/${encodeURIComponent(\n metricId,\n )}`;\n res.setHeader('Deprecation', 'true');\n res.setHeader('Link', `<${successorPath}>; rel=\"alternate\"`);\n next();\n },\n async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n logger.warn(\n `Deprecated Scorecard API: GET /metrics/${metricId}/catalog/aggregations is deprecated; use GET /aggregations/:aggregationId (e.g. when the aggregation id matches the metric id, GET /aggregations/${metricId}).`,\n );\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n metricId,\n metricProvidersRegistry,\n );\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/metrics/:metricId/catalog/aggregations/entities',\n async (req, res) => {\n const { metricId } = req.params;\n\n const {\n page,\n pageSize,\n status,\n owner,\n kind,\n namespace,\n entityName,\n sortBy,\n sortOrder,\n } = validateDrillDownMetricsSchema(req.query, logger);\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entityMetrics = await catalogMetricService.getEntityMetricDetails(\n metricId,\n credentials,\n {\n status,\n owner,\n kind,\n entityName,\n namespace,\n sortBy,\n sortOrder,\n page,\n limit: pageSize,\n },\n );\n\n res.json(entityMetrics);\n },\n );\n\n router.get(\n '/aggregations/:aggregationId',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/metadata',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/time-series',\n validateAggregationIdParam,\n validateAggregationTimeSeriesQueryParams,\n async (req, res) => {\n const { aggregationId } = req.params;\n const { from, to } = req.query;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n res.json(\n await aggregationsService.getAggregatedMetricTimeSeries({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n from: new Date(from as string),\n to: new Date(to as string),\n }),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","filterAuthorizedMetrics","NotAllowedError","NotFoundError","stringifyEntityRef","checkEntityAccess","validateTimeSeriesQueryParams","AuthenticationError","getEntitiesOwnedByUser","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef","AggregatedMetricMapper","validateAggregationTimeSeriesQueryParams"],"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 {\n AuthenticationError,\n InputError,\n NotAllowedError,\n NotFoundError,\n} from '@backstage/errors';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n LoggerService,\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n authorizeConditional,\n getUserEntityRef,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateMetricIdsQueryParams } from '../middlewares/validateMetricIdsQueryParams';\nimport {\n validateAggregationTimeSeriesQueryParams,\n validateTimeSeriesQueryParams,\n} from '../middlewares/validateTimeSeriesQueryParams';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\nimport { AggregationsService } from './aggregations/AggregationsService';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\nimport type { ScorecardCollectorsService } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\nexport type ScorecardRouterOptions = {\n service: {\n aggregationsService: AggregationsService;\n catalogMetricService: CatalogMetricService;\n };\n metricProvidersRegistry: MetricProvidersRegistry;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n collectorsService: ScorecardCollectorsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n service,\n httpAuth,\n permissions,\n logger,\n thresholdResolver,\n collectorsService,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const { aggregationsService, catalogMetricService } = service;\n\n router.get(\n '/metrics',\n validateMetricIdsQueryParams,\n validateDatasourceQueryParams,\n async (req, res) => {\n const { metricIds, datasource } = req.query;\n\n if (metricIds && datasource) {\n throw new InputError('Cannot filter by both metricIds and datasource');\n }\n\n if (metricIds) {\n return res.json({\n metrics: metricProvidersRegistry.listMetrics(\n parseCommaSeparatedString(metricIds as string),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(\n datasource as string,\n ),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n },\n );\n\n router.get('/metrics/:metricId/collectors', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const collectors = (metric.collectorIds ?? []).map(collectorId => {\n try {\n return collectorsService.getCollectorMetadata(collectorId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new Error(\n `Metric '${metricId}' is configured to use collector '${collectorId}', but that collector is not registered.`,\n );\n }\n throw error;\n }\n });\n\n res.json({ collectors });\n });\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name',\n validateMetricIdsQueryParams,\n async (req, res) => {\n const { metricIds } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\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 ? parseCommaSeparatedString(metricIds as string)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n },\n );\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name/time-series',\n validateTimeSeriesQueryParams,\n async (req, res) => {\n const { metricId, from, to } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n const entityRef = stringifyEntityRef({ kind, namespace, name });\n\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const result = await catalogMetricService.getEntityMetricTimeSeries(\n entityRef,\n metricId as string,\n new Date(from as string),\n new Date(to as string),\n conditions,\n );\n res.json(result);\n },\n );\n\n // Deprecated (RFC 8594): use GET /aggregations/:aggregationId instead.\n router.get(\n '/metrics/:metricId/catalog/aggregations',\n (req, res, next) => {\n const { metricId } = req.params;\n const successorPath = `${req.baseUrl}/aggregations/${encodeURIComponent(\n metricId,\n )}`;\n res.setHeader('Deprecation', 'true');\n res.setHeader('Link', `<${successorPath}>; rel=\"alternate\"`);\n next();\n },\n async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entitiesOwnedByAUser =\n await catalogMetricService.getEntitiesOwnedByUser(\n userEntityRef,\n metric.id,\n {\n credentials,\n },\n );\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n logger.warn(\n `Deprecated Scorecard API: GET /metrics/${metricId}/catalog/aggregations is deprecated; use GET /aggregations/:aggregationId (e.g. when the aggregation id matches the metric id, GET /aggregations/${metricId}).`,\n );\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n metricId,\n metricProvidersRegistry,\n );\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/metrics/:metricId/catalog/aggregations/entities',\n async (req, res) => {\n const { metricId } = req.params;\n\n const {\n page,\n pageSize,\n status,\n owner,\n kind,\n namespace,\n entityName,\n sortBy,\n sortOrder,\n } = validateDrillDownMetricsSchema(req.query, logger);\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entityMetrics = await catalogMetricService.getEntityMetricDetails(\n metricId,\n credentials,\n {\n status,\n owner,\n kind,\n entityName,\n namespace,\n sortBy,\n sortOrder,\n page,\n limit: pageSize,\n },\n );\n\n res.json(entityMetrics);\n },\n );\n\n router.get(\n '/aggregations/:aggregationId',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n const entitiesOwnedByAUser =\n await catalogMetricService.getEntitiesOwnedByUser(\n userEntityRef,\n metric.id,\n {\n credentials,\n },\n );\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/metadata',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/time-series',\n validateAggregationIdParam,\n validateAggregationTimeSeriesQueryParams,\n async (req, res) => {\n const { aggregationId } = req.params;\n const { from, to } = req.query;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig = aggregationsService.getAggregationConfig(\n aggregationId,\n metricProvidersRegistry,\n );\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig.metricId,\n );\n\n const entitiesOwnedByAUser =\n await catalogMetricService.getEntitiesOwnedByUser(\n userEntityRef,\n metric.id,\n {\n credentials,\n },\n );\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n res.json(\n await aggregationsService.getAggregatedMetricTimeSeries({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n from: new Date(from as string),\n to: new Date(to as string),\n }),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","filterAuthorizedMetrics","NotAllowedError","NotFoundError","stringifyEntityRef","checkEntityAccess","validateTimeSeriesQueryParams","AuthenticationError","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef","AggregatedMetricMapper","validateAggregationTimeSeriesQueryParams"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@backstage/plugin-catalog-node": "^2.2.4",
|
|
48
48
|
"@backstage/plugin-permission-common": "^0.9.10",
|
|
49
49
|
"@backstage/plugin-permission-node": "^0.11.3",
|
|
50
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^4.
|
|
51
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^4.
|
|
50
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^4.5.1",
|
|
51
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^4.5.1",
|
|
52
52
|
"express": "^4.17.1",
|
|
53
53
|
"express-promise-router": "^4.1.0",
|
|
54
54
|
"knex": "^3.1.0",
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var errors = require('@backstage/errors');
|
|
4
|
-
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
-
|
|
6
|
-
const QUERY_ENTITIES_BATCH_SIZE = 50;
|
|
7
|
-
async function getEntitiesOwnedByUser(userEntityRef, options) {
|
|
8
|
-
const userEntity = await options.catalog.getEntityByRef(userEntityRef, {
|
|
9
|
-
credentials: options.credentials
|
|
10
|
-
});
|
|
11
|
-
if (!userEntity) {
|
|
12
|
-
throw new errors.NotFoundError("User entity not found in catalog");
|
|
13
|
-
}
|
|
14
|
-
const ownerRefs = [userEntityRef];
|
|
15
|
-
const memberOfRelations = userEntity.relations?.filter(
|
|
16
|
-
(relation) => relation.type === catalogModel.RELATION_MEMBER_OF
|
|
17
|
-
) ?? [];
|
|
18
|
-
if (memberOfRelations.length > 0) {
|
|
19
|
-
for (const relation of memberOfRelations) {
|
|
20
|
-
ownerRefs.push(relation.targetRef);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const entitiesOwnedByUserAndGroups = [];
|
|
24
|
-
for (const ownerRef of ownerRefs) {
|
|
25
|
-
let cursor = void 0;
|
|
26
|
-
do {
|
|
27
|
-
const entities = await options.catalog.queryEntities(
|
|
28
|
-
{
|
|
29
|
-
filter: {
|
|
30
|
-
[`relations.${catalogModel.RELATION_OWNED_BY}`]: ownerRef
|
|
31
|
-
},
|
|
32
|
-
fields: ["kind", "metadata"],
|
|
33
|
-
limit: QUERY_ENTITIES_BATCH_SIZE,
|
|
34
|
-
...cursor ? { cursor } : {}
|
|
35
|
-
},
|
|
36
|
-
{ credentials: options.credentials }
|
|
37
|
-
);
|
|
38
|
-
cursor = entities.pageInfo.nextCursor;
|
|
39
|
-
const entityRefs = entities.items.map(
|
|
40
|
-
(entity) => catalogModel.stringifyEntityRef(entity)
|
|
41
|
-
);
|
|
42
|
-
entitiesOwnedByUserAndGroups.push(...entityRefs);
|
|
43
|
-
} while (cursor !== void 0);
|
|
44
|
-
}
|
|
45
|
-
return entitiesOwnedByUserAndGroups;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
exports.getEntitiesOwnedByUser = getEntitiesOwnedByUser;
|
|
49
|
-
//# sourceMappingURL=getEntitiesOwnedByUser.cjs.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"getEntitiesOwnedByUser.cjs.js","sources":["../../src/utils/getEntitiesOwnedByUser.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 { NotFoundError } from '@backstage/errors';\nimport { BackstageCredentials } from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n RELATION_MEMBER_OF,\n RELATION_OWNED_BY,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\n\nconst QUERY_ENTITIES_BATCH_SIZE = 50;\n\nexport async function getEntitiesOwnedByUser(\n userEntityRef: string,\n options: {\n catalog: CatalogService;\n credentials: BackstageCredentials;\n },\n): Promise<string[]> {\n const userEntity = await options.catalog.getEntityByRef(userEntityRef, {\n credentials: options.credentials,\n });\n\n if (!userEntity) {\n throw new NotFoundError('User entity not found in catalog');\n }\n\n const ownerRefs: string[] = [userEntityRef];\n\n const memberOfRelations =\n userEntity.relations?.filter(\n relation => relation.type === RELATION_MEMBER_OF,\n ) ?? [];\n\n if (memberOfRelations.length > 0) {\n for (const relation of memberOfRelations) {\n ownerRefs.push(relation.targetRef);\n }\n }\n\n const entitiesOwnedByUserAndGroups: string[] = [];\n\n for (const ownerRef of ownerRefs) {\n let cursor: string | undefined = undefined;\n\n do {\n const entities = await options.catalog.queryEntities(\n {\n filter: {\n [`relations.${RELATION_OWNED_BY}`]: ownerRef,\n },\n fields: ['kind', 'metadata'],\n limit: QUERY_ENTITIES_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: options.credentials },\n );\n\n cursor = entities.pageInfo.nextCursor;\n\n const entityRefs = entities.items.map(entity =>\n stringifyEntityRef(entity),\n );\n entitiesOwnedByUserAndGroups.push(...entityRefs);\n } while (cursor !== undefined);\n }\n\n return entitiesOwnedByUserAndGroups;\n}\n"],"names":["NotFoundError","RELATION_MEMBER_OF","RELATION_OWNED_BY","stringifyEntityRef"],"mappings":";;;;;AAyBA,MAAM,yBAAA,GAA4B,EAAA;AAElC,eAAsB,sBAAA,CACpB,eACA,OAAA,EAImB;AACnB,EAAA,MAAM,UAAA,GAAa,MAAM,OAAA,CAAQ,OAAA,CAAQ,eAAe,aAAA,EAAe;AAAA,IACrE,aAAa,OAAA,CAAQ;AAAA,GACtB,CAAA;AAED,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,MAAM,IAAIA,qBAAc,kCAAkC,CAAA;AAAA,EAC5D;AAEA,EAAA,MAAM,SAAA,GAAsB,CAAC,aAAa,CAAA;AAE1C,EAAA,MAAM,iBAAA,GACJ,WAAW,SAAA,EAAW,MAAA;AAAA,IACpB,CAAA,QAAA,KAAY,SAAS,IAAA,KAASC;AAAA,OAC3B,EAAC;AAER,EAAA,IAAI,iBAAA,CAAkB,SAAS,CAAA,EAAG;AAChC,IAAA,KAAA,MAAW,YAAY,iBAAA,EAAmB;AACxC,MAAA,SAAA,CAAU,IAAA,CAAK,SAAS,SAAS,CAAA;AAAA,IACnC;AAAA,EACF;AAEA,EAAA,MAAM,+BAAyC,EAAC;AAEhD,EAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,IAAA,IAAI,MAAA,GAA6B,MAAA;AAEjC,IAAA,GAAG;AACD,MAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,OAAA,CAAQ,aAAA;AAAA,QACrC;AAAA,UACE,MAAA,EAAQ;AAAA,YACN,CAAC,CAAA,UAAA,EAAaC,8BAAiB,CAAA,CAAE,GAAG;AAAA,WACtC;AAAA,UACA,MAAA,EAAQ,CAAC,MAAA,EAAQ,UAAU,CAAA;AAAA,UAC3B,KAAA,EAAO,yBAAA;AAAA,UACP,GAAI,MAAA,GAAS,EAAE,MAAA,KAAW;AAAC,SAC7B;AAAA,QACA,EAAE,WAAA,EAAa,OAAA,CAAQ,WAAA;AAAY,OACrC;AAEA,MAAA,MAAA,GAAS,SAAS,QAAA,CAAS,UAAA;AAE3B,MAAA,MAAM,UAAA,GAAa,SAAS,KAAA,CAAM,GAAA;AAAA,QAAI,CAAA,MAAA,KACpCC,gCAAmB,MAAM;AAAA,OAC3B;AACA,MAAA,4BAAA,CAA6B,IAAA,CAAK,GAAG,UAAU,CAAA;AAAA,IACjD,SAAS,MAAA,KAAW,MAAA;AAAA,EACtB;AAEA,EAAA,OAAO,4BAAA;AACT;;;;"}
|