@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.3.5 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/config.d.ts +6 -1
- package/dist/database/DatabaseMetricValues.cjs.js +30 -31
- package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/mappers.cjs.js +11 -10
- package/dist/service/mappers.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +8 -2
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js +22 -20
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +1 -1
- package/migrations/20260206115752_remove_status_check_constraint.js +92 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7062658: Introduces custom threshold rule keys and colors that can be configured in `app-config.yaml`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [7062658]
|
|
12
|
+
- Updated dependencies [dc5e31a]
|
|
13
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.4.0
|
|
14
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.4.0
|
|
15
|
+
|
|
3
16
|
## 2.3.5
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ To use these providers, install the corresponding backend modules:
|
|
|
102
102
|
|
|
103
103
|
## Thresholds
|
|
104
104
|
|
|
105
|
-
Thresholds define conditions
|
|
105
|
+
Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). The Scorecard plugin provides multiple ways to configure thresholds:
|
|
106
106
|
|
|
107
107
|
- **Provider Defaults**: Metric providers define default thresholds
|
|
108
108
|
- **App Configuration**: Override defaults through `app-config.yaml`
|
package/config.d.ts
CHANGED
|
@@ -32,9 +32,14 @@ export interface Config {
|
|
|
32
32
|
/** Threshold configuration for the metric */
|
|
33
33
|
thresholds?: {
|
|
34
34
|
rules?: Array<{
|
|
35
|
-
key:
|
|
35
|
+
key: string;
|
|
36
36
|
/** Threshold expression - supports: >=, <=, >, <, ==, !=, - (range) */
|
|
37
37
|
expression: string;
|
|
38
|
+
/**
|
|
39
|
+
* Color for this threshold rule. Can be a theme palette path (e.g., 'error.main')
|
|
40
|
+
* or a direct color value (e.g., '#ADD8E6', 'blue', 'rgb(255,255,0)')
|
|
41
|
+
*/
|
|
42
|
+
color?: string;
|
|
38
43
|
}>;
|
|
39
44
|
};
|
|
40
45
|
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
|
|
@@ -33,39 +33,38 @@ class DatabaseMetricValues {
|
|
|
33
33
|
* Get aggregated metrics by status for multiple entities and metrics.
|
|
34
34
|
*/
|
|
35
35
|
async readAggregatedMetricByEntityRefs(catalog_entity_refs, metric_id) {
|
|
36
|
-
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
maxTimestamp =
|
|
56
|
-
} else {
|
|
57
|
-
maxTimestamp = /* @__PURE__ */ new Date();
|
|
36
|
+
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("catalog_entity_ref");
|
|
37
|
+
const statusRows = await this.dbClient(this.tableName).select("status").count("* as count").max("timestamp as max_timestamp").whereIn("id", latestIdsSubquery).whereNotNull("status").whereNotNull("value").groupBy("status");
|
|
38
|
+
if (!statusRows || statusRows.length === 0) {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
const normalizeTimestamp = (timestamp) => {
|
|
42
|
+
if (timestamp instanceof Date) {
|
|
43
|
+
return timestamp;
|
|
44
|
+
} else if (typeof timestamp === "number" || typeof timestamp === "string") {
|
|
45
|
+
return new Date(timestamp);
|
|
46
|
+
}
|
|
47
|
+
return /* @__PURE__ */ new Date();
|
|
48
|
+
};
|
|
49
|
+
let maxTimestamp = /* @__PURE__ */ new Date(0);
|
|
50
|
+
let total = 0;
|
|
51
|
+
const statusCounts = {};
|
|
52
|
+
for (const row of statusRows) {
|
|
53
|
+
const rowTimestamp = normalizeTimestamp(row.max_timestamp);
|
|
54
|
+
if (rowTimestamp > maxTimestamp) {
|
|
55
|
+
maxTimestamp = rowTimestamp;
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
success: Number(row.success),
|
|
64
|
-
warning: Number(row.warning),
|
|
65
|
-
error: Number(row.error)
|
|
66
|
-
};
|
|
57
|
+
const name = row.status;
|
|
58
|
+
const count = Number(row.count);
|
|
59
|
+
statusCounts[name] = count;
|
|
60
|
+
total += count;
|
|
67
61
|
}
|
|
68
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
metric_id,
|
|
64
|
+
total,
|
|
65
|
+
max_timestamp: maxTimestamp,
|
|
66
|
+
statusCounts
|
|
67
|
+
};
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('
|
|
1
|
+
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereNotNull('value')\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return undefined;\n }\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: any): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n } else if (\n typeof timestamp === 'number' ||\n typeof timestamp === 'string'\n ) {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n return {\n metric_id,\n total,\n max_timestamp: maxTimestamp,\n statusCounts,\n };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -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 AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { AggregatedMetricMapper } from './mappers';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n};\n\nexport
|
|
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 AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { AggregatedMetricMapper } from './mappers';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n};\n\nexport class CatalogMetricService {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\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 }\n\n /**\n * Get latest metric results for a specific catalog entity and metric providers.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param providerIds - Optional array of provider IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n providerIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = this.registry.listMetrics(providerIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(\n ({ metric_id, value, error_message, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const provider = this.registry.getProvider(metric_id);\n const metric = provider.getMetric();\n\n try {\n thresholds = mergeEntityAndProviderThresholds(entity, provider);\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (error_message) {\n thresholdError = error_message;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = error_message !== null && value === null;\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 history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get an aggregated metric 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 results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper"],"mappings":"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"}
|
|
@@ -2,22 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
class AggregatedMetricMapper {
|
|
4
4
|
static toAggregatedMetric(aggregatedMetric) {
|
|
5
|
-
const success = aggregatedMetric?.success ?? 0;
|
|
6
|
-
const warning = aggregatedMetric?.warning ?? 0;
|
|
7
|
-
const error = aggregatedMetric?.error ?? 0;
|
|
8
5
|
const total = aggregatedMetric?.total ?? 0;
|
|
9
6
|
const timestamp = aggregatedMetric?.max_timestamp ? new Date(aggregatedMetric.max_timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
10
7
|
return {
|
|
11
|
-
values:
|
|
12
|
-
{ count: success, name: "success" },
|
|
13
|
-
{ count: warning, name: "warning" },
|
|
14
|
-
{ count: error, name: "error" }
|
|
15
|
-
],
|
|
8
|
+
values: aggregatedMetric?.statusCounts ?? {},
|
|
16
9
|
total,
|
|
17
10
|
timestamp
|
|
18
11
|
};
|
|
19
12
|
}
|
|
20
|
-
static toAggregatedMetricResult(metric, aggregatedMetric) {
|
|
13
|
+
static toAggregatedMetricResult(metric, thresholds, aggregatedMetric) {
|
|
14
|
+
const allStatusCountValues = thresholds.rules.map((rule) => ({
|
|
15
|
+
name: rule.key,
|
|
16
|
+
count: aggregatedMetric.values[rule.key] ?? 0
|
|
17
|
+
}));
|
|
21
18
|
return {
|
|
22
19
|
id: metric.id,
|
|
23
20
|
status: "success",
|
|
@@ -27,7 +24,11 @@ class AggregatedMetricMapper {
|
|
|
27
24
|
type: metric.type,
|
|
28
25
|
history: metric.history
|
|
29
26
|
},
|
|
30
|
-
result:
|
|
27
|
+
result: {
|
|
28
|
+
...aggregatedMetric,
|
|
29
|
+
values: allStatusCountValues,
|
|
30
|
+
thresholds
|
|
31
|
+
}
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.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 AggregatedMetric,\n AggregatedMetricResult,\n Metric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const
|
|
1
|
+
{"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.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 AggregatedMetric,\n AggregatedMetricResult,\n Metric,\n ThresholdConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const total = aggregatedMetric?.total ?? 0;\n const timestamp = aggregatedMetric?.max_timestamp\n ? new Date(aggregatedMetric.max_timestamp).toISOString()\n : new Date().toISOString();\n\n return {\n values: aggregatedMetric?.statusCounts ?? {},\n total,\n timestamp,\n };\n }\n\n static toAggregatedMetricResult(\n metric: Metric,\n thresholds: ThresholdConfig,\n aggregatedMetric: AggregatedMetric,\n ): AggregatedMetricResult {\n // Build values in threshold rules order, filling missing ones with 0\n const allStatusCountValues = thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n }));\n\n return {\n id: metric.id,\n status: 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n result: {\n ...aggregatedMetric,\n values: allStatusCountValues,\n thresholds,\n },\n };\n }\n}\n"],"names":[],"mappings":";;AAwBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,OAAO,mBACL,gBACkB,EAAA;AAClB,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAA,MAAM,SAAY,GAAA,gBAAA,EAAkB,aAChC,GAAA,IAAI,IAAK,CAAA,gBAAA,CAAiB,aAAa,CAAA,CAAE,WAAY,EAAA,GAAA,iBACjD,IAAA,IAAA,IAAO,WAAY,EAAA;AAE3B,IAAO,OAAA;AAAA,MACL,MAAA,EAAQ,gBAAkB,EAAA,YAAA,IAAgB,EAAC;AAAA,MAC3C,KAAA;AAAA,MACA;AAAA,KACF;AAAA;AACF,EAEA,OAAO,wBAAA,CACL,MACA,EAAA,UAAA,EACA,gBACwB,EAAA;AAExB,IAAA,MAAM,oBAAuB,GAAA,UAAA,CAAW,KAAM,CAAA,GAAA,CAAI,CAAS,IAAA,MAAA;AAAA,MACzD,MAAM,IAAK,CAAA,GAAA;AAAA,MACX,KAAO,EAAA,gBAAA,CAAiB,MAAO,CAAA,IAAA,CAAK,GAAG,CAAK,IAAA;AAAA,KAC5C,CAAA,CAAA;AAEF,IAAO,OAAA;AAAA,MACL,IAAI,MAAO,CAAA,EAAA;AAAA,MACX,MAAQ,EAAA,SAAA;AAAA,MACR,QAAU,EAAA;AAAA,QACR,OAAO,MAAO,CAAA,KAAA;AAAA,QACd,aAAa,MAAO,CAAA,WAAA;AAAA,QACpB,MAAM,MAAO,CAAA,IAAA;AAAA,QACb,SAAS,MAAO,CAAA;AAAA,OAClB;AAAA,MACA,MAAQ,EAAA;AAAA,QACN,GAAG,gBAAA;AAAA,QACH,MAAQ,EAAA,oBAAA;AAAA,QACR;AAAA;AACF,KACF;AAAA;AAEJ;;;;"}
|
|
@@ -89,7 +89,8 @@ async function createRouter({
|
|
|
89
89
|
req,
|
|
90
90
|
backstagePluginScorecardCommon.scorecardMetricReadPermission
|
|
91
91
|
);
|
|
92
|
-
const
|
|
92
|
+
const provider = metricProvidersRegistry.getProvider(metricId);
|
|
93
|
+
const metric = provider.getMetric();
|
|
93
94
|
const authorizedMetrics = permissionUtils.filterAuthorizedMetrics([metric], conditions);
|
|
94
95
|
if (authorizedMetrics.length === 0) {
|
|
95
96
|
throw new errors.NotAllowedError(
|
|
@@ -108,12 +109,17 @@ async function createRouter({
|
|
|
108
109
|
for (const entityRef of entitiesOwnedByAUser) {
|
|
109
110
|
await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
|
|
110
111
|
}
|
|
112
|
+
const thresholds = provider.getMetricThresholds();
|
|
111
113
|
const aggregatedMetric = await catalogMetricService.getAggregatedMetricByEntityRefs(
|
|
112
114
|
entitiesOwnedByAUser,
|
|
113
115
|
metricId
|
|
114
116
|
);
|
|
115
117
|
res.json(
|
|
116
|
-
mappers.AggregatedMetricMapper.toAggregatedMetricResult(
|
|
118
|
+
mappers.AggregatedMetricMapper.toAggregatedMetricResult(
|
|
119
|
+
metric,
|
|
120
|
+
thresholds,
|
|
121
|
+
aggregatedMetric
|
|
122
|
+
)
|
|
117
123
|
);
|
|
118
124
|
});
|
|
119
125
|
return router;
|
|
@@ -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} from '@backstage/errors';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateCatalogMetricsSchema } from '../validation/validateCatalogMetricsSchema';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { validateMetricsSchema } from '../validation/validateMetricsSchema';\nimport { AggregatedMetricMapper } from './mappers';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { metricIds, datasource } = validateMetricsSchema(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),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(datasource),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const { metricIds } = validateCatalogMetricsSchema(req.query);\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)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n router.get('/metrics/:metricId/catalog/aggregations', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const
|
|
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} from '@backstage/errors';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateCatalogMetricsSchema } from '../validation/validateCatalogMetricsSchema';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { validateMetricsSchema } from '../validation/validateMetricsSchema';\nimport { AggregatedMetricMapper } from './mappers';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { metricIds, datasource } = validateMetricsSchema(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),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(datasource),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const { metricIds } = validateCatalogMetricsSchema(req.query);\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)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n router.get('/metrics/:metricId/catalog/aggregations', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const provider = metricProvidersRegistry.getProvider(metricId);\n const metric = provider.getMetric();\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 = provider.getMetricThresholds();\n const aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metricId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\n ),\n );\n });\n\n return router;\n}\n"],"names":["Router","express","AuthorizeResult","NotAllowedError","validateMetricsSchema","InputError","parseCommaSeparatedString","scorecardMetricReadPermission","validateCatalogMetricsSchema","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","AuthenticationError","getEntitiesOwnedByUser","AggregatedMetricMapper"],"mappings":"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"}
|
|
@@ -4,37 +4,26 @@ var catalogModel = require('@backstage/catalog-model');
|
|
|
4
4
|
var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plugin-scorecard-node');
|
|
5
5
|
var errors = require('@backstage/errors');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const thresholdRulesAnnotationPrefix = (providerId) => `scorecard.io/${providerId}.thresholds.rules.`;
|
|
8
|
+
function parseEntityOverrideThresholds(entity, providerId) {
|
|
8
9
|
const annotations = entity.metadata?.annotations || {};
|
|
9
|
-
const prefix =
|
|
10
|
+
const prefix = thresholdRulesAnnotationPrefix(providerId);
|
|
10
11
|
const overrides = [];
|
|
11
12
|
for (const [annotationKey, expression] of Object.entries(annotations)) {
|
|
12
13
|
if (annotationKey.startsWith(prefix) && expression) {
|
|
13
14
|
const key = annotationKey.substring(prefix.length);
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
backstagePluginScorecardNode.validateThresholds({ rules: [entityRule] }, metricType);
|
|
17
|
-
overrides.push(entityRule);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
if (errors.isError(e)) {
|
|
20
|
-
throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
|
|
21
|
-
`Invalid threshold annotation '${annotationKey}: ${expression}' in entity '${catalogModel.stringifyEntityRef(
|
|
22
|
-
entity
|
|
23
|
-
)}': ${e.message}`
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
throw e;
|
|
27
|
-
}
|
|
15
|
+
overrides.push({ key, expression });
|
|
28
16
|
}
|
|
29
17
|
}
|
|
30
18
|
return overrides;
|
|
31
19
|
}
|
|
32
20
|
function mergeEntityAndProviderThresholds(entity, provider) {
|
|
21
|
+
const providerId = provider.getProviderId();
|
|
33
22
|
const providerThresholds = provider.getMetricThresholds();
|
|
23
|
+
const providerMetricType = provider.getMetricType();
|
|
34
24
|
const entityOverrideThresholds = parseEntityOverrideThresholds(
|
|
35
25
|
entity,
|
|
36
|
-
|
|
37
|
-
provider.getMetricType()
|
|
26
|
+
providerId
|
|
38
27
|
);
|
|
39
28
|
const mergedRules = [...providerThresholds.rules];
|
|
40
29
|
for (const override of entityOverrideThresholds) {
|
|
@@ -45,10 +34,23 @@ function mergeEntityAndProviderThresholds(entity, provider) {
|
|
|
45
34
|
entity
|
|
46
35
|
)} thresholds by ${JSON.stringify(
|
|
47
36
|
override
|
|
48
|
-
)}, metric provider ${
|
|
37
|
+
)}, metric provider ${providerId} does not support key ${override.key}`
|
|
49
38
|
);
|
|
50
39
|
}
|
|
51
|
-
mergedRules[foundKey]
|
|
40
|
+
const mergedRule = { ...mergedRules[foundKey], ...override };
|
|
41
|
+
try {
|
|
42
|
+
backstagePluginScorecardNode.validateThresholds({ rules: [mergedRule] }, providerMetricType);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (errors.isError(e)) {
|
|
45
|
+
throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
|
|
46
|
+
`Invalid threshold annotation '${thresholdRulesAnnotationPrefix(
|
|
47
|
+
providerId
|
|
48
|
+
)}${override.key}: ${override.expression}' in entity '${catalogModel.stringifyEntityRef(entity)}': ${e.message}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
mergedRules[foundKey] = mergedRule;
|
|
52
54
|
}
|
|
53
55
|
return {
|
|
54
56
|
rules: mergedRules
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mergeEntityAndProviderThresholds.cjs.js","sources":["../../src/utils/mergeEntityAndProviderThresholds.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport type {\n
|
|
1
|
+
{"version":3,"file":"mergeEntityAndProviderThresholds.cjs.js","sources":["../../src/utils/mergeEntityAndProviderThresholds.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport type {\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n ThresholdConfigFormatError,\n validateThresholds,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isError } from '@backstage/errors';\nimport type { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\nconst thresholdRulesAnnotationPrefix = (providerId: string) =>\n `scorecard.io/${providerId}.thresholds.rules.`;\n\n/**\n * Extract threshold override rules from entity annotations for a given provider, doesn't validate rules.\n */\nfunction parseEntityOverrideThresholds(\n entity: Entity,\n providerId: string,\n): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = thresholdRulesAnnotationPrefix(providerId);\n const overrides: ThresholdRule[] = [];\n\n for (const [annotationKey, expression] of Object.entries(annotations)) {\n if (annotationKey.startsWith(prefix) && expression) {\n const key = annotationKey.substring(prefix.length);\n overrides.push({ key, expression });\n }\n }\n\n return overrides;\n}\n\nexport function mergeEntityAndProviderThresholds(\n entity: Entity,\n provider: MetricProvider,\n): ThresholdConfig {\n const providerId = provider.getProviderId();\n const providerThresholds = provider.getMetricThresholds();\n const providerMetricType = provider.getMetricType();\n const entityOverrideThresholds = parseEntityOverrideThresholds(\n entity,\n providerId,\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityOverrideThresholds) {\n const foundKey = mergedRules.findIndex(rule => rule.key === override.key);\n if (foundKey === -1) {\n throw new ThresholdConfigFormatError(\n `Unable to override ${stringifyEntityRef(\n entity,\n )} thresholds by ${JSON.stringify(\n override,\n )}, metric provider ${providerId} does not support key ${override.key}`,\n );\n }\n\n const mergedRule: ThresholdRule = { ...mergedRules[foundKey], ...override };\n try {\n validateThresholds({ rules: [mergedRule] }, providerMetricType);\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${thresholdRulesAnnotationPrefix(\n providerId,\n )}${override.key}: ${\n override.expression\n }' in entity '${stringifyEntityRef(entity)}': ${e.message}`,\n );\n }\n throw e;\n }\n\n mergedRules[foundKey] = mergedRule;\n }\n\n return {\n rules: mergedRules,\n };\n}\n"],"names":["ThresholdConfigFormatError","stringifyEntityRef","validateThresholds","isError"],"mappings":";;;;;;AA4BA,MAAM,8BAAiC,GAAA,CAAC,UACtC,KAAA,CAAA,aAAA,EAAgB,UAAU,CAAA,kBAAA,CAAA;AAK5B,SAAS,6BAAA,CACP,QACA,UACiB,EAAA;AACjB,EAAA,MAAM,WAAc,GAAA,MAAA,CAAO,QAAU,EAAA,WAAA,IAAe,EAAC;AACrD,EAAM,MAAA,MAAA,GAAS,+BAA+B,UAAU,CAAA;AACxD,EAAA,MAAM,YAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,CAAC,aAAe,EAAA,UAAU,KAAK,MAAO,CAAA,OAAA,CAAQ,WAAW,CAAG,EAAA;AACrE,IAAA,IAAI,aAAc,CAAA,UAAA,CAAW,MAAM,CAAA,IAAK,UAAY,EAAA;AAClD,MAAA,MAAM,GAAM,GAAA,aAAA,CAAc,SAAU,CAAA,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,SAAA,CAAU,IAAK,CAAA,EAAE,GAAK,EAAA,UAAA,EAAY,CAAA;AAAA;AACpC;AAGF,EAAO,OAAA,SAAA;AACT;AAEgB,SAAA,gCAAA,CACd,QACA,QACiB,EAAA;AACjB,EAAM,MAAA,UAAA,GAAa,SAAS,aAAc,EAAA;AAC1C,EAAM,MAAA,kBAAA,GAAqB,SAAS,mBAAoB,EAAA;AACxD,EAAM,MAAA,kBAAA,GAAqB,SAAS,aAAc,EAAA;AAClD,EAAA,MAAM,wBAA2B,GAAA,6BAAA;AAAA,IAC/B,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,wBAA0B,EAAA;AAC/C,IAAA,MAAM,WAAW,WAAY,CAAA,SAAA,CAAU,UAAQ,IAAK,CAAA,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAI,EAAA;AACnB,MAAA,MAAM,IAAIA,uDAAA;AAAA,QACR,CAAsB,mBAAA,EAAAC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAK,CAAA,SAAA;AAAA,UACtB;AAAA,SACD,CAAA,kBAAA,EAAqB,UAAU,CAAA,sBAAA,EAAyB,SAAS,GAAG,CAAA;AAAA,OACvE;AAAA;AAGF,IAAA,MAAM,aAA4B,EAAE,GAAG,YAAY,QAAQ,CAAA,EAAG,GAAG,QAAS,EAAA;AAC1E,IAAI,IAAA;AACF,MAAAC,+CAAA,CAAmB,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,kBAAkB,CAAA;AAAA,aACvD,CAAG,EAAA;AACV,MAAI,IAAAC,cAAA,CAAQ,CAAC,CAAG,EAAA;AACd,QAAA,MAAM,IAAIH,uDAAA;AAAA,UACR,CAAiC,8BAAA,EAAA,8BAAA;AAAA,YAC/B;AAAA,WACD,CAAA,EAAG,QAAS,CAAA,GAAG,CACd,EAAA,EAAA,QAAA,CAAS,UACX,CAAA,aAAA,EAAgBC,+BAAmB,CAAA,MAAM,CAAC,CAAA,GAAA,EAAM,EAAE,OAAO,CAAA;AAAA,SAC3D;AAAA;AAEF,MAAM,MAAA,CAAA;AAAA;AAGR,IAAA,WAAA,CAAY,QAAQ,CAAI,GAAA,UAAA;AAAA;AAG1B,EAAO,OAAA;AAAA,IACL,KAAO,EAAA;AAAA,GACT;AACF;;;;"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Red Hat, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
exports.up = async function up(knex) {
|
|
18
|
+
// Remove the status_check constraint that limits status values to 'success', 'warning', 'error'
|
|
19
|
+
const client = knex.client.config.client;
|
|
20
|
+
|
|
21
|
+
if (client === 'sqlite3' || client === 'better-sqlite3') {
|
|
22
|
+
await knex.raw(`
|
|
23
|
+
ALTER TABLE metric_values
|
|
24
|
+
RENAME COLUMN status TO status_old;
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
await knex.raw(`
|
|
28
|
+
ALTER TABLE metric_values
|
|
29
|
+
ADD COLUMN status VARCHAR(255) NULL;
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
await knex.raw(`
|
|
33
|
+
UPDATE metric_values
|
|
34
|
+
SET status = status_old;
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
await knex.raw(`
|
|
38
|
+
ALTER TABLE metric_values
|
|
39
|
+
DROP COLUMN status_old;
|
|
40
|
+
`);
|
|
41
|
+
} else {
|
|
42
|
+
await knex.schema.alterTable('metric_values', table => {
|
|
43
|
+
table.dropChecks(['status_check']);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
exports.down = async function down(knex) {
|
|
49
|
+
// Re-add the status_check constraint
|
|
50
|
+
|
|
51
|
+
// Fail if any incompatible rows with status values that don't match the constraint
|
|
52
|
+
const incompatibleRows = await knex('metric_values')
|
|
53
|
+
.whereNotIn('status', ['success', 'warning', 'error'])
|
|
54
|
+
.whereNotNull('status')
|
|
55
|
+
.count('* as count')
|
|
56
|
+
.first();
|
|
57
|
+
if (incompatibleRows && incompatibleRows.count > 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Cannot rollback migration: Found ${incompatibleRows.count} rows with status values ` +
|
|
60
|
+
`outside of ['success', 'warning', 'error']. Please migrate or remove these rows before rolling back.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const client = knex.client.config.client;
|
|
65
|
+
|
|
66
|
+
if (client === 'sqlite3' || client === 'better-sqlite3') {
|
|
67
|
+
await knex.raw(`
|
|
68
|
+
ALTER TABLE metric_values
|
|
69
|
+
RENAME COLUMN status TO status_old;
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
await knex.raw(`
|
|
73
|
+
ALTER TABLE metric_values
|
|
74
|
+
ADD COLUMN status VARCHAR(255) NULL
|
|
75
|
+
CHECK (status IN ('success', 'warning', 'error'));
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
await knex.raw(`
|
|
79
|
+
UPDATE metric_values
|
|
80
|
+
SET status = status_old;
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
await knex.raw(`
|
|
84
|
+
ALTER TABLE metric_values
|
|
85
|
+
DROP COLUMN status_old;
|
|
86
|
+
`);
|
|
87
|
+
} else {
|
|
88
|
+
await knex.raw(
|
|
89
|
+
"ALTER TABLE metric_values ADD CONSTRAINT status_check CHECK (status IN ('success', 'warning', 'error'))",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
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": "^1.20.0",
|
|
48
48
|
"@backstage/plugin-permission-common": "^0.9.3",
|
|
49
49
|
"@backstage/plugin-permission-node": "^0.10.6",
|
|
50
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.
|
|
51
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.
|
|
50
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.4.0",
|
|
51
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.4.0",
|
|
52
52
|
"express": "^4.17.1",
|
|
53
53
|
"express-promise-router": "^4.1.0",
|
|
54
54
|
"knex": "^3.1.0",
|