@backstage/plugin-kubernetes-backend 0.21.11-next.1 → 0.21.11-next.2
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 +17 -0
- package/config.schema.json +13 -0
- package/dist/cluster-locator/CatalogClusterLocator.cjs.js +63 -24
- package/dist/cluster-locator/CatalogClusterLocator.cjs.js.map +1 -1
- package/dist/cluster-locator/CatalogClusterLocatorOptions.cjs.js +15 -0
- package/dist/cluster-locator/CatalogClusterLocatorOptions.cjs.js.map +1 -0
- package/dist/cluster-locator/catalogClusterAuthMetadata.cjs.js +19 -0
- package/dist/cluster-locator/catalogClusterAuthMetadata.cjs.js.map +1 -0
- package/dist/cluster-locator/index.cjs.js +6 -1
- package/dist/cluster-locator/index.cjs.js.map +1 -1
- package/dist/cluster-locator/validateClusterApiServerUrl.cjs.js +89 -0
- package/dist/cluster-locator/validateClusterApiServerUrl.cjs.js.map +1 -0
- package/dist/package.json.cjs.js +1 -1
- package/dist/service/KubernetesConnection.cjs.js +8 -3
- package/dist/service/KubernetesConnection.cjs.js.map +1 -1
- package/package.json +18 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @backstage/plugin-kubernetes-backend
|
|
2
2
|
|
|
3
|
+
## 0.21.11-next.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5268d63: Migrated tests from MSW v1 to MSW v2.
|
|
8
|
+
- 97a7e8e: The catalog cluster locator now validates Kubernetes API server URLs to block SSRF targets (non-public addresses, cloud metadata endpoints, and non-HTTPS URLs by default). Operators may list trusted hostnames in `dangerouslyAllowClusterUrls` on the catalog locator method to permit HTTP or non-public addresses for those hosts only (for example local minikube). Catalog entities cannot use the `serviceAccount` auth provider, cannot enable TLS verification skipping unless `dangerouslyAllowSkipTLSVerify` is set on the locator method, and only permitted annotations are passed through as auth metadata. Kubernetes API fetches no longer follow HTTP redirects automatically.
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
- @backstage/config@1.3.9-next.0
|
|
11
|
+
- @backstage/plugin-kubernetes-node@0.4.8-next.2
|
|
12
|
+
- @backstage/catalog-client@1.16.2-next.1
|
|
13
|
+
- @backstage/plugin-catalog-node@2.2.5-next.1
|
|
14
|
+
- @backstage/backend-plugin-api@1.10.1-next.1
|
|
15
|
+
- @backstage/integration-aws-node@0.2.2-next.0
|
|
16
|
+
- @backstage/plugin-permission-common@0.9.11-next.0
|
|
17
|
+
- @backstage/plugin-permission-node@0.11.4-next.1
|
|
18
|
+
- @backstage/plugin-kubernetes-common@0.9.13-next.1
|
|
19
|
+
|
|
3
20
|
## 0.21.11-next.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/config.schema.json
CHANGED
|
@@ -150,6 +150,19 @@
|
|
|
150
150
|
"type": "string",
|
|
151
151
|
"const": "catalog",
|
|
152
152
|
"visibility": "frontend"
|
|
153
|
+
},
|
|
154
|
+
"dangerouslyAllowClusterUrls": {
|
|
155
|
+
"type": "array",
|
|
156
|
+
"items": {
|
|
157
|
+
"type": "string"
|
|
158
|
+
},
|
|
159
|
+
"description": "Hostname patterns (for example `127.0.0.1` or `*.example.com`) for which catalog cluster API server URLs may use HTTP or non-public addresses. Omitted hostnames must use HTTPS and pass SSRF checks.",
|
|
160
|
+
"visibility": "frontend"
|
|
161
|
+
},
|
|
162
|
+
"dangerouslyAllowSkipTLSVerify": {
|
|
163
|
+
"type": "boolean",
|
|
164
|
+
"description": "When true, catalog cluster entities may set skip TLS verify via annotation. Defaults to false.",
|
|
165
|
+
"visibility": "frontend"
|
|
153
166
|
}
|
|
154
167
|
},
|
|
155
168
|
"required": [
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var catalogClient = require('@backstage/catalog-client');
|
|
4
4
|
var pluginKubernetesCommon = require('@backstage/plugin-kubernetes-common');
|
|
5
|
+
var catalogClusterAuthMetadata = require('./catalogClusterAuthMetadata.cjs.js');
|
|
6
|
+
var CatalogClusterLocatorOptions = require('./CatalogClusterLocatorOptions.cjs.js');
|
|
7
|
+
var validateClusterApiServerUrl = require('./validateClusterApiServerUrl.cjs.js');
|
|
5
8
|
|
|
6
9
|
function isObject(obj) {
|
|
7
10
|
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
|
@@ -9,12 +12,21 @@ function isObject(obj) {
|
|
|
9
12
|
class CatalogClusterLocator {
|
|
10
13
|
catalogService;
|
|
11
14
|
auth;
|
|
12
|
-
|
|
15
|
+
options;
|
|
16
|
+
logger;
|
|
17
|
+
constructor(catalogService, auth, options, logger) {
|
|
13
18
|
this.catalogService = catalogService;
|
|
14
19
|
this.auth = auth;
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.logger = logger;
|
|
15
22
|
}
|
|
16
|
-
static fromConfig(catalogApi, auth) {
|
|
17
|
-
return new CatalogClusterLocator(
|
|
23
|
+
static fromConfig(catalogApi, auth, clusterLocatorConfig, logger) {
|
|
24
|
+
return new CatalogClusterLocator(
|
|
25
|
+
catalogApi,
|
|
26
|
+
auth,
|
|
27
|
+
CatalogClusterLocatorOptions.readCatalogClusterLocatorOptions(clusterLocatorConfig),
|
|
28
|
+
logger
|
|
29
|
+
);
|
|
18
30
|
}
|
|
19
31
|
async getClusters(options) {
|
|
20
32
|
const apiServerKey = `metadata.annotations.${pluginKubernetesCommon.ANNOTATION_KUBERNETES_API_SERVER}`;
|
|
@@ -35,28 +47,55 @@ class CatalogClusterLocator {
|
|
|
35
47
|
credentials: options?.credentials ?? await this.auth.getNoneCredentials()
|
|
36
48
|
}
|
|
37
49
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
`Invalid cluster '${entity.metadata.name}': authProvider 'serviceAccount' is not supported by the catalog cluster locator`
|
|
44
|
-
);
|
|
50
|
+
const clusterDetails = [];
|
|
51
|
+
for (const entity of clusters.items) {
|
|
52
|
+
const details = await this.toClusterDetails(entity);
|
|
53
|
+
if (details) {
|
|
54
|
+
clusterDetails.push(details);
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
}
|
|
57
|
+
return clusterDetails;
|
|
58
|
+
}
|
|
59
|
+
async toClusterDetails(entity) {
|
|
60
|
+
const name = entity.metadata.name;
|
|
61
|
+
const annotations = entity.metadata.annotations;
|
|
62
|
+
if (!annotations) {
|
|
63
|
+
this.logger.warn(
|
|
64
|
+
`Ignoring kubernetes-cluster Resource "${name}" without annotations`
|
|
65
|
+
);
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
const authProvider = annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
69
|
+
if (authProvider === "serviceAccount") {
|
|
70
|
+
this.logger.warn(
|
|
71
|
+
`Ignoring kubernetes-cluster Resource "${name}": catalog cluster locator does not support the serviceAccount auth provider`
|
|
72
|
+
);
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
const apiServerUrl = annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_API_SERVER];
|
|
76
|
+
try {
|
|
77
|
+
await validateClusterApiServerUrl.validateClusterApiServerUrl(apiServerUrl, this.options);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.logger.warn(
|
|
80
|
+
`Ignoring kubernetes-cluster Resource "${name}": ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const allowSkipTlsVerify = this.options.dangerouslyAllowSkipTLSVerify ?? false;
|
|
85
|
+
const skipTLSVerify = allowSkipTlsVerify && annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY] === "true";
|
|
86
|
+
const clusterDetails = {
|
|
87
|
+
name,
|
|
88
|
+
title: entity.metadata.title,
|
|
89
|
+
url: apiServerUrl,
|
|
90
|
+
authMetadata: catalogClusterAuthMetadata.filterCatalogClusterAuthMetadata(annotations),
|
|
91
|
+
caData: annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_API_SERVER_CA],
|
|
92
|
+
skipMetricsLookup: annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP] === "true",
|
|
93
|
+
skipTLSVerify,
|
|
94
|
+
dashboardUrl: annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_DASHBOARD_URL],
|
|
95
|
+
dashboardApp: annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_DASHBOARD_APP],
|
|
96
|
+
dashboardParameters: this.getDashboardParameters(annotations)
|
|
97
|
+
};
|
|
98
|
+
return clusterDetails;
|
|
60
99
|
}
|
|
61
100
|
getDashboardParameters(annotations) {
|
|
62
101
|
const dashboardParamsString = annotations[pluginKubernetesCommon.ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogClusterLocator.cjs.js","sources":["../../src/cluster-locator/CatalogClusterLocator.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n} from '@backstage/backend-plugin-api';\nimport {\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\nimport { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';\nimport {\n ANNOTATION_KUBERNETES_API_SERVER,\n ANNOTATION_KUBERNETES_API_SERVER_CA,\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP,\n ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY,\n ANNOTATION_KUBERNETES_DASHBOARD_URL,\n ANNOTATION_KUBERNETES_DASHBOARD_APP,\n ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS,\n} from '@backstage/plugin-kubernetes-common';\nimport { JsonObject } from '@backstage/types';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nfunction isObject(obj: unknown): obj is JsonObject {\n return typeof obj === 'object' && obj !== null && !Array.isArray(obj);\n}\n\nexport class CatalogClusterLocator implements KubernetesClustersSupplier {\n private catalogService: CatalogService;\n private auth: AuthService;\n\n constructor(catalogService: CatalogService, auth: AuthService) {\n this.catalogService = catalogService;\n this.auth = auth;\n }\n\n static fromConfig(\n catalogApi: CatalogService,\n auth: AuthService,\n ): CatalogClusterLocator {\n return new CatalogClusterLocator(catalogApi, auth);\n }\n\n async getClusters(options?: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const apiServerKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER}`;\n const apiServerCaKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER_CA}`;\n const authProviderKey = `metadata.annotations.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}`;\n\n const filter: Record<string, symbol | string> = {\n kind: 'Resource',\n 'spec.type': 'kubernetes-cluster',\n [apiServerKey]: CATALOG_FILTER_EXISTS,\n [apiServerCaKey]: CATALOG_FILTER_EXISTS,\n [authProviderKey]: CATALOG_FILTER_EXISTS,\n };\n\n const clusters = await this.catalogService.getEntities(\n {\n filter: [filter],\n },\n {\n credentials:\n options?.credentials ?? (await this.auth.getNoneCredentials()),\n },\n );\n return clusters.items.map(entity => {\n const annotations = entity.metadata.annotations!;\n const authProvider = annotations[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n if (authProvider === 'serviceAccount') {\n throw new Error(\n `Invalid cluster '${entity.metadata.name}': authProvider 'serviceAccount' is not supported by the catalog cluster locator`,\n );\n }\n\n const clusterDetails: ClusterDetails = {\n name: entity.metadata.name,\n title: entity.metadata.title,\n url: annotations[ANNOTATION_KUBERNETES_API_SERVER],\n authMetadata: annotations,\n caData: annotations[ANNOTATION_KUBERNETES_API_SERVER_CA],\n skipMetricsLookup:\n annotations[ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP] === 'true',\n skipTLSVerify:\n annotations[ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY] === 'true',\n dashboardUrl: annotations[ANNOTATION_KUBERNETES_DASHBOARD_URL],\n dashboardApp: annotations[ANNOTATION_KUBERNETES_DASHBOARD_APP],\n dashboardParameters: this.getDashboardParameters(annotations),\n };\n\n return clusterDetails;\n });\n }\n\n private getDashboardParameters(\n annotations: Record<string, string>,\n ): JsonObject | undefined {\n const dashboardParamsString =\n annotations[ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS];\n if (dashboardParamsString) {\n try {\n const dashboardParams = JSON.parse(dashboardParamsString);\n return isObject(dashboardParams) ? dashboardParams : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n}\n"],"names":["ANNOTATION_KUBERNETES_API_SERVER","ANNOTATION_KUBERNETES_API_SERVER_CA","ANNOTATION_KUBERNETES_AUTH_PROVIDER","CATALOG_FILTER_EXISTS","ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP","ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY","ANNOTATION_KUBERNETES_DASHBOARD_URL","ANNOTATION_KUBERNETES_DASHBOARD_APP","ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogClusterLocator.cjs.js","sources":["../../src/cluster-locator/CatalogClusterLocator.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport {\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\nimport { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';\nimport {\n ANNOTATION_KUBERNETES_API_SERVER,\n ANNOTATION_KUBERNETES_API_SERVER_CA,\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP,\n ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY,\n ANNOTATION_KUBERNETES_DASHBOARD_URL,\n ANNOTATION_KUBERNETES_DASHBOARD_APP,\n ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS,\n} from '@backstage/plugin-kubernetes-common';\nimport { JsonObject } from '@backstage/types';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { filterCatalogClusterAuthMetadata } from './catalogClusterAuthMetadata';\nimport {\n CatalogClusterLocatorOptions,\n readCatalogClusterLocatorOptions,\n} from './CatalogClusterLocatorOptions';\nimport { validateClusterApiServerUrl } from './validateClusterApiServerUrl';\n\nfunction isObject(obj: unknown): obj is JsonObject {\n return typeof obj === 'object' && obj !== null && !Array.isArray(obj);\n}\n\nexport class CatalogClusterLocator implements KubernetesClustersSupplier {\n private catalogService: CatalogService;\n private auth: AuthService;\n private readonly options: CatalogClusterLocatorOptions;\n private readonly logger: LoggerService;\n\n constructor(\n catalogService: CatalogService,\n auth: AuthService,\n options: CatalogClusterLocatorOptions,\n logger: LoggerService,\n ) {\n this.catalogService = catalogService;\n this.auth = auth;\n this.options = options;\n this.logger = logger;\n }\n\n static fromConfig(\n catalogApi: CatalogService,\n auth: AuthService,\n clusterLocatorConfig: Config,\n logger: LoggerService,\n ): CatalogClusterLocator {\n return new CatalogClusterLocator(\n catalogApi,\n auth,\n readCatalogClusterLocatorOptions(clusterLocatorConfig),\n logger,\n );\n }\n\n async getClusters(options?: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const apiServerKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER}`;\n const apiServerCaKey = `metadata.annotations.${ANNOTATION_KUBERNETES_API_SERVER_CA}`;\n const authProviderKey = `metadata.annotations.${ANNOTATION_KUBERNETES_AUTH_PROVIDER}`;\n\n const filter: Record<string, symbol | string> = {\n kind: 'Resource',\n 'spec.type': 'kubernetes-cluster',\n [apiServerKey]: CATALOG_FILTER_EXISTS,\n [apiServerCaKey]: CATALOG_FILTER_EXISTS,\n [authProviderKey]: CATALOG_FILTER_EXISTS,\n };\n\n const clusters = await this.catalogService.getEntities(\n {\n filter: [filter],\n },\n {\n credentials:\n options?.credentials ?? (await this.auth.getNoneCredentials()),\n },\n );\n\n const clusterDetails: ClusterDetails[] = [];\n for (const entity of clusters.items) {\n const details = await this.toClusterDetails(entity);\n if (details) {\n clusterDetails.push(details);\n }\n }\n return clusterDetails;\n }\n\n private async toClusterDetails(\n entity: Awaited<ReturnType<CatalogService['getEntities']>>['items'][number],\n ): Promise<ClusterDetails | undefined> {\n const name = entity.metadata.name;\n const annotations = entity.metadata.annotations;\n if (!annotations) {\n this.logger.warn(\n `Ignoring kubernetes-cluster Resource \"${name}\" without annotations`,\n );\n return undefined;\n }\n\n const authProvider = annotations[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n if (authProvider === 'serviceAccount') {\n this.logger.warn(\n `Ignoring kubernetes-cluster Resource \"${name}\": catalog cluster locator does not support the serviceAccount auth provider`,\n );\n return undefined;\n }\n\n const apiServerUrl = annotations[ANNOTATION_KUBERNETES_API_SERVER];\n try {\n await validateClusterApiServerUrl(apiServerUrl, this.options);\n } catch (error) {\n this.logger.warn(\n `Ignoring kubernetes-cluster Resource \"${name}\": ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return undefined;\n }\n\n const allowSkipTlsVerify =\n this.options.dangerouslyAllowSkipTLSVerify ?? false;\n const skipTLSVerify =\n allowSkipTlsVerify &&\n annotations[ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY] === 'true';\n\n const clusterDetails: ClusterDetails = {\n name,\n title: entity.metadata.title,\n url: apiServerUrl,\n authMetadata: filterCatalogClusterAuthMetadata(annotations),\n caData: annotations[ANNOTATION_KUBERNETES_API_SERVER_CA],\n skipMetricsLookup:\n annotations[ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP] === 'true',\n skipTLSVerify,\n dashboardUrl: annotations[ANNOTATION_KUBERNETES_DASHBOARD_URL],\n dashboardApp: annotations[ANNOTATION_KUBERNETES_DASHBOARD_APP],\n dashboardParameters: this.getDashboardParameters(annotations),\n };\n\n return clusterDetails;\n }\n\n private getDashboardParameters(\n annotations: Record<string, string>,\n ): JsonObject | undefined {\n const dashboardParamsString =\n annotations[ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS];\n if (dashboardParamsString) {\n try {\n const dashboardParams = JSON.parse(dashboardParamsString);\n return isObject(dashboardParams) ? dashboardParams : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n}\n"],"names":["readCatalogClusterLocatorOptions","ANNOTATION_KUBERNETES_API_SERVER","ANNOTATION_KUBERNETES_API_SERVER_CA","ANNOTATION_KUBERNETES_AUTH_PROVIDER","CATALOG_FILTER_EXISTS","validateClusterApiServerUrl","ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY","filterCatalogClusterAuthMetadata","ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP","ANNOTATION_KUBERNETES_DASHBOARD_URL","ANNOTATION_KUBERNETES_DASHBOARD_APP","ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function readCatalogClusterLocatorOptions(config) {
|
|
4
|
+
return {
|
|
5
|
+
dangerouslyAllowClusterUrls: config.getOptionalStringArray(
|
|
6
|
+
"dangerouslyAllowClusterUrls"
|
|
7
|
+
),
|
|
8
|
+
dangerouslyAllowSkipTLSVerify: config.getOptionalBoolean(
|
|
9
|
+
"dangerouslyAllowSkipTLSVerify"
|
|
10
|
+
)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.readCatalogClusterLocatorOptions = readCatalogClusterLocatorOptions;
|
|
15
|
+
//# sourceMappingURL=CatalogClusterLocatorOptions.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CatalogClusterLocatorOptions.cjs.js","sources":["../../src/cluster-locator/CatalogClusterLocatorOptions.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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 { Config } from '@backstage/config';\nimport { ValidateClusterApiServerUrlOptions } from './validateClusterApiServerUrl';\n\nexport type CatalogClusterLocatorOptions =\n ValidateClusterApiServerUrlOptions & {\n dangerouslyAllowSkipTLSVerify?: boolean;\n };\n\nexport function readCatalogClusterLocatorOptions(\n config: Config,\n): CatalogClusterLocatorOptions {\n return {\n dangerouslyAllowClusterUrls: config.getOptionalStringArray(\n 'dangerouslyAllowClusterUrls',\n ),\n dangerouslyAllowSkipTLSVerify: config.getOptionalBoolean(\n 'dangerouslyAllowSkipTLSVerify',\n ),\n };\n}\n"],"names":[],"mappings":";;AAwBO,SAAS,iCACd,MAAA,EAC8B;AAC9B,EAAA,OAAO;AAAA,IACL,6BAA6B,MAAA,CAAO,sBAAA;AAAA,MAClC;AAAA,KACF;AAAA,IACA,+BAA+B,MAAA,CAAO,kBAAA;AAAA,MACpC;AAAA;AACF,GACF;AACF;;;;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const KUBERNETES_IO_PREFIX = "kubernetes.io/";
|
|
4
|
+
const BLOCKED_AUTH_METADATA_KEYS = /* @__PURE__ */ new Set(["serviceAccountToken"]);
|
|
5
|
+
function filterCatalogClusterAuthMetadata(annotations) {
|
|
6
|
+
const filtered = {};
|
|
7
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
8
|
+
if (BLOCKED_AUTH_METADATA_KEYS.has(key)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (key.startsWith(KUBERNETES_IO_PREFIX)) {
|
|
12
|
+
filtered[key] = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return filtered;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
exports.filterCatalogClusterAuthMetadata = filterCatalogClusterAuthMetadata;
|
|
19
|
+
//# sourceMappingURL=catalogClusterAuthMetadata.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogClusterAuthMetadata.cjs.js","sources":["../../src/cluster-locator/catalogClusterAuthMetadata.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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\nconst KUBERNETES_IO_PREFIX = 'kubernetes.io/';\n\nconst BLOCKED_AUTH_METADATA_KEYS = new Set(['serviceAccountToken']);\n\nexport function filterCatalogClusterAuthMetadata(\n annotations: Record<string, string>,\n): Record<string, string> {\n const filtered: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(annotations)) {\n if (BLOCKED_AUTH_METADATA_KEYS.has(key)) {\n continue;\n }\n if (key.startsWith(KUBERNETES_IO_PREFIX)) {\n filtered[key] = value;\n }\n }\n\n return filtered;\n}\n"],"names":[],"mappings":";;AAgBA,MAAM,oBAAA,GAAuB,gBAAA;AAE7B,MAAM,0BAAA,mBAA6B,IAAI,GAAA,CAAI,CAAC,qBAAqB,CAAC,CAAA;AAE3D,SAAS,iCACd,WAAA,EACwB;AACxB,EAAA,MAAM,WAAmC,EAAC;AAE1C,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACtD,IAAA,IAAI,0BAAA,CAA2B,GAAA,CAAI,GAAG,CAAA,EAAG;AACvC,MAAA;AAAA,IACF;AACA,IAAA,IAAI,GAAA,CAAI,UAAA,CAAW,oBAAoB,CAAA,EAAG;AACxC,MAAA,QAAA,CAAS,GAAG,CAAA,GAAI,KAAA;AAAA,IAClB;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;;;;"}
|
|
@@ -60,7 +60,12 @@ const getCombinedClusterSupplier = (rootConfig, catalogService, authStrategy, lo
|
|
|
60
60
|
const type = clusterLocatorMethod.getString("type");
|
|
61
61
|
switch (type) {
|
|
62
62
|
case "catalog":
|
|
63
|
-
return CatalogClusterLocator.CatalogClusterLocator.fromConfig(
|
|
63
|
+
return CatalogClusterLocator.CatalogClusterLocator.fromConfig(
|
|
64
|
+
catalogService,
|
|
65
|
+
auth,
|
|
66
|
+
clusterLocatorMethod,
|
|
67
|
+
logger
|
|
68
|
+
);
|
|
64
69
|
case "localKubectlProxy":
|
|
65
70
|
return new LocalKubectlProxyLocator.LocalKubectlProxyClusterLocator();
|
|
66
71
|
case "config":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../../src/cluster-locator/index.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\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 { Config } from '@backstage/config';\nimport { toError } from '@backstage/errors';\nimport { Duration } from 'luxon';\nimport { ConfigClusterLocator } from './ConfigClusterLocator';\nimport { GkeClusterLocator } from './GkeClusterLocator';\nimport { CatalogClusterLocator } from './CatalogClusterLocator';\nimport { LocalKubectlProxyClusterLocator } from './LocalKubectlProxyLocator';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nclass CombinedClustersSupplier implements KubernetesClustersSupplier {\n readonly clusterSuppliers: KubernetesClustersSupplier[];\n readonly logger: LoggerService;\n readonly continueOnError: boolean;\n\n constructor(\n clusterSuppliers: KubernetesClustersSupplier[],\n logger: LoggerService,\n continueOnError: boolean = false,\n ) {\n this.clusterSuppliers = clusterSuppliers;\n this.logger = logger;\n this.continueOnError = continueOnError;\n }\n\n async getClusters(options: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const clusters = this.continueOnError\n ? await this.getClustersSettled(options)\n : await Promise.all(\n this.clusterSuppliers.map(supplier => supplier.getClusters(options)),\n ).then(res => res.flat());\n return this.warnDuplicates(clusters);\n }\n\n private async getClustersSettled(options: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const results = await Promise.allSettled(\n this.clusterSuppliers.map(supplier => supplier.getClusters(options)),\n );\n const clusters: ClusterDetails[] = [];\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (result.status === 'fulfilled') {\n clusters.push(...result.value);\n } else {\n this.logger.error(\n `Failed to retrieve clusters from cluster locator method #${i + 1}`,\n toError(result.reason),\n );\n }\n }\n return clusters;\n }\n\n private warnDuplicates(clusters: ClusterDetails[]): ClusterDetails[] {\n const clusterNames = new Set<string>();\n const duplicatedNames = new Set<string>();\n for (const clusterName of clusters.map(c => c.name)) {\n if (clusterNames.has(clusterName)) {\n duplicatedNames.add(clusterName);\n } else {\n clusterNames.add(clusterName);\n }\n }\n for (const clusterName of duplicatedNames) {\n this.logger.warn(`Duplicate cluster name '${clusterName}'`);\n }\n return clusters;\n }\n}\n\nexport const getCombinedClusterSupplier = (\n rootConfig: Config,\n catalogService: CatalogService,\n authStrategy: AuthenticationStrategy,\n logger: LoggerService,\n refreshInterval: Duration | undefined = undefined,\n auth: AuthService,\n): KubernetesClustersSupplier => {\n const clusterSuppliers = rootConfig\n .getConfigArray('kubernetes.clusterLocatorMethods')\n .map(clusterLocatorMethod => {\n const type = clusterLocatorMethod.getString('type');\n switch (type) {\n case 'catalog':\n return CatalogClusterLocator.fromConfig(catalogService
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../../src/cluster-locator/index.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\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 { Config } from '@backstage/config';\nimport { toError } from '@backstage/errors';\nimport { Duration } from 'luxon';\nimport { ConfigClusterLocator } from './ConfigClusterLocator';\nimport { GkeClusterLocator } from './GkeClusterLocator';\nimport { CatalogClusterLocator } from './CatalogClusterLocator';\nimport { LocalKubectlProxyClusterLocator } from './LocalKubectlProxyLocator';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nclass CombinedClustersSupplier implements KubernetesClustersSupplier {\n readonly clusterSuppliers: KubernetesClustersSupplier[];\n readonly logger: LoggerService;\n readonly continueOnError: boolean;\n\n constructor(\n clusterSuppliers: KubernetesClustersSupplier[],\n logger: LoggerService,\n continueOnError: boolean = false,\n ) {\n this.clusterSuppliers = clusterSuppliers;\n this.logger = logger;\n this.continueOnError = continueOnError;\n }\n\n async getClusters(options: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const clusters = this.continueOnError\n ? await this.getClustersSettled(options)\n : await Promise.all(\n this.clusterSuppliers.map(supplier => supplier.getClusters(options)),\n ).then(res => res.flat());\n return this.warnDuplicates(clusters);\n }\n\n private async getClustersSettled(options: {\n credentials: BackstageCredentials;\n }): Promise<ClusterDetails[]> {\n const results = await Promise.allSettled(\n this.clusterSuppliers.map(supplier => supplier.getClusters(options)),\n );\n const clusters: ClusterDetails[] = [];\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (result.status === 'fulfilled') {\n clusters.push(...result.value);\n } else {\n this.logger.error(\n `Failed to retrieve clusters from cluster locator method #${i + 1}`,\n toError(result.reason),\n );\n }\n }\n return clusters;\n }\n\n private warnDuplicates(clusters: ClusterDetails[]): ClusterDetails[] {\n const clusterNames = new Set<string>();\n const duplicatedNames = new Set<string>();\n for (const clusterName of clusters.map(c => c.name)) {\n if (clusterNames.has(clusterName)) {\n duplicatedNames.add(clusterName);\n } else {\n clusterNames.add(clusterName);\n }\n }\n for (const clusterName of duplicatedNames) {\n this.logger.warn(`Duplicate cluster name '${clusterName}'`);\n }\n return clusters;\n }\n}\n\nexport const getCombinedClusterSupplier = (\n rootConfig: Config,\n catalogService: CatalogService,\n authStrategy: AuthenticationStrategy,\n logger: LoggerService,\n refreshInterval: Duration | undefined = undefined,\n auth: AuthService,\n): KubernetesClustersSupplier => {\n const clusterSuppliers = rootConfig\n .getConfigArray('kubernetes.clusterLocatorMethods')\n .map(clusterLocatorMethod => {\n const type = clusterLocatorMethod.getString('type');\n switch (type) {\n case 'catalog':\n return CatalogClusterLocator.fromConfig(\n catalogService,\n auth,\n clusterLocatorMethod,\n logger,\n );\n case 'localKubectlProxy':\n return new LocalKubectlProxyClusterLocator();\n case 'config':\n return ConfigClusterLocator.fromConfig(\n clusterLocatorMethod,\n authStrategy,\n );\n case 'gke':\n return GkeClusterLocator.fromConfig(\n clusterLocatorMethod,\n logger,\n refreshInterval,\n );\n default:\n throw new Error(\n `Unsupported kubernetes.clusterLocatorMethods: \"${type}\"`,\n );\n }\n });\n\n const continueOnError =\n rootConfig.getOptionalBoolean('kubernetes.clusterLocatorContinueOnError') ??\n false;\n\n return new CombinedClustersSupplier(\n clusterSuppliers,\n logger,\n continueOnError,\n );\n};\n"],"names":["toError","CatalogClusterLocator","LocalKubectlProxyClusterLocator","ConfigClusterLocator","GkeClusterLocator"],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('node:dns/promises');
|
|
4
|
+
var ipaddr = require('ipaddr.js');
|
|
5
|
+
|
|
6
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var ipaddr__default = /*#__PURE__*/_interopDefaultCompat(ipaddr);
|
|
9
|
+
|
|
10
|
+
function isNonPublicIp(ip) {
|
|
11
|
+
try {
|
|
12
|
+
const addr = ipaddr__default.default.parse(ip);
|
|
13
|
+
return addr.range() !== "unicast";
|
|
14
|
+
} catch {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function hostnameMatchesPattern(hostname, pattern) {
|
|
19
|
+
const normalizedHost = hostname.toLowerCase();
|
|
20
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
21
|
+
if (normalizedPattern.startsWith("*.")) {
|
|
22
|
+
const suffix = normalizedPattern.slice(1);
|
|
23
|
+
const bare = normalizedPattern.slice(2);
|
|
24
|
+
return normalizedHost.endsWith(suffix) || normalizedHost === bare;
|
|
25
|
+
}
|
|
26
|
+
return normalizedHost === normalizedPattern;
|
|
27
|
+
}
|
|
28
|
+
function isHostnameDangerouslyAllowed(hostname, patterns) {
|
|
29
|
+
if (!patterns?.length) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return patterns.some((pattern) => hostnameMatchesPattern(hostname, pattern));
|
|
33
|
+
}
|
|
34
|
+
async function validateHostNotPrivate(hostname) {
|
|
35
|
+
const addresses = await promises.lookup(hostname, { all: true });
|
|
36
|
+
const nonPublic = addresses.find((addr) => isNonPublicIp(addr.address));
|
|
37
|
+
if (nonPublic) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Kubernetes cluster API server URL hostname "${hostname}" resolves to a non-public address`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function validateLiteralHost(hostname) {
|
|
44
|
+
if (ipaddr__default.default.isValid(hostname) || ipaddr__default.default.IPv6.isValid(hostname)) {
|
|
45
|
+
if (isNonPublicIp(hostname)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Kubernetes cluster API server URL must not use a non-public IP address"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function validateClusterApiServerUrl(apiServerUrl, options = {}) {
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(apiServerUrl);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error("Kubernetes cluster API server URL is not a valid URL");
|
|
58
|
+
}
|
|
59
|
+
if (url.username || url.password) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Kubernetes cluster API server URL must not contain credentials"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const hostname = url.hostname.toLowerCase();
|
|
65
|
+
const ssrfExempt = isHostnameDangerouslyAllowed(
|
|
66
|
+
hostname,
|
|
67
|
+
options.dangerouslyAllowClusterUrls
|
|
68
|
+
);
|
|
69
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Kubernetes cluster API server URL must use the HTTP or HTTPS scheme"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!ssrfExempt) {
|
|
75
|
+
if (url.protocol !== "https:") {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Kubernetes cluster API server URL must use the HTTPS scheme"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
validateLiteralHost(hostname);
|
|
81
|
+
if (!ipaddr__default.default.isValid(hostname) && !ipaddr__default.default.IPv6.isValid(hostname)) {
|
|
82
|
+
await validateHostNotPrivate(hostname);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return url;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.validateClusterApiServerUrl = validateClusterApiServerUrl;
|
|
89
|
+
//# sourceMappingURL=validateClusterApiServerUrl.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateClusterApiServerUrl.cjs.js","sources":["../../src/cluster-locator/validateClusterApiServerUrl.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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 { lookup } from 'node:dns/promises';\nimport ipaddr from 'ipaddr.js';\n\nexport type ValidateClusterApiServerUrlOptions = {\n /**\n * Hostname patterns (for example `127.0.0.1` or `*.example.com`) for which\n * catalog cluster API server URLs may use HTTP or non-public addresses.\n * Configured in app-config only; not controllable from catalog entities.\n */\n dangerouslyAllowClusterUrls?: string[];\n};\n\nfunction isNonPublicIp(ip: string): boolean {\n try {\n const addr = ipaddr.parse(ip);\n return addr.range() !== 'unicast';\n } catch {\n return true;\n }\n}\n\nfunction hostnameMatchesPattern(hostname: string, pattern: string): boolean {\n const normalizedHost = hostname.toLowerCase();\n const normalizedPattern = pattern.toLowerCase();\n\n if (normalizedPattern.startsWith('*.')) {\n const suffix = normalizedPattern.slice(1);\n const bare = normalizedPattern.slice(2);\n return normalizedHost.endsWith(suffix) || normalizedHost === bare;\n }\n\n return normalizedHost === normalizedPattern;\n}\n\nfunction isHostnameDangerouslyAllowed(\n hostname: string,\n patterns: string[] | undefined,\n): boolean {\n if (!patterns?.length) {\n return false;\n }\n return patterns.some(pattern => hostnameMatchesPattern(hostname, pattern));\n}\n\nasync function validateHostNotPrivate(hostname: string): Promise<void> {\n const addresses = await lookup(hostname, { all: true });\n const nonPublic = addresses.find(addr => isNonPublicIp(addr.address));\n if (nonPublic) {\n throw new Error(\n `Kubernetes cluster API server URL hostname \"${hostname}\" resolves to a non-public address`,\n );\n }\n}\n\nfunction validateLiteralHost(hostname: string): void {\n if (ipaddr.isValid(hostname) || ipaddr.IPv6.isValid(hostname)) {\n if (isNonPublicIp(hostname)) {\n throw new Error(\n 'Kubernetes cluster API server URL must not use a non-public IP address',\n );\n }\n }\n}\n\n/**\n * Validates a catalog-provided Kubernetes API server URL against SSRF protections.\n *\n * @throws when the URL is not permitted\n */\nexport async function validateClusterApiServerUrl(\n apiServerUrl: string,\n options: ValidateClusterApiServerUrlOptions = {},\n): Promise<URL> {\n let url: URL;\n try {\n url = new URL(apiServerUrl);\n } catch {\n throw new Error('Kubernetes cluster API server URL is not a valid URL');\n }\n\n if (url.username || url.password) {\n throw new Error(\n 'Kubernetes cluster API server URL must not contain credentials',\n );\n }\n\n const hostname = url.hostname.toLowerCase();\n const ssrfExempt = isHostnameDangerouslyAllowed(\n hostname,\n options.dangerouslyAllowClusterUrls,\n );\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new Error(\n 'Kubernetes cluster API server URL must use the HTTP or HTTPS scheme',\n );\n }\n\n if (!ssrfExempt) {\n if (url.protocol !== 'https:') {\n throw new Error(\n 'Kubernetes cluster API server URL must use the HTTPS scheme',\n );\n }\n\n validateLiteralHost(hostname);\n\n if (!ipaddr.isValid(hostname) && !ipaddr.IPv6.isValid(hostname)) {\n await validateHostNotPrivate(hostname);\n }\n }\n\n return url;\n}\n"],"names":["ipaddr","lookup"],"mappings":"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"}
|
package/dist/package.json.cjs.js
CHANGED
|
@@ -45,10 +45,15 @@ const statusCodeToErrorType = (statusCode) => {
|
|
|
45
45
|
};
|
|
46
46
|
class KubernetesConnection {
|
|
47
47
|
logger;
|
|
48
|
+
agentFactory;
|
|
48
49
|
agentCache = /* @__PURE__ */ new Map();
|
|
49
50
|
inClusterCache;
|
|
50
|
-
constructor({
|
|
51
|
+
constructor({
|
|
52
|
+
logger,
|
|
53
|
+
agentFactory
|
|
54
|
+
}) {
|
|
51
55
|
this.logger = logger;
|
|
56
|
+
this.agentFactory = agentFactory ?? ((options) => new https__namespace.Agent(options));
|
|
52
57
|
}
|
|
53
58
|
buildResourcePath(group, apiVersion, plural, namespace) {
|
|
54
59
|
const encode = (s) => encodeURIComponent(s);
|
|
@@ -140,7 +145,7 @@ class KubernetesConnection {
|
|
|
140
145
|
kc.loadFromCluster();
|
|
141
146
|
const cluster = kc.getCurrentCluster();
|
|
142
147
|
const url2 = new URL(cluster.server);
|
|
143
|
-
const agent2 = url2.protocol === "https:" ?
|
|
148
|
+
const agent2 = url2.protocol === "https:" ? this.agentFactory({
|
|
144
149
|
ca: fs__default.default.readFileSync(cluster.caFile),
|
|
145
150
|
keepAlive: true
|
|
146
151
|
}) : void 0;
|
|
@@ -163,7 +168,7 @@ class KubernetesConnection {
|
|
|
163
168
|
const key = this.buildAgentCacheKey(clusterDetails, credential);
|
|
164
169
|
let agent = this.agentCache.get(key);
|
|
165
170
|
if (!agent) {
|
|
166
|
-
agent =
|
|
171
|
+
agent = this.agentFactory({
|
|
167
172
|
ca,
|
|
168
173
|
rejectUnauthorized: !clusterDetails.skipTLSVerify,
|
|
169
174
|
keepAlive: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesConnection.cjs.js","sources":["../../src/service/KubernetesConnection.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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 type { Cluster } from '@kubernetes/client-node';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n KubernetesErrorTypes,\n KubernetesFetchError,\n} from '@backstage/plugin-kubernetes-common';\nimport fetch, { RequestInit, Response } from 'node-fetch';\nimport * as https from 'node:https';\nimport fs from 'fs-extra';\nimport {\n ClusterDetails,\n KubernetesCredential,\n} from '@backstage/plugin-kubernetes-node';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport const statusCodeToErrorType = (\n statusCode: number,\n): KubernetesErrorTypes => {\n switch (statusCode) {\n case 400:\n return 'BAD_REQUEST';\n case 401:\n return 'UNAUTHORIZED_ERROR';\n case 404:\n return 'NOT_FOUND';\n case 500:\n return 'SYSTEM_ERROR';\n default:\n return 'UNKNOWN_ERROR';\n }\n};\n\nexport type ConnectionResult =\n | { ok: true; url: URL; requestInit: RequestInit }\n | { ok: false; reason: 'missing_credentials' };\n\nexport class KubernetesConnection {\n private readonly logger: LoggerService;\n private readonly agentCache = new Map<string, https.Agent>();\n private inClusterCache:\n | { url: URL; agent: https.Agent | undefined }\n | undefined;\n\n constructor({ logger }: { logger: LoggerService }) {\n this.logger = logger;\n }\n\n buildResourcePath(\n group: string,\n apiVersion: string,\n plural: string,\n namespace?: string,\n ): string {\n const encode = (s: string) => encodeURIComponent(s);\n let path = group\n ? `/apis/${encode(group)}/${encode(apiVersion)}`\n : `/api/${encode(apiVersion)}`;\n if (namespace) {\n path += `/namespaces/${encode(namespace)}`;\n }\n path += `/${encode(plural)}`;\n return path;\n }\n\n async resolveConnection(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): Promise<ConnectionResult> {\n const authProvider =\n clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (this.isServiceAccountAuthentication(authProvider, clusterDetails)) {\n const [url, requestInit] = await this.fetchArgsInCluster(credential);\n return { ok: true, url, requestInit };\n } else if (!this.isCredentialMissing(authProvider, credential)) {\n const [url, requestInit] = await this.fetchArgs(\n clusterDetails,\n credential,\n );\n return { ok: true, url, requestInit };\n }\n return { ok: false, reason: 'missing_credentials' };\n }\n\n async fetchWithConnection(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n resourcePath: string,\n labelSelector?: string,\n ): Promise<Response> {\n const result = await this.resolveConnection(clusterDetails, credential);\n if (!result.ok) {\n return Promise.reject(\n new Error(\n `no bearer token or client cert for cluster '${clusterDetails.name}' and not running in Kubernetes`,\n ),\n );\n }\n\n const { url, requestInit } = result;\n if (url.pathname === '/') {\n url.pathname = resourcePath;\n } else {\n url.pathname += resourcePath;\n }\n\n if (labelSelector) {\n url.search = `labelSelector=${encodeURIComponent(labelSelector)}`;\n }\n\n return fetch(url, requestInit);\n }\n\n async handleUnsuccessfulResponse(\n clusterName: string,\n res: Response,\n ): Promise<KubernetesFetchError> {\n const resourcePath = new URL(res.url).pathname;\n this.logger.warn(\n `Received ${\n res.status\n } status when fetching \"${resourcePath}\" from cluster \"${clusterName}\"; body=[${await res.text()}]`,\n );\n return {\n errorType: statusCodeToErrorType(res.status),\n statusCode: res.status,\n resourcePath,\n };\n }\n\n private isServiceAccountAuthentication(\n authProvider: string,\n clusterDetails: ClusterDetails,\n ) {\n return (\n authProvider === 'serviceAccount' &&\n !clusterDetails.authMetadata.serviceAccountToken &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH)\n );\n }\n\n private isCredentialMissing(\n authProvider: string,\n credential: KubernetesCredential,\n ) {\n return (\n authProvider !== 'localKubectlProxy' && credential.type === 'anonymous'\n );\n }\n\n private buildRequestHeaders(\n credential: KubernetesCredential,\n ): Record<string, string> {\n return {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n ...(credential.type === 'bearer token' && {\n Authorization: `Bearer ${credential.token}`,\n }),\n };\n }\n\n private async fetchArgs(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): Promise<[URL, fetch.RequestInit]> {\n const { bufferFromFileOrString } = await import('@kubernetes/client-node');\n const requestInit: RequestInit = {\n method: 'GET',\n headers: this.buildRequestHeaders(credential),\n redirect: 'manual',\n };\n\n const url: URL = new URL(clusterDetails.url);\n if (url.protocol === 'https:') {\n const ca =\n bufferFromFileOrString(clusterDetails.caFile, clusterDetails.caData) ??\n undefined;\n requestInit.agent = this.getOrCreateAgent(clusterDetails, credential, ca);\n }\n return [url, requestInit];\n }\n\n private async fetchArgsInCluster(\n credential: KubernetesCredential,\n ): Promise<[URL, fetch.RequestInit]> {\n if (!this.inClusterCache) {\n const { KubeConfig } = await import('@kubernetes/client-node');\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const cluster = kc.getCurrentCluster() as Cluster;\n const url = new URL(cluster.server);\n const agent =\n url.protocol === 'https:'\n ? new https.Agent({\n ca: fs.readFileSync(cluster.caFile as string),\n keepAlive: true,\n })\n : undefined;\n this.inClusterCache = { url, agent };\n }\n\n const { url, agent } = this.inClusterCache;\n const requestInit: RequestInit = {\n method: 'GET',\n headers: this.buildRequestHeaders(credential),\n redirect: 'manual',\n ...(agent && { agent }),\n };\n return [new URL(url.toString()), requestInit];\n }\n\n private buildAgentCacheKey(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): string {\n const certPart =\n credential.type === 'x509 client certificate'\n ? `${credential.cert}|${credential.key}`\n : '';\n return `${clusterDetails.url}|${clusterDetails.skipTLSVerify ?? false}|${\n clusterDetails.caData ?? ''\n }|${clusterDetails.caFile ?? ''}|${certPart}`;\n }\n\n private getOrCreateAgent(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ca: Buffer | string | undefined,\n ): https.Agent {\n const key = this.buildAgentCacheKey(clusterDetails, credential);\n\n let agent = this.agentCache.get(key);\n if (!agent) {\n agent = new https.Agent({\n ca,\n rejectUnauthorized: !clusterDetails.skipTLSVerify,\n keepAlive: true,\n ...(credential.type === 'x509 client certificate' && {\n cert: credential.cert,\n key: credential.key,\n }),\n });\n this.agentCache.set(key, agent);\n }\n return agent;\n }\n}\n"],"names":["ANNOTATION_KUBERNETES_AUTH_PROVIDER","fetch","fs","SERVICEACCOUNT_CA_PATH","url","agent","https"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KubernetesConnection.cjs.js","sources":["../../src/service/KubernetesConnection.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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 type { Cluster } from '@kubernetes/client-node';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n KubernetesErrorTypes,\n KubernetesFetchError,\n} from '@backstage/plugin-kubernetes-common';\nimport fetch, { RequestInit, Response } from 'node-fetch';\nimport * as https from 'node:https';\nimport fs from 'fs-extra';\nimport {\n ClusterDetails,\n KubernetesCredential,\n} from '@backstage/plugin-kubernetes-node';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport const statusCodeToErrorType = (\n statusCode: number,\n): KubernetesErrorTypes => {\n switch (statusCode) {\n case 400:\n return 'BAD_REQUEST';\n case 401:\n return 'UNAUTHORIZED_ERROR';\n case 404:\n return 'NOT_FOUND';\n case 500:\n return 'SYSTEM_ERROR';\n default:\n return 'UNKNOWN_ERROR';\n }\n};\n\nexport type ConnectionResult =\n | { ok: true; url: URL; requestInit: RequestInit }\n | { ok: false; reason: 'missing_credentials' };\n\nexport class KubernetesConnection {\n private readonly logger: LoggerService;\n private readonly agentFactory: (options: https.AgentOptions) => https.Agent;\n private readonly agentCache = new Map<string, https.Agent>();\n private inClusterCache:\n | { url: URL; agent: https.Agent | undefined }\n | undefined;\n\n constructor({\n logger,\n agentFactory,\n }: {\n logger: LoggerService;\n agentFactory?: (options: https.AgentOptions) => https.Agent;\n }) {\n this.logger = logger;\n this.agentFactory = agentFactory ?? (options => new https.Agent(options));\n }\n\n buildResourcePath(\n group: string,\n apiVersion: string,\n plural: string,\n namespace?: string,\n ): string {\n const encode = (s: string) => encodeURIComponent(s);\n let path = group\n ? `/apis/${encode(group)}/${encode(apiVersion)}`\n : `/api/${encode(apiVersion)}`;\n if (namespace) {\n path += `/namespaces/${encode(namespace)}`;\n }\n path += `/${encode(plural)}`;\n return path;\n }\n\n async resolveConnection(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): Promise<ConnectionResult> {\n const authProvider =\n clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (this.isServiceAccountAuthentication(authProvider, clusterDetails)) {\n const [url, requestInit] = await this.fetchArgsInCluster(credential);\n return { ok: true, url, requestInit };\n } else if (!this.isCredentialMissing(authProvider, credential)) {\n const [url, requestInit] = await this.fetchArgs(\n clusterDetails,\n credential,\n );\n return { ok: true, url, requestInit };\n }\n return { ok: false, reason: 'missing_credentials' };\n }\n\n async fetchWithConnection(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n resourcePath: string,\n labelSelector?: string,\n ): Promise<Response> {\n const result = await this.resolveConnection(clusterDetails, credential);\n if (!result.ok) {\n return Promise.reject(\n new Error(\n `no bearer token or client cert for cluster '${clusterDetails.name}' and not running in Kubernetes`,\n ),\n );\n }\n\n const { url, requestInit } = result;\n if (url.pathname === '/') {\n url.pathname = resourcePath;\n } else {\n url.pathname += resourcePath;\n }\n\n if (labelSelector) {\n url.search = `labelSelector=${encodeURIComponent(labelSelector)}`;\n }\n\n return fetch(url, requestInit);\n }\n\n async handleUnsuccessfulResponse(\n clusterName: string,\n res: Response,\n ): Promise<KubernetesFetchError> {\n const resourcePath = new URL(res.url).pathname;\n this.logger.warn(\n `Received ${\n res.status\n } status when fetching \"${resourcePath}\" from cluster \"${clusterName}\"; body=[${await res.text()}]`,\n );\n return {\n errorType: statusCodeToErrorType(res.status),\n statusCode: res.status,\n resourcePath,\n };\n }\n\n private isServiceAccountAuthentication(\n authProvider: string,\n clusterDetails: ClusterDetails,\n ) {\n return (\n authProvider === 'serviceAccount' &&\n !clusterDetails.authMetadata.serviceAccountToken &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH)\n );\n }\n\n private isCredentialMissing(\n authProvider: string,\n credential: KubernetesCredential,\n ) {\n return (\n authProvider !== 'localKubectlProxy' && credential.type === 'anonymous'\n );\n }\n\n private buildRequestHeaders(\n credential: KubernetesCredential,\n ): Record<string, string> {\n return {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n ...(credential.type === 'bearer token' && {\n Authorization: `Bearer ${credential.token}`,\n }),\n };\n }\n\n private async fetchArgs(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): Promise<[URL, fetch.RequestInit]> {\n const { bufferFromFileOrString } = await import('@kubernetes/client-node');\n const requestInit: RequestInit = {\n method: 'GET',\n headers: this.buildRequestHeaders(credential),\n redirect: 'manual',\n };\n\n const url: URL = new URL(clusterDetails.url);\n if (url.protocol === 'https:') {\n const ca =\n bufferFromFileOrString(clusterDetails.caFile, clusterDetails.caData) ??\n undefined;\n requestInit.agent = this.getOrCreateAgent(clusterDetails, credential, ca);\n }\n return [url, requestInit];\n }\n\n private async fetchArgsInCluster(\n credential: KubernetesCredential,\n ): Promise<[URL, fetch.RequestInit]> {\n if (!this.inClusterCache) {\n const { KubeConfig } = await import('@kubernetes/client-node');\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const cluster = kc.getCurrentCluster() as Cluster;\n const url = new URL(cluster.server);\n const agent =\n url.protocol === 'https:'\n ? this.agentFactory({\n ca: fs.readFileSync(cluster.caFile as string),\n keepAlive: true,\n })\n : undefined;\n this.inClusterCache = { url, agent };\n }\n\n const { url, agent } = this.inClusterCache;\n const requestInit: RequestInit = {\n method: 'GET',\n headers: this.buildRequestHeaders(credential),\n redirect: 'manual',\n ...(agent && { agent }),\n };\n return [new URL(url.toString()), requestInit];\n }\n\n private buildAgentCacheKey(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ): string {\n const certPart =\n credential.type === 'x509 client certificate'\n ? `${credential.cert}|${credential.key}`\n : '';\n return `${clusterDetails.url}|${clusterDetails.skipTLSVerify ?? false}|${\n clusterDetails.caData ?? ''\n }|${clusterDetails.caFile ?? ''}|${certPart}`;\n }\n\n private getOrCreateAgent(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n ca: Buffer | string | undefined,\n ): https.Agent {\n const key = this.buildAgentCacheKey(clusterDetails, credential);\n\n let agent = this.agentCache.get(key);\n if (!agent) {\n agent = this.agentFactory({\n ca,\n rejectUnauthorized: !clusterDetails.skipTLSVerify,\n keepAlive: true,\n ...(credential.type === 'x509 client certificate' && {\n cert: credential.cert,\n key: credential.key,\n }),\n });\n this.agentCache.set(key, agent);\n }\n return agent;\n }\n}\n"],"names":["https","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fetch","fs","SERVICEACCOUNT_CA_PATH","url","agent"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-kubernetes-backend",
|
|
3
|
-
"version": "0.21.11-next.
|
|
3
|
+
"version": "0.21.11-next.2",
|
|
4
4
|
"description": "A Backstage backend plugin that integrates towards Kubernetes",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -43,23 +43,23 @@
|
|
|
43
43
|
"prepack": "backstage-cli package prepack",
|
|
44
44
|
"postpack": "backstage-cli package postpack",
|
|
45
45
|
"start": "backstage-cli package start",
|
|
46
|
-
"test": "backstage-cli package test"
|
|
46
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules backstage-cli package test"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@aws-crypto/sha256-js": "^5.0.0",
|
|
50
50
|
"@aws-sdk/credential-providers": "^3.350.0",
|
|
51
51
|
"@azure/identity": "^4.0.0",
|
|
52
|
-
"@backstage/backend-plugin-api": "1.10.1-next.
|
|
53
|
-
"@backstage/catalog-client": "1.16.2-next.
|
|
52
|
+
"@backstage/backend-plugin-api": "1.10.1-next.1",
|
|
53
|
+
"@backstage/catalog-client": "1.16.2-next.1",
|
|
54
54
|
"@backstage/catalog-model": "1.10.0",
|
|
55
|
-
"@backstage/config": "1.3.
|
|
55
|
+
"@backstage/config": "1.3.9-next.0",
|
|
56
56
|
"@backstage/errors": "1.3.1",
|
|
57
|
-
"@backstage/integration-aws-node": "0.2.
|
|
58
|
-
"@backstage/plugin-catalog-node": "2.2.5-next.
|
|
59
|
-
"@backstage/plugin-kubernetes-common": "0.9.13-next.
|
|
60
|
-
"@backstage/plugin-kubernetes-node": "0.4.8-next.
|
|
61
|
-
"@backstage/plugin-permission-common": "0.9.
|
|
62
|
-
"@backstage/plugin-permission-node": "0.11.4-next.
|
|
57
|
+
"@backstage/integration-aws-node": "0.2.2-next.0",
|
|
58
|
+
"@backstage/plugin-catalog-node": "2.2.5-next.1",
|
|
59
|
+
"@backstage/plugin-kubernetes-common": "0.9.13-next.1",
|
|
60
|
+
"@backstage/plugin-kubernetes-node": "0.4.8-next.2",
|
|
61
|
+
"@backstage/plugin-permission-common": "0.9.11-next.0",
|
|
62
|
+
"@backstage/plugin-permission-node": "0.11.4-next.1",
|
|
63
63
|
"@backstage/types": "1.2.2",
|
|
64
64
|
"@google-cloud/container": "^5.0.0",
|
|
65
65
|
"@jest-mock/express": "^2.0.1",
|
|
@@ -70,20 +70,21 @@
|
|
|
70
70
|
"express-promise-router": "^4.1.0",
|
|
71
71
|
"fs-extra": "^11.2.0",
|
|
72
72
|
"http-proxy-middleware": "^2.0.6",
|
|
73
|
+
"ipaddr.js": "^2.3.0",
|
|
73
74
|
"lodash": "^4.17.21",
|
|
74
75
|
"luxon": "^3.0.0",
|
|
75
76
|
"node-fetch": "^2.7.0",
|
|
76
77
|
"split2": "^4.2.0"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
|
-
"@backstage/backend-defaults": "0.17.9-next.
|
|
80
|
-
"@backstage/backend-test-utils": "1.11.7-next.
|
|
81
|
-
"@backstage/cli": "0.36.6-next.
|
|
82
|
-
"@backstage/plugin-permission-backend": "0.7.16-next.
|
|
83
|
-
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.23-next.
|
|
80
|
+
"@backstage/backend-defaults": "0.17.9-next.1",
|
|
81
|
+
"@backstage/backend-test-utils": "1.11.7-next.1",
|
|
82
|
+
"@backstage/cli": "0.36.6-next.1",
|
|
83
|
+
"@backstage/plugin-permission-backend": "0.7.16-next.1",
|
|
84
|
+
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.23-next.1",
|
|
84
85
|
"@types/express": "^4.17.6",
|
|
85
86
|
"@types/split2": "^4.2.0",
|
|
86
|
-
"msw": "^
|
|
87
|
+
"msw": "^2.0.0",
|
|
87
88
|
"supertest": "^7.0.0",
|
|
88
89
|
"ws": "^8.20.1"
|
|
89
90
|
},
|