@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.5.2 → 2.6.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 +12 -0
- package/README.md +8 -6
- package/dist/providers/MetricProvidersRegistry.cjs.js +74 -48
- package/dist/providers/MetricProvidersRegistry.cjs.js.map +1 -1
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +88 -1
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +6 -6
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +5 -4
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 2.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4ecaacd: Add support for batch metric providers, allowing a single provider to handle multiple metrics efficiently. Introduce a new backend module for configurable file existence checks (filecheck.\*) that verify whether required files (like README, LICENSE, or CODEOWNERS) are present in a repository.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [4ecaacd]
|
|
12
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.6.0
|
|
13
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.6.0
|
|
14
|
+
|
|
3
15
|
## 2.5.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -88,12 +88,13 @@ For more information about schedule configuration options, see the [Metric Colle
|
|
|
88
88
|
|
|
89
89
|
The following metric providers are available:
|
|
90
90
|
|
|
91
|
-
| Provider | Metric ID | Title | Description | Type
|
|
92
|
-
| -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
-
| **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number
|
|
94
|
-
| **
|
|
95
|
-
| **
|
|
96
|
-
| **
|
|
91
|
+
| Provider | Metric ID | Title | Description | Type |
|
|
92
|
+
| -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
93
|
+
| **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
|
|
94
|
+
| **Filecheck** | `filecheck.*` | File Checks | Checks whether specific files (e.g., `README.md`, `LICENSE`, `CODEOWNERS`) exist in a repository. | boolean |
|
|
95
|
+
| **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
|
|
96
|
+
| **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
|
|
97
|
+
| **Dependabot** | `dependabot.*` | Dependabot Alerts | Critical, High, Medium and Low CVE Alerts | number |
|
|
97
98
|
|
|
98
99
|
To use these providers, install the corresponding backend modules:
|
|
99
100
|
|
|
@@ -101,6 +102,7 @@ To use these providers, install the corresponding backend modules:
|
|
|
101
102
|
- Jira: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira`](../scorecard-backend-module-jira/README.md)
|
|
102
103
|
- OpenSSF: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf`](../scorecard-backend-module-openssf/README.md)
|
|
103
104
|
- Dependabot: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot`](../scorecard-backend-module-dependabot/README.md)
|
|
105
|
+
- Filecheck: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck`](../scorecard-backend-module-filecheck/README.md)
|
|
104
106
|
|
|
105
107
|
### Disabling Metrics
|
|
106
108
|
|
|
@@ -6,44 +6,51 @@ class MetricProvidersRegistry {
|
|
|
6
6
|
metricProviders = /* @__PURE__ */ new Map();
|
|
7
7
|
datasourceIndex = /* @__PURE__ */ new Map();
|
|
8
8
|
register(metricProvider) {
|
|
9
|
-
const providerId = metricProvider.getProviderId();
|
|
10
9
|
const providerDatasource = metricProvider.getProviderDatasourceId();
|
|
11
|
-
const metric = metricProvider.getMetric();
|
|
12
10
|
const metricType = metricProvider.getMetricType();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
11
|
+
const metricIds = metricProvider.getMetricIds?.() ?? [
|
|
12
|
+
metricProvider.getProviderId()
|
|
13
|
+
];
|
|
14
|
+
const metrics = metricProvider.getMetrics?.() ?? [
|
|
15
|
+
metricProvider.getMetric()
|
|
16
|
+
];
|
|
17
|
+
for (const metricId of metricIds) {
|
|
18
|
+
const metric = metrics.find((m) => m.id === metricId);
|
|
19
|
+
if (!metric) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Invalid metric provider: metric ID '${metricId}' returned by getMetricIds() does not have a corresponding metric in getMetrics()`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (metricType !== metric.type) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid metric provider with ID ${metricId}, getMetricType() must match getMetric().type. Expected '${metricType}', but got '${metric.type}'`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const expectedPrefix = `${providerDatasource}.`;
|
|
30
|
+
if (!metricId.startsWith(expectedPrefix) || metricId === expectedPrefix) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid metric provider with ID ${metricId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (this.metricProviders.has(metricId)) {
|
|
36
|
+
throw new errors.ConflictError(
|
|
37
|
+
`Metric provider with ID '${metricId}' has already been registered`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.metricProviders.set(metricId, metricProvider);
|
|
41
|
+
let datasourceProviders = this.datasourceIndex.get(providerDatasource);
|
|
42
|
+
if (!datasourceProviders) {
|
|
43
|
+
datasourceProviders = /* @__PURE__ */ new Set();
|
|
44
|
+
this.datasourceIndex.set(providerDatasource, datasourceProviders);
|
|
45
|
+
}
|
|
46
|
+
datasourceProviders.add(metricId);
|
|
39
47
|
}
|
|
40
|
-
datasourceProviders.add(providerId);
|
|
41
48
|
}
|
|
42
|
-
getProvider(
|
|
43
|
-
const metricProvider = this.metricProviders.get(
|
|
49
|
+
getProvider(metricId) {
|
|
50
|
+
const metricProvider = this.metricProviders.get(metricId);
|
|
44
51
|
if (!metricProvider) {
|
|
45
52
|
throw new errors.NotFoundError(
|
|
46
|
-
`
|
|
53
|
+
`No metric provider registered for metric ID '${metricId}'.`
|
|
47
54
|
);
|
|
48
55
|
}
|
|
49
56
|
return metricProvider;
|
|
@@ -51,33 +58,49 @@ class MetricProvidersRegistry {
|
|
|
51
58
|
hasProvider(providerId) {
|
|
52
59
|
return this.metricProviders.has(providerId);
|
|
53
60
|
}
|
|
54
|
-
getMetric(
|
|
55
|
-
|
|
61
|
+
getMetric(metricId) {
|
|
62
|
+
const provider = this.getProvider(metricId);
|
|
63
|
+
if (provider.getMetrics) {
|
|
64
|
+
const metrics = provider.getMetrics();
|
|
65
|
+
const metric = metrics.find((m) => m.id === metricId);
|
|
66
|
+
if (metric) {
|
|
67
|
+
return metric;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return provider.getMetric();
|
|
56
71
|
}
|
|
57
|
-
async calculateMetric(
|
|
58
|
-
return this.getProvider(
|
|
72
|
+
async calculateMetric(metricId, entity) {
|
|
73
|
+
return this.getProvider(metricId).calculateMetric(entity);
|
|
59
74
|
}
|
|
60
|
-
async calculateMetrics(
|
|
75
|
+
async calculateMetrics(metricIds, entity) {
|
|
61
76
|
const results = await Promise.allSettled(
|
|
62
|
-
|
|
77
|
+
metricIds.map((metricId) => this.calculateMetric(metricId, entity))
|
|
63
78
|
);
|
|
64
79
|
return results.map((result, index) => {
|
|
65
|
-
const
|
|
80
|
+
const metricId = metricIds[index];
|
|
66
81
|
if (result.status === "fulfilled") {
|
|
67
|
-
return {
|
|
82
|
+
return { metricId, value: result.value };
|
|
68
83
|
}
|
|
69
|
-
return {
|
|
84
|
+
return { metricId, error: result.reason };
|
|
70
85
|
});
|
|
71
86
|
}
|
|
72
87
|
listProviders() {
|
|
73
|
-
return
|
|
88
|
+
return [...new Set(this.metricProviders.values())];
|
|
74
89
|
}
|
|
75
|
-
listMetrics(
|
|
76
|
-
if (
|
|
77
|
-
return
|
|
90
|
+
listMetrics(metricIds) {
|
|
91
|
+
if (metricIds && metricIds.length !== 0) {
|
|
92
|
+
return metricIds.map((metricId) => {
|
|
93
|
+
const provider = this.metricProviders.get(metricId);
|
|
94
|
+
if (!provider) return void 0;
|
|
95
|
+
if (provider.getMetrics) {
|
|
96
|
+
const metrics = provider.getMetrics();
|
|
97
|
+
return metrics.find((m) => m.id === metricId);
|
|
98
|
+
}
|
|
99
|
+
return provider.getMetric();
|
|
100
|
+
}).filter((m) => m !== void 0);
|
|
78
101
|
}
|
|
79
|
-
return
|
|
80
|
-
(provider) => provider.getMetric()
|
|
102
|
+
return this.listProviders().flatMap(
|
|
103
|
+
(provider) => provider.getMetrics?.() ?? [provider.getMetric()]
|
|
81
104
|
);
|
|
82
105
|
}
|
|
83
106
|
listMetricsByDatasource(datasourceId) {
|
|
@@ -85,7 +108,10 @@ class MetricProvidersRegistry {
|
|
|
85
108
|
if (!providerIdsOfDatasource) {
|
|
86
109
|
return [];
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
const providers = [...providerIdsOfDatasource].map((id) => this.metricProviders.get(id)).filter((p) => p !== void 0);
|
|
112
|
+
return [...new Set(providers)].flatMap(
|
|
113
|
+
(provider) => provider.getMetrics?.() ?? [provider.getMetric()]
|
|
114
|
+
);
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerId = metricProvider.getProviderId();\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metric = metricProvider.getMetric();\n const metricType = metricProvider.getMetricType();\n\n if (providerId !== metric.id) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, provider ID must match metric ID '${metric.id}'`,\n );\n }\n\n if (metricType !== metric.type) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, getMetricType() must match getMetric().type. Expected '${metricType}', but got '${metric.type}'`,\n );\n }\n\n const expectedPrefix = `${providerDatasource}.`;\n if (\n !providerId.startsWith(expectedPrefix) ||\n providerId === expectedPrefix\n ) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(providerId)) {\n throw new ConflictError(\n `Metric provider with ID '${providerId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(providerId, metricProvider);\n\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(providerId);\n }\n\n getProvider(providerId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(providerId);\n if (!metricProvider) {\n throw new NotFoundError(\n `Metric provider with ID '${providerId}' is not registered.`,\n );\n }\n return metricProvider;\n }\n\n hasProvider(providerId: string): boolean {\n return this.metricProviders.has(providerId);\n }\n\n getMetric(providerId: string): Metric {\n return this.getProvider(providerId).getMetric();\n }\n\n async calculateMetric(\n providerId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(providerId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n providerIds: string[],\n entity: Entity,\n ): Promise<{ providerId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n providerIds.map(providerId => this.calculateMetric(providerId, entity)),\n );\n\n return results.map((result, index) => {\n const providerId = providerIds[index];\n if (result.status === 'fulfilled') {\n return { providerId, value: result.value };\n }\n return { providerId, error: result.reason as Error };\n });\n }\n\n listProviders(): MetricProvider[] {\n return Array.from(this.metricProviders.values());\n }\n\n listMetrics(providerIds?: string[]): Metric[] {\n if (providerIds && providerIds.length !== 0) {\n return providerIds\n .map(providerId => this.metricProviders.get(providerId)?.getMetric())\n .filter((m): m is Metric => m !== undefined);\n }\n return [...this.metricProviders.values()].map(provider =>\n provider.getMetric(),\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n return Array.from(providerIdsOfDatasource)\n .map(providerId => this.metricProviders.get(providerId))\n .filter((provider): provider is MetricProvider => provider !== undefined)\n .map(provider => provider.getMetric());\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metricType = metricProvider.getMetricType();\n\n // Support both single and batch providers\n const metricIds = metricProvider.getMetricIds?.() ?? [\n metricProvider.getProviderId(),\n ];\n const metrics = metricProvider.getMetrics?.() ?? [\n metricProvider.getMetric(),\n ];\n\n // Validate: Each metric ID must have a corresponding metric definition\n for (const metricId of metricIds) {\n const metric = metrics.find(m => m.id === metricId);\n if (!metric) {\n throw new Error(\n `Invalid metric provider: metric ID '${metricId}' returned by getMetricIds() ` +\n `does not have a corresponding metric in getMetrics()`,\n );\n }\n\n if (metricType !== metric.type) {\n throw new Error(\n `Invalid metric provider with ID ${metricId}, getMetricType() must match ` +\n `getMetric().type. Expected '${metricType}', but got '${metric.type}'`,\n );\n }\n\n // Validate: Provider ID format (datasource.metric_name)\n const expectedPrefix = `${providerDatasource}.`;\n if (!metricId.startsWith(expectedPrefix) || metricId === expectedPrefix) {\n throw new Error(\n `Invalid metric provider with ID ${metricId}, must have format ` +\n `'${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(metricId)) {\n throw new ConflictError(\n `Metric provider with ID '${metricId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(metricId, metricProvider);\n\n // Index by datasource\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(metricId);\n }\n }\n\n getProvider(metricId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(metricId);\n if (!metricProvider) {\n throw new NotFoundError(\n `No metric provider registered for metric ID '${metricId}'.`,\n );\n }\n return metricProvider;\n }\n\n hasProvider(providerId: string): boolean {\n return this.metricProviders.has(providerId);\n }\n\n getMetric(metricId: string): Metric {\n const provider = this.getProvider(metricId);\n\n // For batch providers, find the specific metric by ID\n if (provider.getMetrics) {\n const metrics = provider.getMetrics();\n const metric = metrics.find(m => m.id === metricId);\n if (metric) {\n return metric;\n }\n }\n\n return provider.getMetric();\n }\n\n async calculateMetric(\n metricId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(metricId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n metricIds: string[],\n entity: Entity,\n ): Promise<{ metricId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n metricIds.map(metricId => this.calculateMetric(metricId, entity)),\n );\n\n return results.map((result, index) => {\n const metricId = metricIds[index];\n if (result.status === 'fulfilled') {\n return { metricId, value: result.value };\n }\n return { metricId, error: result.reason as Error };\n });\n }\n\n listProviders(): MetricProvider[] {\n // Deduplicate providers since batch providers are stored under multiple metric IDs\n return [...new Set(this.metricProviders.values())];\n }\n\n listMetrics(metricIds?: string[]): Metric[] {\n if (metricIds && metricIds.length !== 0) {\n return metricIds\n .map(metricId => {\n const provider = this.metricProviders.get(metricId);\n if (!provider) return undefined;\n\n if (provider.getMetrics) {\n const metrics = provider.getMetrics();\n return metrics.find(m => m.id === metricId);\n }\n\n return provider.getMetric();\n })\n .filter((m): m is Metric => m !== undefined);\n }\n\n // List all metrics from all providers (deduplicate batch providers)\n return this.listProviders().flatMap(\n provider => provider.getMetrics?.() ?? [provider.getMetric()],\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n // Get unique providers for this datasource, then get their metrics\n const providers = [...providerIdsOfDatasource]\n .map(id => this.metricProviders.get(id))\n .filter((p): p is MetricProvider => p !== undefined);\n\n return [...new Set(providers)].flatMap(\n provider => provider.getMetrics?.() ?? [provider.getMetric()],\n );\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}
|
|
@@ -67,6 +67,8 @@ class PullMetricsByProviderTask {
|
|
|
67
67
|
let totalProcessed = 0;
|
|
68
68
|
let cursor = void 0;
|
|
69
69
|
const metricType = provider.getMetricType();
|
|
70
|
+
const isBatchProvider = typeof provider.calculateMetrics === "function";
|
|
71
|
+
const metricIds = provider.getMetricIds?.() ?? [provider.getProviderId()];
|
|
70
72
|
try {
|
|
71
73
|
do {
|
|
72
74
|
const entitiesResponse = await this.catalog.queryEntities(
|
|
@@ -80,6 +82,81 @@ class PullMetricsByProviderTask {
|
|
|
80
82
|
cursor = entitiesResponse.pageInfo.nextCursor;
|
|
81
83
|
const batchResults = await Promise.allSettled(
|
|
82
84
|
entitiesResponse.items.map(async (entity) => {
|
|
85
|
+
if (isBatchProvider && provider.calculateMetrics) {
|
|
86
|
+
const entityRef = catalogModel.stringifyEntityRef(entity);
|
|
87
|
+
const entityKind = normalizeField(entity.kind);
|
|
88
|
+
const entityNamespace = normalizeField(entity.metadata.namespace);
|
|
89
|
+
const entityOwner = normalizeOwnerRef.normalizeOwnerRef(entity?.spec?.owner);
|
|
90
|
+
const enabledMetricIds = metricIds.filter(
|
|
91
|
+
(metricId) => !metricUtils.isMetricIdDisabled(this.config, metricId, entity, logger)
|
|
92
|
+
);
|
|
93
|
+
if (enabledMetricIds.length === 0) {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const resultsMap = await provider.calculateMetrics(entity);
|
|
98
|
+
return enabledMetricIds.map((metricId) => {
|
|
99
|
+
if (!resultsMap.has(metricId)) {
|
|
100
|
+
return {
|
|
101
|
+
catalog_entity_ref: entityRef,
|
|
102
|
+
metric_id: metricId,
|
|
103
|
+
value: void 0,
|
|
104
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
105
|
+
error_message: `calculateMetrics() did not return an entry for metric '${metricId}'`,
|
|
106
|
+
entity_kind: entityKind,
|
|
107
|
+
entity_namespace: entityNamespace,
|
|
108
|
+
entity_owner: entityOwner
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const value2 = resultsMap.get(metricId);
|
|
112
|
+
try {
|
|
113
|
+
const thresholds = mergeEntityAndProviderThresholds.mergeEntityAndProviderThresholds(
|
|
114
|
+
entity,
|
|
115
|
+
provider
|
|
116
|
+
);
|
|
117
|
+
const status = this.thresholdEvaluator.getFirstMatchingThreshold(
|
|
118
|
+
value2,
|
|
119
|
+
metricType,
|
|
120
|
+
thresholds
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
catalog_entity_ref: entityRef,
|
|
124
|
+
metric_id: metricId,
|
|
125
|
+
value: value2,
|
|
126
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
127
|
+
status,
|
|
128
|
+
entity_kind: entityKind,
|
|
129
|
+
entity_namespace: entityNamespace,
|
|
130
|
+
entity_owner: entityOwner
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
catalog_entity_ref: entityRef,
|
|
135
|
+
metric_id: metricId,
|
|
136
|
+
value: value2,
|
|
137
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
138
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
139
|
+
entity_kind: entityKind,
|
|
140
|
+
entity_namespace: entityNamespace,
|
|
141
|
+
entity_owner: entityOwner
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return enabledMetricIds.map(
|
|
147
|
+
(metricId) => ({
|
|
148
|
+
catalog_entity_ref: entityRef,
|
|
149
|
+
metric_id: metricId,
|
|
150
|
+
value: void 0,
|
|
151
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
152
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
153
|
+
entity_kind: entityKind,
|
|
154
|
+
entity_namespace: entityNamespace,
|
|
155
|
+
entity_owner: entityOwner
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
83
160
|
let value;
|
|
84
161
|
try {
|
|
85
162
|
if (metricUtils.isMetricIdDisabled(
|
|
@@ -132,11 +209,21 @@ class PullMetricsByProviderTask {
|
|
|
132
209
|
).then(
|
|
133
210
|
(promises) => promises.reduce((acc, curr) => {
|
|
134
211
|
if (curr.status === "fulfilled" && curr.value !== void 0) {
|
|
135
|
-
|
|
212
|
+
const result = curr.value;
|
|
213
|
+
if (Array.isArray(result)) {
|
|
214
|
+
return [...acc, ...result];
|
|
215
|
+
}
|
|
216
|
+
return [...acc, result];
|
|
136
217
|
}
|
|
137
218
|
return acc;
|
|
138
219
|
}, [])
|
|
139
220
|
);
|
|
221
|
+
if (batchResults.length > 0) {
|
|
222
|
+
const errorCount = batchResults.filter((r) => r.error_message).length;
|
|
223
|
+
logger.debug(
|
|
224
|
+
`Storing ${batchResults.length} metric values (${errorCount} errors)`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
140
227
|
await this.database.createMetricValues(batchResults);
|
|
141
228
|
totalProcessed += entitiesResponse.items.length;
|
|
142
229
|
} while (cursor !== void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PullMetricsByProviderTask.cjs.js","sources":["../../../src/scheduler/tasks/PullMetricsByProviderTask.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport {\n AuthService,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { mergeEntityAndProviderThresholds } from '../../utils/mergeEntityAndProviderThresholds';\nimport { isMetricIdDisabled } from '../../utils/metricUtils';\nimport { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';\nimport { v4 as uuid } from 'uuid';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { DbMetricValueCreate } from '../../database/types';\nimport { SchedulerOptions, SchedulerTask } from '../types';\nimport { ThresholdEvaluator } from '../../threshold/ThresholdEvaluator';\nimport { MetricValue } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\ntype Options = Pick<\n SchedulerOptions,\n | 'scheduler'\n | 'logger'\n | 'database'\n | 'config'\n | 'catalog'\n | 'auth'\n | 'thresholdEvaluator'\n>;\n\nexport class PullMetricsByProviderTask implements SchedulerTask {\n private readonly config: Config;\n private readonly auth: AuthService;\n private readonly providerId: string;\n private readonly logger: LoggerService;\n private readonly catalog: CatalogService;\n private readonly provider: MetricProvider;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n\n private static readonly CATALOG_BATCH_SIZE = 50;\n\n private static readonly DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { hours: 1 },\n timeout: { minutes: 15 },\n initialDelay: { minutes: 1 },\n };\n\n constructor(options: Options, provider: MetricProvider) {\n this.config = options.config;\n this.auth = options.auth;\n this.providerId = provider.getProviderId();\n this.logger = options.logger;\n this.catalog = options.catalog;\n this.provider = provider;\n this.scheduler = options.scheduler;\n this.database = options.database;\n this.thresholdEvaluator = options.thresholdEvaluator;\n }\n\n async start(): Promise<void> {\n const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;\n const schedule = this.getScheduleFromConfig(scheduleConfigPath);\n\n const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);\n\n await taskRunner.run({\n id: this.providerId,\n fn: async () => {\n const logger = this.logger.child({\n class: this.constructor.name,\n taskId: this.providerId,\n taskInstanceId: uuid(),\n });\n\n try {\n await this.pullProviderMetrics(this.provider, logger);\n } catch (error) {\n logger.error(\n `${this.providerId} pulling metrics failed, ${error}`,\n error,\n );\n }\n },\n });\n }\n\n private getScheduleFromConfig(\n schedulePath: string,\n ): SchedulerServiceTaskScheduleDefinition {\n return this.config.has(schedulePath)\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n this.config.getConfig(schedulePath),\n )\n : PullMetricsByProviderTask.DEFAULT_SCHEDULE;\n }\n\n private async pullProviderMetrics(\n provider: MetricProvider,\n logger: LoggerService,\n ): Promise<void> {\n logger.info(`Pulling metrics for ${this.providerId}`);\n\n let totalProcessed = 0;\n let cursor: string | undefined = undefined;\n\n const metricType = provider.getMetricType();\n\n try {\n do {\n const entitiesResponse = await this.catalog.queryEntities(\n {\n filter: provider.getCatalogFilter(),\n limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n cursor = entitiesResponse.pageInfo.nextCursor;\n\n const batchResults = await Promise.allSettled(\n entitiesResponse.items.map(async entity => {\n let value: MetricValue | undefined;\n\n try {\n if (\n isMetricIdDisabled(\n this.config,\n provider.getProviderId(),\n entity,\n logger,\n )\n ) {\n return undefined;\n }\n\n value = await provider.calculateMetric(entity);\n\n const thresholds = mergeEntityAndProviderThresholds(\n entity,\n provider,\n );\n\n const status = this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metricType,\n thresholds,\n );\n\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value,\n timestamp: new Date(),\n status,\n entity_kind: normalizeField(entity.kind),\n entity_namespace: normalizeField(entity.metadata.namespace),\n entity_owner: normalizeOwnerRef(entity?.spec?.owner),\n } as DbMetricValueCreate;\n } catch (error) {\n // status is intentionally omitted — a calculation failure produces a NULL status\n // in the database, which sorts last when sortBy=status is used\n logger.warn(\n `Failed to calculate metric for entity ${stringifyEntityRef(\n entity,\n )}: ${error}`,\n error instanceof Error ? error : undefined,\n );\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: normalizeField(entity.kind),\n entity_namespace: normalizeField(entity.metadata.namespace),\n entity_owner: normalizeOwnerRef(entity?.spec?.owner),\n } as DbMetricValueCreate;\n }\n }),\n ).then(promises =>\n promises.reduce((acc, curr) => {\n if (curr.status === 'fulfilled' && curr.value !== undefined) {\n return [...acc, curr.value];\n }\n return acc;\n }, [] as DbMetricValueCreate[]),\n );\n\n await this.database.createMetricValues(batchResults);\n totalProcessed += entitiesResponse.items.length;\n } while (cursor !== undefined);\n\n logger.info(\n `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`,\n );\n } catch (error) {\n logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);\n\n throw error;\n }\n }\n}\n\nfunction normalizeField(field: unknown): string | undefined {\n if (typeof field !== 'string') return undefined;\n const normalized = field.trim().toLowerCase();\n if (!normalized) return undefined;\n\n // Prevent DB insertion failures (limits column length to 255 characters)\n return normalized.length <= 255 ? normalized : normalized.slice(0, 255);\n}\n"],"names":["uuid","readSchedulerServiceTaskScheduleDefinitionFromConfig","isMetricIdDisabled","mergeEntityAndProviderThresholds","stringifyEntityRef","normalizeOwnerRef"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PullMetricsByProviderTask.cjs.js","sources":["../../../src/scheduler/tasks/PullMetricsByProviderTask.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport {\n AuthService,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { mergeEntityAndProviderThresholds } from '../../utils/mergeEntityAndProviderThresholds';\nimport { isMetricIdDisabled } from '../../utils/metricUtils';\nimport { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';\nimport { v4 as uuid } from 'uuid';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { DbMetricValueCreate } from '../../database/types';\nimport { SchedulerOptions, SchedulerTask } from '../types';\nimport { ThresholdEvaluator } from '../../threshold/ThresholdEvaluator';\nimport { MetricValue } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\ntype Options = Pick<\n SchedulerOptions,\n | 'scheduler'\n | 'logger'\n | 'database'\n | 'config'\n | 'catalog'\n | 'auth'\n | 'thresholdEvaluator'\n>;\n\nexport class PullMetricsByProviderTask implements SchedulerTask {\n private readonly config: Config;\n private readonly auth: AuthService;\n private readonly providerId: string;\n private readonly logger: LoggerService;\n private readonly catalog: CatalogService;\n private readonly provider: MetricProvider;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n\n private static readonly CATALOG_BATCH_SIZE = 50;\n\n private static readonly DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { hours: 1 },\n timeout: { minutes: 15 },\n initialDelay: { minutes: 1 },\n };\n\n constructor(options: Options, provider: MetricProvider) {\n this.config = options.config;\n this.auth = options.auth;\n this.providerId = provider.getProviderId();\n this.logger = options.logger;\n this.catalog = options.catalog;\n this.provider = provider;\n this.scheduler = options.scheduler;\n this.database = options.database;\n this.thresholdEvaluator = options.thresholdEvaluator;\n }\n\n async start(): Promise<void> {\n const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;\n const schedule = this.getScheduleFromConfig(scheduleConfigPath);\n\n const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);\n\n await taskRunner.run({\n id: this.providerId,\n fn: async () => {\n const logger = this.logger.child({\n class: this.constructor.name,\n taskId: this.providerId,\n taskInstanceId: uuid(),\n });\n\n try {\n await this.pullProviderMetrics(this.provider, logger);\n } catch (error) {\n logger.error(\n `${this.providerId} pulling metrics failed, ${error}`,\n error,\n );\n }\n },\n });\n }\n\n private getScheduleFromConfig(\n schedulePath: string,\n ): SchedulerServiceTaskScheduleDefinition {\n return this.config.has(schedulePath)\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n this.config.getConfig(schedulePath),\n )\n : PullMetricsByProviderTask.DEFAULT_SCHEDULE;\n }\n\n private async pullProviderMetrics(\n provider: MetricProvider,\n logger: LoggerService,\n ): Promise<void> {\n logger.info(`Pulling metrics for ${this.providerId}`);\n\n let totalProcessed = 0;\n let cursor: string | undefined = undefined;\n\n const metricType = provider.getMetricType();\n const isBatchProvider = typeof provider.calculateMetrics === 'function';\n const metricIds = provider.getMetricIds?.() ?? [provider.getProviderId()];\n\n try {\n do {\n const entitiesResponse = await this.catalog.queryEntities(\n {\n filter: provider.getCatalogFilter(),\n limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n cursor = entitiesResponse.pageInfo.nextCursor;\n\n const batchResults = await Promise.allSettled(\n entitiesResponse.items.map(async entity => {\n // Handle batch providers\n if (isBatchProvider && provider.calculateMetrics) {\n const entityRef = stringifyEntityRef(entity);\n const entityKind = normalizeField(entity.kind);\n const entityNamespace = normalizeField(entity.metadata.namespace);\n const entityOwner = normalizeOwnerRef(entity?.spec?.owner);\n\n const enabledMetricIds = metricIds.filter(\n metricId =>\n !isMetricIdDisabled(this.config, metricId, entity, logger),\n );\n\n if (enabledMetricIds.length === 0) {\n return undefined;\n }\n\n try {\n const resultsMap = await provider.calculateMetrics(entity);\n\n return enabledMetricIds.map(metricId => {\n if (!resultsMap.has(metricId)) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message: `calculateMetrics() did not return an entry for metric '${metricId}'`,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n\n const value = resultsMap.get(metricId) as MetricValue;\n\n try {\n const thresholds = mergeEntityAndProviderThresholds(\n entity,\n provider,\n );\n\n const status =\n this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metricType,\n thresholds,\n );\n\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n status,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n } catch (error) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n });\n } catch (error) {\n return enabledMetricIds.map(\n metricId =>\n ({\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate),\n );\n }\n }\n\n let value: MetricValue | undefined;\n\n try {\n if (\n isMetricIdDisabled(\n this.config,\n provider.getProviderId(),\n entity,\n logger,\n )\n ) {\n return undefined;\n }\n\n value = await provider.calculateMetric(entity);\n\n const thresholds = mergeEntityAndProviderThresholds(\n entity,\n provider,\n );\n\n const status = this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metricType,\n thresholds,\n );\n\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value,\n timestamp: new Date(),\n status,\n entity_kind: normalizeField(entity.kind),\n entity_namespace: normalizeField(entity.metadata.namespace),\n entity_owner: normalizeOwnerRef(entity?.spec?.owner),\n } as DbMetricValueCreate;\n } catch (error) {\n // status is intentionally omitted — a calculation failure produces a NULL status\n // in the database, which sorts last when sortBy=status is used\n logger.warn(\n `Failed to calculate metric for entity ${stringifyEntityRef(\n entity,\n )}: ${error}`,\n error instanceof Error ? error : undefined,\n );\n return {\n catalog_entity_ref: stringifyEntityRef(entity),\n metric_id: this.providerId,\n value,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: normalizeField(entity.kind),\n entity_namespace: normalizeField(entity.metadata.namespace),\n entity_owner: normalizeOwnerRef(entity?.spec?.owner),\n } as DbMetricValueCreate;\n }\n }),\n ).then(promises =>\n promises.reduce((acc, curr) => {\n if (curr.status === 'fulfilled' && curr.value !== undefined) {\n // Batch providers return an array of results, single providers return one result\n const result = curr.value;\n if (Array.isArray(result)) {\n return [...acc, ...result];\n }\n return [...acc, result];\n }\n return acc;\n }, [] as DbMetricValueCreate[]),\n );\n\n if (batchResults.length > 0) {\n const errorCount = batchResults.filter(r => r.error_message).length;\n logger.debug(\n `Storing ${batchResults.length} metric values (${errorCount} errors)`,\n );\n }\n\n await this.database.createMetricValues(batchResults);\n totalProcessed += entitiesResponse.items.length;\n } while (cursor !== undefined);\n\n logger.info(\n `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`,\n );\n } catch (error) {\n logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);\n\n throw error;\n }\n }\n}\n\nfunction normalizeField(field: unknown): string | undefined {\n if (typeof field !== 'string') return undefined;\n const normalized = field.trim().toLowerCase();\n if (!normalized) return undefined;\n\n // Prevent DB insertion failures (limits column length to 255 characters)\n return normalized.length <= 255 ? normalized : normalized.slice(0, 255);\n}\n"],"names":["uuid","readSchedulerServiceTaskScheduleDefinitionFromConfig","stringifyEntityRef","normalizeOwnerRef","isMetricIdDisabled","value","mergeEntityAndProviderThresholds"],"mappings":"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"}
|
|
@@ -27,22 +27,22 @@ class CatalogMetricService {
|
|
|
27
27
|
this.config = options.config;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Get latest metric results for a specific catalog entity
|
|
30
|
+
* Get latest metric results for a specific catalog entity.
|
|
31
31
|
*
|
|
32
32
|
* @param entityRef - Entity reference in format "kind:namespace/name"
|
|
33
|
-
* @param
|
|
34
|
-
*
|
|
33
|
+
* @param metricIds - Optional array of metric IDs to get latest metrics of.
|
|
34
|
+
* If not provided, gets all available latest metrics.
|
|
35
35
|
* @param filter - Permission filter
|
|
36
36
|
* @returns Metric results with entity-specific thresholds applied
|
|
37
37
|
*/
|
|
38
|
-
async getLatestEntityMetrics(entityRef,
|
|
38
|
+
async getLatestEntityMetrics(entityRef, metricIds, filter) {
|
|
39
39
|
const entity = await this.catalog.getEntityByRef(entityRef, {
|
|
40
40
|
credentials: await this.auth.getOwnServiceCredentials()
|
|
41
41
|
});
|
|
42
42
|
if (!entity) {
|
|
43
43
|
throw new errors.NotFoundError(`Entity not found: ${entityRef}`);
|
|
44
44
|
}
|
|
45
|
-
const metricsToFetch = this.registry.listMetrics(
|
|
45
|
+
const metricsToFetch = this.registry.listMetrics(metricIds);
|
|
46
46
|
const authorizedMetricsToFetch = permissionUtils.filterAuthorizedMetrics(
|
|
47
47
|
metricsToFetch,
|
|
48
48
|
filter
|
|
@@ -56,7 +56,7 @@ class CatalogMetricService {
|
|
|
56
56
|
let thresholds;
|
|
57
57
|
let thresholdError;
|
|
58
58
|
const provider = this.registry.getProvider(metric_id);
|
|
59
|
-
const metric =
|
|
59
|
+
const metric = this.registry.getMetric(metric_id);
|
|
60
60
|
try {
|
|
61
61
|
thresholds = mergeEntityAndProviderThresholds.mergeEntityAndProviderThresholds(entity, provider);
|
|
62
62
|
if (value === null) {
|
|
@@ -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 EntityMetricDetailResponse,\n EntityMetricDetail,\n aggregationTypes,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} 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';\nimport { DbMetricValue } from '../database/types';\nimport type { Config } from '@backstage/config';\nimport {\n buildAggregationConfig,\n type AggregationConfig,\n} from '../utils/buildAggregationConfig';\nimport { AGGREGATION_KPIS_CONFIG_PATH } from '../constants';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n config: Config;\n};\n\nexport class CatalogMetricService {\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly config: Config;\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.config = options.config;\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 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.open_prs\")\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 },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\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.catalog_entity_ref),\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].catalog_entity_ref, 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 },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\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 },\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 };\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.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\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 },\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 };\n }\n\n /**\n * Get the aggregation configs for a given aggregation IDs filtered by metric IDs.\n * If no aggregation IDs are provided, all aggregation configs will be returned.\n * If no metric IDs are provided, all metrics will be included.\n *\n * @param aggregationIds - Optional array of aggregation IDs to fetch the configs for.\n * @param metricIds - Optional array of metric IDs, when provided, only aggregations whose config metricId is in this array will be returned.\n * @returns Aggregation configs\n */\n getAggregationConfigs(\n aggregationIds: string[] = [],\n metricIds: string[] = [],\n ): AggregationConfig[] {\n const aggregationKPIsConfig = this.config.getOptionalConfig(\n AGGREGATION_KPIS_CONFIG_PATH,\n );\n\n if (!aggregationKPIsConfig) {\n return [];\n }\n\n const aggregations: AggregationConfig[] = [];\n\n const aggregationConfigIds =\n aggregationIds.length > 0 ? aggregationIds : aggregationKPIsConfig.keys();\n\n for (const aggregationId of aggregationConfigIds) {\n const config = aggregationKPIsConfig.getOptionalConfig(aggregationId);\n\n if (config) {\n const aggregationConfig = buildAggregationConfig(aggregationId, {\n config,\n });\n\n if (\n metricIds.length === 0 ||\n metricIds.includes(aggregationConfig.metricId)\n ) {\n aggregations.push(aggregationConfig);\n }\n }\n }\n\n return aggregations;\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef","AGGREGATION_KPIS_CONFIG_PATH","buildAggregationConfig"],"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 AggregatedMetric,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n aggregationTypes,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} 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';\nimport { DbMetricValue } from '../database/types';\nimport type { Config } from '@backstage/config';\nimport {\n buildAggregationConfig,\n type AggregationConfig,\n} from '../utils/buildAggregationConfig';\nimport { AGGREGATION_KPIS_CONFIG_PATH } from '../constants';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n config: Config;\n};\n\nexport class CatalogMetricService {\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly config: Config;\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.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 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 = this.registry.getMetric(metric_id);\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 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.open_prs\")\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 },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\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.catalog_entity_ref),\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].catalog_entity_ref, 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 },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\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 },\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 };\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.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\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 },\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 };\n }\n\n /**\n * Get the aggregation configs for a given aggregation IDs filtered by metric IDs.\n * If no aggregation IDs are provided, all aggregation configs will be returned.\n * If no metric IDs are provided, all metrics will be included.\n *\n * @param aggregationIds - Optional array of aggregation IDs to fetch the configs for.\n * @param metricIds - Optional array of metric IDs, when provided, only aggregations whose config metricId is in this array will be returned.\n * @returns Aggregation configs\n */\n getAggregationConfigs(\n aggregationIds: string[] = [],\n metricIds: string[] = [],\n ): AggregationConfig[] {\n const aggregationKPIsConfig = this.config.getOptionalConfig(\n AGGREGATION_KPIS_CONFIG_PATH,\n );\n\n if (!aggregationKPIsConfig) {\n return [];\n }\n\n const aggregations: AggregationConfig[] = [];\n\n const aggregationConfigIds =\n aggregationIds.length > 0 ? aggregationIds : aggregationKPIsConfig.keys();\n\n for (const aggregationId of aggregationConfigIds) {\n const config = aggregationKPIsConfig.getOptionalConfig(aggregationId);\n\n if (config) {\n const aggregationConfig = buildAggregationConfig(aggregationId, {\n config,\n });\n\n if (\n metricIds.length === 0 ||\n metricIds.includes(aggregationConfig.metricId)\n ) {\n aggregations.push(aggregationConfig);\n }\n }\n }\n\n return aggregations;\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef","AGGREGATION_KPIS_CONFIG_PATH","buildAggregationConfig"],"mappings":"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"}
|
|
@@ -96,7 +96,7 @@ async function createRouter({
|
|
|
96
96
|
backstagePluginScorecardCommon.scorecardMetricReadPermission
|
|
97
97
|
);
|
|
98
98
|
const provider = metricProvidersRegistry.getProvider(metricId);
|
|
99
|
-
const metric =
|
|
99
|
+
const metric = metricProvidersRegistry.getMetric(metricId);
|
|
100
100
|
const authorizedMetrics = permissionUtils.filterAuthorizedMetrics([metric], conditions);
|
|
101
101
|
if (authorizedMetrics.length === 0) {
|
|
102
102
|
throw new errors.NotAllowedError(
|
|
@@ -202,7 +202,9 @@ async function createRouter({
|
|
|
202
202
|
const provider = metricProvidersRegistry.getProvider(
|
|
203
203
|
aggregationConfig?.metricId ?? aggregationId
|
|
204
204
|
);
|
|
205
|
-
const metric =
|
|
205
|
+
const metric = metricProvidersRegistry.getMetric(
|
|
206
|
+
aggregationConfig?.metricId ?? aggregationId
|
|
207
|
+
);
|
|
206
208
|
const entitiesOwnedByAUser = await getEntitiesOwnedByUser.getEntitiesOwnedByUser(userEntityRef, {
|
|
207
209
|
catalog,
|
|
208
210
|
credentials
|
|
@@ -240,10 +242,9 @@ async function createRouter({
|
|
|
240
242
|
const [aggregationConfig] = catalogMetricService.getAggregationConfigs([
|
|
241
243
|
aggregationId
|
|
242
244
|
]);
|
|
243
|
-
const
|
|
245
|
+
const metric = metricProvidersRegistry.getMetric(
|
|
244
246
|
aggregationConfig?.metricId ?? aggregationId
|
|
245
247
|
);
|
|
246
|
-
const metric = provider.getMetric();
|
|
247
248
|
res.json(
|
|
248
249
|
mappers.AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig)
|
|
249
250
|
);
|
|
@@ -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 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 { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport {\n aggregationTypes,\n scorecardMetricReadPermission,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n logger,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\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(\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 // 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 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 aggregationTypes.statusGrouped, // By default, used the status grouped aggregation type\n );\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 res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\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] = catalogMetricService.getAggregationConfigs([\n aggregationId,\n ]);\n\n const provider = metricProvidersRegistry.getProvider(\n aggregationConfig?.metricId ?? aggregationId,\n );\n const metric = provider.getMetric();\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\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 = provider.getMetricThresholds();\n\n const aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metric.id,\n aggregationConfig?.type ?? aggregationTypes.statusGrouped,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\n aggregationConfig,\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] = catalogMetricService.getAggregationConfigs([\n aggregationId,\n ]);\n\n const provider = metricProvidersRegistry.getProvider(\n aggregationConfig?.metricId ?? aggregationId,\n );\n const metric = provider.getMetric();\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","NotAllowedError","AuthenticationError","getEntitiesOwnedByUser","aggregationTypes","AggregatedMetricMapper","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef"],"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} 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 { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport {\n aggregationTypes,\n scorecardMetricReadPermission,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n logger,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\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(\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 // 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 provider = metricProvidersRegistry.getProvider(metricId);\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 = provider.getMetricThresholds();\n const aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metricId,\n aggregationTypes.statusGrouped, // By default, used the status grouped aggregation type\n );\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 res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\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] = catalogMetricService.getAggregationConfigs([\n aggregationId,\n ]);\n\n const provider = metricProvidersRegistry.getProvider(\n aggregationConfig?.metricId ?? aggregationId,\n );\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\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 = provider.getMetricThresholds();\n\n const aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metric.id,\n aggregationConfig?.type ?? aggregationTypes.statusGrouped,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\n aggregationConfig,\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] = catalogMetricService.getAggregationConfigs([\n aggregationId,\n ]);\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","NotAllowedError","AuthenticationError","getEntitiesOwnedByUser","aggregationTypes","AggregatedMetricMapper","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef"],"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": "2.
|
|
3
|
+
"version": "2.6.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": "^2.1.0",
|
|
48
48
|
"@backstage/plugin-permission-common": "^0.9.7",
|
|
49
49
|
"@backstage/plugin-permission-node": "^0.10.11",
|
|
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.6.0",
|
|
51
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.6.0",
|
|
52
52
|
"express": "^4.17.1",
|
|
53
53
|
"express-promise-router": "^4.1.0",
|
|
54
54
|
"knex": "^3.1.0",
|