@backstage/plugin-kubernetes-backend 0.21.7 → 0.21.9
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 +14 -0
- package/config.schema.json +13 -0
- package/dist/cluster-locator/CatalogClusterLocator.cjs.js +64 -19
- 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/KubernetesFetcher.cjs.js +20 -13
- package/dist/service/KubernetesFetcher.cjs.js.map +1 -1
- package/dist/service/KubernetesRouter.cjs.js +17 -2
- package/dist/service/KubernetesRouter.cjs.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @backstage/plugin-kubernetes-backend
|
|
2
2
|
|
|
3
|
+
## 0.21.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 89d4c81: 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.
|
|
8
|
+
|
|
9
|
+
Kubernetes Secret redaction on resource query endpoints now masks both `data` and `stringData` values regardless of how Secrets are fetched, including via the custom resources query path.
|
|
10
|
+
|
|
11
|
+
## 0.21.8
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 9d950fa: Improved entity resolution on the deprecated services endpoint.
|
|
16
|
+
|
|
3
17
|
## 0.21.7
|
|
4
18
|
|
|
5
19
|
### 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,22 +47,55 @@ class CatalogClusterLocator {
|
|
|
35
47
|
credentials: options?.credentials ?? await this.auth.getNoneCredentials()
|
|
36
48
|
}
|
|
37
49
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const clusterDetails = [];
|
|
51
|
+
for (const entity of clusters.items) {
|
|
52
|
+
const details = await this.toClusterDetails(entity);
|
|
53
|
+
if (details) {
|
|
54
|
+
clusterDetails.push(details);
|
|
55
|
+
}
|
|
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;
|
|
54
99
|
}
|
|
55
100
|
getDashboardParameters(annotations) {
|
|
56
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 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":";;;;;AAsCA,SAAS,SAAS,GAAA,EAAiC;AACjD,EAAA,OAAO,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,QAAQ,CAAC,KAAA,CAAM,QAAQ,GAAG,CAAA;AACtE;AAEO,MAAM,qBAAA,CAA4D;AAAA,EAC/D,cAAA;AAAA,EACA,IAAA;AAAA,EAER,WAAA,CAAY,gBAAgC,IAAA,EAAmB;AAC7D,IAAA,IAAA,CAAK,cAAA,GAAiB,cAAA;AACtB,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA,EAEA,OAAO,UAAA,CACL,UAAA,EACA,IAAA,EACuB;AACvB,IAAA,OAAO,IAAI,qBAAA,CAAsB,UAAA,EAAY,IAAI,CAAA;AAAA,EACnD;AAAA,EAEA,MAAM,YAAY,OAAA,EAEY;AAC5B,IAAA,MAAM,YAAA,GAAe,wBAAwBA,uDAAgC,CAAA,CAAA;AAC7E,IAAA,MAAM,cAAA,GAAiB,wBAAwBC,0DAAmC,CAAA,CAAA;AAClF,IAAA,MAAM,eAAA,GAAkB,wBAAwBC,0DAAmC,CAAA,CAAA;AAEnF,IAAA,MAAM,MAAA,GAA0C;AAAA,MAC9C,IAAA,EAAM,UAAA;AAAA,MACN,WAAA,EAAa,oBAAA;AAAA,MACb,CAAC,YAAY,GAAGC,mCAAA;AAAA,MAChB,CAAC,cAAc,GAAGA,mCAAA;AAAA,MAClB,CAAC,eAAe,GAAGA;AAAA,KACrB;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,cAAA,CAAe,WAAA;AAAA,MACzC;AAAA,QACE,MAAA,EAAQ,CAAC,MAAM;AAAA,OACjB;AAAA,MACA;AAAA,QACE,aACE,OAAA,EAAS,WAAA,IAAgB,MAAM,IAAA,CAAK,KAAK,kBAAA;AAAmB;AAChE,KACF;AACA,IAAA,OAAO,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,CAAA,MAAA,KAAU;AAClC,MAAA,MAAM,WAAA,GAAc,OAAO,QAAA,CAAS,WAAA;AACpC,MAAA,MAAM,cAAA,GAAiC;AAAA,QACrC,IAAA,EAAM,OAAO,QAAA,CAAS,IAAA;AAAA,QACtB,KAAA,EAAO,OAAO,QAAA,CAAS,KAAA;AAAA,QACvB,GAAA,EAAK,YAAYH,uDAAgC,CAAA;AAAA,QACjD,YAAA,EAAc,WAAA;AAAA,QACd,MAAA,EAAQ,YAAYC,0DAAmC,CAAA;AAAA,QACvD,iBAAA,EACE,WAAA,CAAYG,gEAAyC,CAAA,KAAM,MAAA;AAAA,QAC7D,aAAA,EACE,WAAA,CAAYC,4DAAqC,CAAA,KAAM,MAAA;AAAA,QACzD,YAAA,EAAc,YAAYC,0DAAmC,CAAA;AAAA,QAC7D,YAAA,EAAc,YAAYC,0DAAmC,CAAA;AAAA,QAC7D,mBAAA,EAAqB,IAAA,CAAK,sBAAA,CAAuB,WAAW;AAAA,OAC9D;AAEA,MAAA,OAAO,cAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,uBACN,WAAA,EACwB;AACxB,IAAA,MAAM,qBAAA,GACJ,YAAYC,iEAA0C,CAAA;AACxD,IAAA,IAAI,qBAAA,EAAuB;AACzB,MAAA,IAAI;AACF,QAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,qBAAqB,CAAA;AACxD,QAAA,OAAO,QAAA,CAAS,eAAe,CAAA,GAAI,eAAA,GAAkB,KAAA,CAAA;AAAA,MACvD,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,MAAA;AAAA,MACT;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;;"}
|
|
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
|
@@ -200,7 +200,8 @@ class KubernetesClientBasedFetcher {
|
|
|
200
200
|
const { bufferFromFileOrString } = await import('@kubernetes/client-node');
|
|
201
201
|
const requestInit = {
|
|
202
202
|
method: "GET",
|
|
203
|
-
headers: this.buildRequestHeaders(credential)
|
|
203
|
+
headers: this.buildRequestHeaders(credential),
|
|
204
|
+
redirect: "manual"
|
|
204
205
|
};
|
|
205
206
|
const url = new URL(clusterDetails.url);
|
|
206
207
|
if (url.protocol === "https:") {
|
|
@@ -226,6 +227,7 @@ class KubernetesClientBasedFetcher {
|
|
|
226
227
|
const requestInit = {
|
|
227
228
|
method: "GET",
|
|
228
229
|
headers: this.buildRequestHeaders(credential),
|
|
230
|
+
redirect: "manual",
|
|
229
231
|
...agent && { agent }
|
|
230
232
|
};
|
|
231
233
|
return [new URL(url.toString()), requestInit];
|
|
@@ -252,26 +254,31 @@ class KubernetesClientBasedFetcher {
|
|
|
252
254
|
return agent;
|
|
253
255
|
}
|
|
254
256
|
transformResources(objectType, kind, items) {
|
|
257
|
+
let result = items;
|
|
255
258
|
if (objectType === "customresources") {
|
|
256
|
-
|
|
259
|
+
const singularKind = kind.replace(/(List)$/, "");
|
|
260
|
+
result = result.map((item) => ({
|
|
257
261
|
...item,
|
|
258
|
-
kind:
|
|
262
|
+
kind: singularKind
|
|
259
263
|
}));
|
|
260
264
|
}
|
|
261
|
-
if (objectType === "secrets") {
|
|
262
|
-
|
|
265
|
+
if (objectType === "secrets" || kind && kind.replace(/List$/, "") === "Secret") {
|
|
266
|
+
result = result.map((item) => {
|
|
267
|
+
const redacted = { ...item };
|
|
263
268
|
if (item.data && typeof item.data === "object") {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
redacted.data = Object.fromEntries(
|
|
270
|
+
Object.keys(item.data).map((key) => [key, "***"])
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (item.stringData && typeof item.stringData === "object") {
|
|
274
|
+
redacted.stringData = Object.fromEntries(
|
|
275
|
+
Object.keys(item.stringData).map((key) => [key, "***"])
|
|
276
|
+
);
|
|
270
277
|
}
|
|
271
|
-
return
|
|
278
|
+
return redacted;
|
|
272
279
|
});
|
|
273
280
|
}
|
|
274
|
-
return
|
|
281
|
+
return result;
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesFetcher.cjs.js","sources":["../../src/service/KubernetesFetcher.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 type { Cluster, CoreV1Api, Metrics } from '@kubernetes/client-node';\nimport {\n FetchResponseWrapper,\n KubernetesFetcher,\n ObjectFetchParams,\n ObjectToFetch,\n} from '@backstage/plugin-kubernetes-node';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n FetchResponse,\n KubernetesErrorTypes,\n KubernetesFetchError,\n PodStatusFetchResponse,\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 { JsonObject } from '@backstage/types';\nimport {\n ClusterDetails,\n KubernetesCredential,\n} from '@backstage/plugin-kubernetes-node';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport interface KubernetesClientBasedFetcherOptions {\n logger: LoggerService;\n}\n\ntype FetchResult = FetchResponse | KubernetesFetchError;\n\nconst isError = (fr: FetchResult): fr is KubernetesFetchError =>\n fr.hasOwnProperty('errorType');\n\nfunction fetchResultsToResponseWrapper(\n results: FetchResult[],\n): FetchResponseWrapper {\n const errors: KubernetesFetchError[] = [];\n const responses: FetchResponse[] = [];\n for (const result of results) {\n if (isError(result)) {\n errors.push(result);\n } else {\n responses.push(result);\n }\n }\n return { errors, responses };\n}\n\nconst statusCodeToErrorType = (statusCode: number): 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 class KubernetesClientBasedFetcher implements KubernetesFetcher {\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 }: KubernetesClientBasedFetcherOptions) {\n this.logger = logger;\n }\n\n fetchObjectsForService(\n params: ObjectFetchParams,\n ): Promise<FetchResponseWrapper> {\n const fetchResults = Array.from(params.objectTypesToFetch)\n .concat(params.customResources)\n .map(({ objectType, group, apiVersion, plural }) =>\n this.fetchResource(\n params.clusterDetails,\n params.credential,\n { group, apiVersion, plural },\n params.namespace,\n params.labelSelector,\n ).then(\n (r: Response): Promise<FetchResult> =>\n r.ok\n ? r.json().then(\n ({ kind, items }): FetchResponse => ({\n type: objectType,\n resources: this.transformResources(objectType, kind, items),\n }),\n )\n : this.handleUnsuccessfulResponse(params.clusterDetails.name, r),\n ),\n );\n\n return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);\n }\n\n async fetchPodMetricsByNamespaces(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n namespaces: Set<string>,\n labelSelector?: string,\n ): Promise<FetchResponseWrapper> {\n const fetchResults = Array.from(namespaces).map(ns =>\n this.fetchPodMetricsForNamespace(\n clusterDetails,\n credential,\n ns,\n labelSelector,\n ),\n );\n\n return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);\n }\n\n private async fetchPodMetricsForNamespace(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n namespace: string,\n labelSelector?: string,\n ): Promise<FetchResult> {\n const [podMetrics, podList] = await Promise.all([\n this.fetchResource(\n clusterDetails,\n credential,\n { group: 'metrics.k8s.io', apiVersion: 'v1beta1', plural: 'pods' },\n namespace,\n labelSelector,\n ),\n this.fetchResource(\n clusterDetails,\n credential,\n { group: '', apiVersion: 'v1', plural: 'pods' },\n namespace,\n labelSelector,\n ),\n ]);\n if (podMetrics.ok && podList.ok) {\n const { topPods } = await import('@kubernetes/client-node');\n return topPods(\n {\n listPodForAllNamespaces: () => podList.json(),\n } as unknown as CoreV1Api,\n {\n getPodMetrics: () => podMetrics.json(),\n } as unknown as Metrics,\n ).then(\n (resources): PodStatusFetchResponse => ({\n type: 'podstatus',\n resources,\n }),\n );\n } else if (podMetrics.ok) {\n return this.handleUnsuccessfulResponse(clusterDetails.name, podList);\n }\n return this.handleUnsuccessfulResponse(clusterDetails.name, podMetrics);\n }\n\n private 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 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 private async fetchResource(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n resource: Pick<ObjectToFetch, 'group' | 'apiVersion' | 'plural'>,\n namespace?: string,\n labelSelector?: string,\n ): Promise<Response> {\n const resourcePath = this.buildResourcePath(\n resource.group,\n resource.apiVersion,\n resource.plural,\n namespace,\n );\n\n let url: URL;\n let requestInit: RequestInit;\n const authProvider =\n clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (this.isServiceAccountAuthentication(authProvider, clusterDetails)) {\n [url, requestInit] = await this.fetchArgsInCluster(credential);\n } else if (!this.isCredentialMissing(authProvider, credential)) {\n [url, requestInit] = await this.fetchArgs(clusterDetails, credential);\n } else {\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 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 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 };\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 ...(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 private transformResources(\n objectType: string,\n kind: string,\n items: JsonObject[],\n ): JsonObject[] {\n if (objectType === 'customresources') {\n return items.map((item: JsonObject) => ({\n ...item,\n kind: kind.replace(/(List)$/, ''),\n }));\n }\n\n if (objectType === 'secrets') {\n return items.map((item: JsonObject) => {\n if (item.data && typeof item.data === 'object') {\n return {\n ...item,\n data: Object.fromEntries(\n Object.keys(item.data).map(key => [key, '***']),\n ),\n };\n }\n return item;\n });\n }\n\n return items;\n }\n}\n"],"names":["ANNOTATION_KUBERNETES_AUTH_PROVIDER","fetch","fs","SERVICEACCOUNT_CA_PATH","url","agent","https"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAM,OAAA,GAAU,CAAC,EAAA,KACf,EAAA,CAAG,eAAe,WAAW,CAAA;AAE/B,SAAS,8BACP,OAAA,EACsB;AACtB,EAAA,MAAM,SAAiC,EAAC;AACxC,EAAA,MAAM,YAA6B,EAAC;AACpC,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,IAAI,OAAA,CAAQ,MAAM,CAAA,EAAG;AACnB,MAAA,MAAA,CAAO,KAAK,MAAM,CAAA;AAAA,IACpB,CAAA,MAAO;AACL,MAAA,SAAA,CAAU,KAAK,MAAM,CAAA;AAAA,IACvB;AAAA,EACF;AACA,EAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAC7B;AAEA,MAAM,qBAAA,GAAwB,CAAC,UAAA,KAA6C;AAC1E,EAAA,QAAQ,UAAA;AAAY,IAClB,KAAK,GAAA;AACH,MAAA,OAAO,aAAA;AAAA,IACT,KAAK,GAAA;AACH,MAAA,OAAO,oBAAA;AAAA,IACT,KAAK,GAAA;AACH,MAAA,OAAO,WAAA;AAAA,IACT,KAAK,GAAA;AACH,MAAA,OAAO,cAAA;AAAA,IACT;AACE,MAAA,OAAO,eAAA;AAAA;AAEb,CAAA;AAEO,MAAM,4BAAA,CAA0D;AAAA,EACpD,MAAA;AAAA,EACA,UAAA,uBAAiB,GAAA,EAAyB;AAAA,EACnD,cAAA;AAAA,EAIR,WAAA,CAAY,EAAE,MAAA,EAAO,EAAwC;AAC3D,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAAA,EAEA,uBACE,MAAA,EAC+B;AAC/B,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,MAAA,CAAO,kBAAkB,CAAA,CACtD,MAAA,CAAO,MAAA,CAAO,eAAe,CAAA,CAC7B,GAAA;AAAA,MAAI,CAAC,EAAE,UAAA,EAAY,OAAO,UAAA,EAAY,MAAA,OACrC,IAAA,CAAK,aAAA;AAAA,QACH,MAAA,CAAO,cAAA;AAAA,QACP,MAAA,CAAO,UAAA;AAAA,QACP,EAAE,KAAA,EAAO,UAAA,EAAY,MAAA,EAAO;AAAA,QAC5B,MAAA,CAAO,SAAA;AAAA,QACP,MAAA,CAAO;AAAA,OACT,CAAE,IAAA;AAAA,QACA,CAAC,CAAA,KACC,CAAA,CAAE,EAAA,GACE,CAAA,CAAE,MAAK,CAAE,IAAA;AAAA,UACP,CAAC,EAAE,IAAA,EAAM,KAAA,EAAM,MAAsB;AAAA,YACnC,IAAA,EAAM,UAAA;AAAA,YACN,SAAA,EAAW,IAAA,CAAK,kBAAA,CAAmB,UAAA,EAAY,MAAM,KAAK;AAAA,WAC5D;AAAA,YAEF,IAAA,CAAK,0BAAA,CAA2B,MAAA,CAAO,cAAA,CAAe,MAAM,CAAC;AAAA;AACrE,KACF;AAEF,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,CAAE,KAAK,6BAA6B,CAAA;AAAA,EACrE;AAAA,EAEA,MAAM,2BAAA,CACJ,cAAA,EACA,UAAA,EACA,YACA,aAAA,EAC+B;AAC/B,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,IAAA,CAAK,UAAU,CAAA,CAAE,GAAA;AAAA,MAAI,QAC9C,IAAA,CAAK,2BAAA;AAAA,QACH,cAAA;AAAA,QACA,UAAA;AAAA,QACA,EAAA;AAAA,QACA;AAAA;AACF,KACF;AAEA,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,CAAE,KAAK,6BAA6B,CAAA;AAAA,EACrE;AAAA,EAEA,MAAc,2BAAA,CACZ,cAAA,EACA,UAAA,EACA,WACA,aAAA,EACsB;AACtB,IAAA,MAAM,CAAC,UAAA,EAAY,OAAO,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC9C,IAAA,CAAK,aAAA;AAAA,QACH,cAAA;AAAA,QACA,UAAA;AAAA,QACA,EAAE,KAAA,EAAO,gBAAA,EAAkB,UAAA,EAAY,SAAA,EAAW,QAAQ,MAAA,EAAO;AAAA,QACjE,SAAA;AAAA,QACA;AAAA,OACF;AAAA,MACA,IAAA,CAAK,aAAA;AAAA,QACH,cAAA;AAAA,QACA,UAAA;AAAA,QACA,EAAE,KAAA,EAAO,EAAA,EAAI,UAAA,EAAY,IAAA,EAAM,QAAQ,MAAA,EAAO;AAAA,QAC9C,SAAA;AAAA,QACA;AAAA;AACF,KACD,CAAA;AACD,IAAA,IAAI,UAAA,CAAW,EAAA,IAAM,OAAA,CAAQ,EAAA,EAAI;AAC/B,MAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAM,OAAO,yBAAyB,CAAA;AAC1D,MAAA,OAAO,OAAA;AAAA,QACL;AAAA,UACE,uBAAA,EAAyB,MAAM,OAAA,CAAQ,IAAA;AAAK,SAC9C;AAAA,QACA;AAAA,UACE,aAAA,EAAe,MAAM,UAAA,CAAW,IAAA;AAAK;AACvC,OACF,CAAE,IAAA;AAAA,QACA,CAAC,SAAA,MAAuC;AAAA,UACtC,IAAA,EAAM,WAAA;AAAA,UACN;AAAA,SACF;AAAA,OACF;AAAA,IACF,CAAA,MAAA,IAAW,WAAW,EAAA,EAAI;AACxB,MAAA,OAAO,IAAA,CAAK,0BAAA,CAA2B,cAAA,CAAe,IAAA,EAAM,OAAO,CAAA;AAAA,IACrE;AACA,IAAA,OAAO,IAAA,CAAK,0BAAA,CAA2B,cAAA,CAAe,IAAA,EAAM,UAAU,CAAA;AAAA,EACxE;AAAA,EAEA,MAAc,0BAAA,CACZ,WAAA,EACA,GAAA,EAC+B;AAC/B,IAAA,MAAM,YAAA,GAAe,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,CAAE,QAAA;AACtC,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,MACV,CAAA,SAAA,EACE,GAAA,CAAI,MACN,CAAA,uBAAA,EAA0B,YAAY,CAAA,gBAAA,EAAmB,WAAW,CAAA,SAAA,EAAY,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAA;AAAA,KAClG;AACA,IAAA,OAAO;AAAA,MACL,SAAA,EAAW,qBAAA,CAAsB,GAAA,CAAI,MAAM,CAAA;AAAA,MAC3C,YAAY,GAAA,CAAI,MAAA;AAAA,MAChB;AAAA,KACF;AAAA,EACF;AAAA,EAEQ,iBAAA,CACN,KAAA,EACA,UAAA,EACA,MAAA,EACA,SAAA,EACQ;AACR,IAAA,MAAM,MAAA,GAAS,CAAC,CAAA,KAAc,kBAAA,CAAmB,CAAC,CAAA;AAClD,IAAA,IAAI,IAAA,GAAO,KAAA,GACP,CAAA,MAAA,EAAS,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,UAAU,CAAC,CAAA,CAAA,GAC5C,CAAA,KAAA,EAAQ,MAAA,CAAO,UAAU,CAAC,CAAA,CAAA;AAC9B,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,IAAA,IAAQ,CAAA,YAAA,EAAe,MAAA,CAAO,SAAS,CAAC,CAAA,CAAA;AAAA,IAC1C;AACA,IAAA,IAAA,IAAQ,CAAA,CAAA,EAAI,MAAA,CAAO,MAAM,CAAC,CAAA,CAAA;AAC1B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAc,aAAA,CACZ,cAAA,EACA,UAAA,EACA,QAAA,EACA,WACA,aAAA,EACmB;AACnB,IAAA,MAAM,eAAe,IAAA,CAAK,iBAAA;AAAA,MACxB,QAAA,CAAS,KAAA;AAAA,MACT,QAAA,CAAS,UAAA;AAAA,MACT,QAAA,CAAS,MAAA;AAAA,MACT;AAAA,KACF;AAEA,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI,WAAA;AACJ,IAAA,MAAM,YAAA,GACJ,cAAA,CAAe,YAAA,CAAaA,0DAAmC,CAAA;AAEjE,IAAA,IAAI,IAAA,CAAK,8BAAA,CAA+B,YAAA,EAAc,cAAc,CAAA,EAAG;AACrE,MAAA,CAAC,KAAK,WAAW,CAAA,GAAI,MAAM,IAAA,CAAK,mBAAmB,UAAU,CAAA;AAAA,IAC/D,WAAW,CAAC,IAAA,CAAK,mBAAA,CAAoB,YAAA,EAAc,UAAU,CAAA,EAAG;AAC9D,MAAA,CAAC,KAAK,WAAW,CAAA,GAAI,MAAM,IAAA,CAAK,SAAA,CAAU,gBAAgB,UAAU,CAAA;AAAA,IACtE,CAAA,MAAO;AACL,MAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,QACb,IAAI,KAAA;AAAA,UACF,CAAA,4CAAA,EAA+C,eAAe,IAAI,CAAA,+BAAA;AAAA;AACpE,OACF;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,CAAI,aAAa,GAAA,EAAK;AACxB,MAAA,GAAA,CAAI,QAAA,GAAW,YAAA;AAAA,IACjB,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,QAAA,IAAY,YAAA;AAAA,IAClB;AAEA,IAAA,IAAI,aAAA,EAAe;AACjB,MAAA,GAAA,CAAI,MAAA,GAAS,CAAA,cAAA,EAAiB,kBAAA,CAAmB,aAAa,CAAC,CAAA,CAAA;AAAA,IACjE;AAEA,IAAA,OAAOC,sBAAA,CAAM,KAAK,WAAW,CAAA;AAAA,EAC/B;AAAA,EAEQ,8BAAA,CACN,cACA,cAAA,EACA;AACA,IAAA,OACE,YAAA,KAAiB,oBACjB,CAAC,cAAA,CAAe,aAAa,mBAAA,IAC7BC,mBAAA,CAAG,eAAeC,6CAAsB,CAAA;AAAA,EAE5C;AAAA,EAEQ,mBAAA,CACN,cACA,UAAA,EACA;AACA,IAAA,OACE,YAAA,KAAiB,mBAAA,IAAuB,UAAA,CAAW,IAAA,KAAS,WAAA;AAAA,EAEhE;AAAA,EAEQ,oBACN,UAAA,EACwB;AACxB,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,kBAAA;AAAA,MACR,cAAA,EAAgB,kBAAA;AAAA,MAChB,GAAI,UAAA,CAAW,IAAA,KAAS,cAAA,IAAkB;AAAA,QACxC,aAAA,EAAe,CAAA,OAAA,EAAU,UAAA,CAAW,KAAK,CAAA;AAAA;AAC3C,KACF;AAAA,EACF;AAAA,EAEA,MAAc,SAAA,CACZ,cAAA,EACA,UAAA,EACmC;AACnC,IAAA,MAAM,EAAE,sBAAA,EAAuB,GAAI,MAAM,OAAO,yBAAyB,CAAA;AACzE,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC/B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS,IAAA,CAAK,mBAAA,CAAoB,UAAU;AAAA,KAC9C;AAEA,IAAA,MAAM,GAAA,GAAW,IAAI,GAAA,CAAI,cAAA,CAAe,GAAG,CAAA;AAC3C,IAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC7B,MAAA,MAAM,KACJ,sBAAA,CAAuB,cAAA,CAAe,MAAA,EAAQ,cAAA,CAAe,MAAM,CAAA,IACnE,MAAA;AACF,MAAA,WAAA,CAAY,KAAA,GAAQ,IAAA,CAAK,gBAAA,CAAiB,cAAA,EAAgB,YAAY,EAAE,CAAA;AAAA,IAC1E;AACA,IAAA,OAAO,CAAC,KAAK,WAAW,CAAA;AAAA,EAC1B;AAAA,EAEA,MAAc,mBACZ,UAAA,EACmC;AACnC,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AACxB,MAAA,MAAM,EAAE,UAAA,EAAW,GAAI,MAAM,OAAO,yBAAyB,CAAA;AAC7D,MAAA,MAAM,EAAA,GAAK,IAAI,UAAA,EAAW;AAC1B,MAAA,EAAA,CAAG,eAAA,EAAgB;AACnB,MAAA,MAAM,OAAA,GAAU,GAAG,iBAAA,EAAkB;AACrC,MAAA,MAAMC,IAAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,MAAM,CAAA;AAClC,MAAA,MAAMC,SACJD,IAAAA,CAAI,QAAA,KAAa,QAAA,GACb,IAAIE,iBAAM,KAAA,CAAM;AAAA,QACd,EAAA,EAAIJ,mBAAA,CAAG,YAAA,CAAa,OAAA,CAAQ,MAAgB,CAAA;AAAA,QAC5C,SAAA,EAAW;AAAA,OACZ,CAAA,GACD,MAAA;AACN,MAAA,IAAA,CAAK,cAAA,GAAiB,EAAE,GAAA,EAAAE,IAAAA,EAAK,OAAAC,MAAAA,EAAM;AAAA,IACrC;AAEA,IAAA,MAAM,EAAE,GAAA,EAAK,KAAA,EAAM,GAAI,IAAA,CAAK,cAAA;AAC5B,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC/B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS,IAAA,CAAK,mBAAA,CAAoB,UAAU,CAAA;AAAA,MAC5C,GAAI,KAAA,IAAS,EAAE,KAAA;AAAM,KACvB;AACA,IAAA,OAAO,CAAC,IAAI,GAAA,CAAI,IAAI,QAAA,EAAU,GAAG,WAAW,CAAA;AAAA,EAC9C;AAAA,EAEQ,kBAAA,CACN,gBACA,UAAA,EACQ;AACR,IAAA,MAAM,QAAA,GACJ,UAAA,CAAW,IAAA,KAAS,yBAAA,GAChB,CAAA,EAAG,WAAW,IAAI,CAAA,CAAA,EAAI,UAAA,CAAW,GAAG,CAAA,CAAA,GACpC,EAAA;AACN,IAAA,OAAO,GAAG,cAAA,CAAe,GAAG,CAAA,CAAA,EAAI,cAAA,CAAe,iBAAiB,KAAK,CAAA,CAAA,EACnE,cAAA,CAAe,MAAA,IAAU,EAC3B,CAAA,CAAA,EAAI,cAAA,CAAe,MAAA,IAAU,EAAE,IAAI,QAAQ,CAAA,CAAA;AAAA,EAC7C;AAAA,EAEQ,gBAAA,CACN,cAAA,EACA,UAAA,EACA,EAAA,EACa;AACb,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,kBAAA,CAAmB,cAAA,EAAgB,UAAU,CAAA;AAE9D,IAAA,IAAI,KAAA,GAAQ,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,GAAG,CAAA;AACnC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,KAAA,GAAQ,IAAIC,iBAAM,KAAA,CAAM;AAAA,QACtB,EAAA;AAAA,QACA,kBAAA,EAAoB,CAAC,cAAA,CAAe,aAAA;AAAA,QACpC,SAAA,EAAW,IAAA;AAAA,QACX,GAAI,UAAA,CAAW,IAAA,KAAS,yBAAA,IAA6B;AAAA,UACnD,MAAM,UAAA,CAAW,IAAA;AAAA,UACjB,KAAK,UAAA,CAAW;AAAA;AAClB,OACD,CAAA;AACD,MAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,IAChC;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEQ,kBAAA,CACN,UAAA,EACA,IAAA,EACA,KAAA,EACc;AACd,IAAA,IAAI,eAAe,iBAAA,EAAmB;AACpC,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,MAAsB;AAAA,QACtC,GAAG,IAAA;AAAA,QACH,IAAA,EAAM,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,EAAE;AAAA,OAClC,CAAE,CAAA;AAAA,IACJ;AAEA,IAAA,IAAI,eAAe,SAAA,EAAW;AAC5B,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,KAAqB;AACrC,QAAA,IAAI,IAAA,CAAK,IAAA,IAAQ,OAAO,IAAA,CAAK,SAAS,QAAA,EAAU;AAC9C,UAAA,OAAO;AAAA,YACL,GAAG,IAAA;AAAA,YACH,MAAM,MAAA,CAAO,WAAA;AAAA,cACX,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,CAAE,IAAI,CAAA,GAAA,KAAO,CAAC,GAAA,EAAK,KAAK,CAAC;AAAA;AAChD,WACF;AAAA,QACF;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"KubernetesFetcher.cjs.js","sources":["../../src/service/KubernetesFetcher.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 type { Cluster, CoreV1Api, Metrics } from '@kubernetes/client-node';\nimport {\n FetchResponseWrapper,\n KubernetesFetcher,\n ObjectFetchParams,\n ObjectToFetch,\n} from '@backstage/plugin-kubernetes-node';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n FetchResponse,\n KubernetesErrorTypes,\n KubernetesFetchError,\n PodStatusFetchResponse,\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 { JsonObject } from '@backstage/types';\nimport {\n ClusterDetails,\n KubernetesCredential,\n} from '@backstage/plugin-kubernetes-node';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport interface KubernetesClientBasedFetcherOptions {\n logger: LoggerService;\n}\n\ntype FetchResult = FetchResponse | KubernetesFetchError;\n\nconst isError = (fr: FetchResult): fr is KubernetesFetchError =>\n fr.hasOwnProperty('errorType');\n\nfunction fetchResultsToResponseWrapper(\n results: FetchResult[],\n): FetchResponseWrapper {\n const errors: KubernetesFetchError[] = [];\n const responses: FetchResponse[] = [];\n for (const result of results) {\n if (isError(result)) {\n errors.push(result);\n } else {\n responses.push(result);\n }\n }\n return { errors, responses };\n}\n\nconst statusCodeToErrorType = (statusCode: number): 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 class KubernetesClientBasedFetcher implements KubernetesFetcher {\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 }: KubernetesClientBasedFetcherOptions) {\n this.logger = logger;\n }\n\n fetchObjectsForService(\n params: ObjectFetchParams,\n ): Promise<FetchResponseWrapper> {\n const fetchResults = Array.from(params.objectTypesToFetch)\n .concat(params.customResources)\n .map(({ objectType, group, apiVersion, plural }) =>\n this.fetchResource(\n params.clusterDetails,\n params.credential,\n { group, apiVersion, plural },\n params.namespace,\n params.labelSelector,\n ).then(\n (r: Response): Promise<FetchResult> =>\n r.ok\n ? r.json().then(\n ({ kind, items }): FetchResponse => ({\n type: objectType,\n resources: this.transformResources(objectType, kind, items),\n }),\n )\n : this.handleUnsuccessfulResponse(params.clusterDetails.name, r),\n ),\n );\n\n return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);\n }\n\n async fetchPodMetricsByNamespaces(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n namespaces: Set<string>,\n labelSelector?: string,\n ): Promise<FetchResponseWrapper> {\n const fetchResults = Array.from(namespaces).map(ns =>\n this.fetchPodMetricsForNamespace(\n clusterDetails,\n credential,\n ns,\n labelSelector,\n ),\n );\n\n return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);\n }\n\n private async fetchPodMetricsForNamespace(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n namespace: string,\n labelSelector?: string,\n ): Promise<FetchResult> {\n const [podMetrics, podList] = await Promise.all([\n this.fetchResource(\n clusterDetails,\n credential,\n { group: 'metrics.k8s.io', apiVersion: 'v1beta1', plural: 'pods' },\n namespace,\n labelSelector,\n ),\n this.fetchResource(\n clusterDetails,\n credential,\n { group: '', apiVersion: 'v1', plural: 'pods' },\n namespace,\n labelSelector,\n ),\n ]);\n if (podMetrics.ok && podList.ok) {\n const { topPods } = await import('@kubernetes/client-node');\n return topPods(\n {\n listPodForAllNamespaces: () => podList.json(),\n } as unknown as CoreV1Api,\n {\n getPodMetrics: () => podMetrics.json(),\n } as unknown as Metrics,\n ).then(\n (resources): PodStatusFetchResponse => ({\n type: 'podstatus',\n resources,\n }),\n );\n } else if (podMetrics.ok) {\n return this.handleUnsuccessfulResponse(clusterDetails.name, podList);\n }\n return this.handleUnsuccessfulResponse(clusterDetails.name, podMetrics);\n }\n\n private 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 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 private async fetchResource(\n clusterDetails: ClusterDetails,\n credential: KubernetesCredential,\n resource: Pick<ObjectToFetch, 'group' | 'apiVersion' | 'plural'>,\n namespace?: string,\n labelSelector?: string,\n ): Promise<Response> {\n const resourcePath = this.buildResourcePath(\n resource.group,\n resource.apiVersion,\n resource.plural,\n namespace,\n );\n\n let url: URL;\n let requestInit: RequestInit;\n const authProvider =\n clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (this.isServiceAccountAuthentication(authProvider, clusterDetails)) {\n [url, requestInit] = await this.fetchArgsInCluster(credential);\n } else if (!this.isCredentialMissing(authProvider, credential)) {\n [url, requestInit] = await this.fetchArgs(clusterDetails, credential);\n } else {\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 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 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 private transformResources(\n objectType: string,\n kind: string,\n items: JsonObject[],\n ): JsonObject[] {\n let result = items;\n\n if (objectType === 'customresources') {\n const singularKind = kind.replace(/(List)$/, '');\n result = result.map((item: JsonObject) => ({\n ...item,\n kind: singularKind,\n }));\n }\n\n if (\n objectType === 'secrets' ||\n (kind && kind.replace(/List$/, '') === 'Secret')\n ) {\n result = result.map((item: JsonObject) => {\n const redacted: JsonObject = { ...item };\n if (item.data && typeof item.data === 'object') {\n redacted.data = Object.fromEntries(\n Object.keys(item.data).map(key => [key, '***']),\n );\n }\n if (item.stringData && typeof item.stringData === 'object') {\n redacted.stringData = Object.fromEntries(\n Object.keys(item.stringData).map(key => [key, '***']),\n );\n }\n return redacted;\n });\n }\n\n return result;\n }\n}\n"],"names":["ANNOTATION_KUBERNETES_AUTH_PROVIDER","fetch","fs","SERVICEACCOUNT_CA_PATH","url","agent","https"],"mappings":"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"}
|
|
@@ -141,12 +141,27 @@ class KubernetesRouter {
|
|
|
141
141
|
httpAuth,
|
|
142
142
|
req
|
|
143
143
|
);
|
|
144
|
+
const credentials = await httpAuth.credentials(req);
|
|
145
|
+
let resolvedEntity = requestBody?.entity;
|
|
146
|
+
if (requestBody?.entity) {
|
|
147
|
+
if (!entityRef) {
|
|
148
|
+
throw new errors.NotAllowedError("Invalid entity reference");
|
|
149
|
+
}
|
|
150
|
+
const parsedRef = catalogModel.parseEntityRef(entityRef);
|
|
151
|
+
const catalogEntity = await catalog.getEntityByRef(parsedRef, {
|
|
152
|
+
credentials
|
|
153
|
+
});
|
|
154
|
+
if (!catalogEntity) {
|
|
155
|
+
throw new errors.NotAllowedError(`Entity not found, ${entityRef}`);
|
|
156
|
+
}
|
|
157
|
+
resolvedEntity = catalogEntity;
|
|
158
|
+
}
|
|
144
159
|
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
|
145
160
|
{
|
|
146
|
-
entity:
|
|
161
|
+
entity: resolvedEntity,
|
|
147
162
|
auth: requestBody?.auth || {}
|
|
148
163
|
},
|
|
149
|
-
{ credentials
|
|
164
|
+
{ credentials }
|
|
150
165
|
);
|
|
151
166
|
res.json(response);
|
|
152
167
|
auditorEvent.success().catch(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesRouter.cjs.js","sources":["../../src/service/KubernetesRouter.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 */\nimport { Config } from '@backstage/config';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,\n kubernetesClustersReadPermission,\n kubernetesPermissions,\n kubernetesResourcesReadPermission,\n} from '@backstage/plugin-kubernetes-common';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\n\nimport { DispatchStrategy } from '../auth';\nimport { NotAllowedError, toError } from '@backstage/errors';\n\nimport {\n AuthService,\n AuditorService,\n BackstageCredentials,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n AuthMetadata,\n KubernetesClustersSupplier,\n KubernetesFetcher,\n KubernetesObjectsProvider,\n KubernetesRouterFactory,\n KubernetesServiceLocator,\n} from '@backstage/plugin-kubernetes-node';\nimport { addResourceRoutesToRouter } from '../routes/resourcesRoutes';\nimport { ObjectsByEntityRequest } from '../types/types';\nimport { KubernetesProxy } from './KubernetesProxy';\nimport { requirePermission } from '../auth/requirePermission';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { resolveProxyMiddlewareCacheOptions } from './ProxyMiddlewareCache';\n\nexport interface KubernetesEnvironment {\n logger: LoggerService;\n config: Config;\n catalog: CatalogService;\n discovery: DiscoveryService;\n permissions: PermissionEvaluator;\n auth: AuthService;\n httpAuth: HttpAuthService;\n auditor: AuditorService;\n authStrategyMap: { [key: string]: AuthenticationStrategy };\n fetcher: KubernetesFetcher;\n clusterSupplier: KubernetesClustersSupplier;\n serviceLocator: KubernetesServiceLocator;\n objectsProvider: KubernetesObjectsProvider;\n customRouter?: KubernetesRouterFactory;\n}\n\nexport class KubernetesRouter {\n static create(env: KubernetesEnvironment) {\n return new KubernetesRouter(env);\n }\n\n protected readonly env: KubernetesEnvironment;\n\n constructor(env: KubernetesEnvironment) {\n this.env = env;\n }\n\n public async getRouter() {\n const {\n logger,\n config,\n permissions,\n authStrategyMap,\n clusterSupplier,\n objectsProvider,\n catalog,\n discovery,\n httpAuth,\n customRouter,\n } = this.env;\n\n logger.info('Initializing Kubernetes backend');\n\n if (!config.has('kubernetes')) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error('Kubernetes configuration is missing');\n }\n logger.warn(\n 'Failed to initialize kubernetes backend: kubernetes config is missing',\n );\n return Router();\n }\n\n await this.warnForClustersWithSkipTLSVerify(logger, clusterSupplier);\n\n const proxy = this.buildProxy(\n logger,\n clusterSupplier,\n discovery,\n httpAuth,\n authStrategyMap,\n );\n\n return (\n customRouter?.({\n getDefault: () =>\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n ),\n objectsProvider,\n clusterSupplier,\n authStrategyMap,\n }) ??\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n )\n );\n }\n\n private buildProxy(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n discovery: DiscoveryService,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): KubernetesProxy {\n const authStrategy = new DispatchStrategy({\n authStrategyMap,\n });\n const middlewareCacheConfig = this.env.config.getOptionalConfig(\n 'kubernetes.proxy.middlewareCache',\n );\n const { ttlMs, maxSize } = resolveProxyMiddlewareCacheOptions({\n ttlMs: middlewareCacheConfig?.getOptionalNumber('ttl.milliseconds'),\n maxSize: middlewareCacheConfig?.getOptionalNumber('maxSize'),\n });\n\n return new KubernetesProxy({\n logger,\n clusterSupplier,\n authStrategy,\n discovery,\n httpAuth,\n auditor: this.env.auditor,\n middlewareCache: { ttlMs, maxSize },\n });\n }\n\n private buildDefaultRouter(\n objectsProvider: KubernetesObjectsProvider,\n clusterSupplier: KubernetesClustersSupplier,\n catalog: CatalogService,\n proxy: KubernetesProxy,\n permissionApi: PermissionEvaluator,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): express.Router {\n const logger = this.env.logger;\n const auditor = this.env.auditor;\n const router = Router();\n router.use('/proxy', proxy.createRequestHandler({ permissionApi }));\n router.use(express.json());\n router.use(\n createPermissionIntegrationRouter({\n permissions: kubernetesPermissions,\n }),\n );\n\n // @deprecated\n router.post('/services/:serviceId', async (req, res) => {\n const serviceId = req.params.serviceId;\n const requestBody: ObjectsByEntityRequest = req.body;\n let entityRef: string | undefined;\n if (requestBody?.entity) {\n try {\n entityRef = stringifyEntityRef(requestBody.entity);\n } catch {\n entityRef = undefined;\n }\n }\n\n const auditorEvent = await auditor.createEvent({\n eventId: 'resource-fetch',\n request: req,\n meta: { queryType: 'services', entityRef, serviceId },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n const response = await objectsProvider.getKubernetesObjectsByEntity(\n {\n entity: requestBody?.entity,\n auth: requestBody?.auth || {},\n },\n { credentials: await httpAuth.credentials(req) },\n );\n res.json(response);\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event resource-fetch (services)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n if (e instanceof NotAllowedError) {\n throw e;\n }\n logger.error(\n `action=retrieveObjectsByServiceId service=${serviceId}, error: ${err}`,\n );\n if (!res.headersSent) {\n res.status(500).json({ error: err.message });\n }\n }\n });\n\n router.get('/clusters', async (req, res) => {\n const auditorEvent = await auditor.createEvent({\n eventId: 'cluster-fetch',\n request: req,\n meta: { queryType: 'list' },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesClustersReadPermission,\n httpAuth,\n req,\n );\n const credentials = await httpAuth.credentials(req);\n const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {\n credentials,\n });\n res.json({\n items: clusterDetails.map(cd => {\n const oidcTokenProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];\n const authProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n const strategy = authStrategyMap[authProvider];\n let auth: AuthMetadata = {};\n if (strategy) {\n auth = strategy.presentAuthMetadata(cd.authMetadata);\n }\n\n return {\n name: cd.name,\n title: cd.title,\n dashboardUrl: cd.dashboardUrl,\n authProvider,\n ...(oidcTokenProvider && { oidcTokenProvider }),\n ...(auth && Object.keys(auth).length !== 0 && { auth }),\n };\n }),\n });\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event cluster-fetch (list)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n throw e;\n }\n });\n\n addResourceRoutesToRouter(\n router,\n catalog,\n objectsProvider,\n httpAuth,\n permissionApi,\n auditor,\n logger,\n );\n\n return router;\n }\n\n private async fetchClusterDetails(\n clusterSupplier: KubernetesClustersSupplier,\n options: { credentials: BackstageCredentials },\n ) {\n const clusterDetails = await clusterSupplier.getClusters(options);\n\n this.env.logger.debug(\n `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`,\n );\n\n return clusterDetails;\n }\n\n private async warnForClustersWithSkipTLSVerify(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n ): Promise<void> {\n try {\n const credentials = await this.env.auth.getOwnServiceCredentials();\n const clusters = await clusterSupplier.getClusters({ credentials });\n\n for (const cluster of clusters) {\n if (cluster.skipTLSVerify) {\n logger.warn(\n `Cluster '${cluster.name}' is configured with skipTLSVerify: true; TLS certificate verification is disabled for Kubernetes API traffic to this cluster`,\n );\n }\n }\n } catch (error) {\n logger.warn(\n `Failed to log skipTLSVerify warnings at startup: ${\n toError(error).message\n }`,\n );\n }\n }\n}\n"],"names":["Router","DispatchStrategy","resolveProxyMiddlewareCacheOptions","KubernetesProxy","express","createPermissionIntegrationRouter","kubernetesPermissions","stringifyEntityRef","requirePermission","kubernetesResourcesReadPermission","NotAllowedError","kubernetesClustersReadPermission","ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER","ANNOTATION_KUBERNETES_AUTH_PROVIDER","addResourceRoutesToRouter","toError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KubernetesRouter.cjs.js","sources":["../../src/service/KubernetesRouter.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 */\nimport { Config } from '@backstage/config';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,\n kubernetesClustersReadPermission,\n kubernetesPermissions,\n kubernetesResourcesReadPermission,\n} from '@backstage/plugin-kubernetes-common';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\n\nimport { DispatchStrategy } from '../auth';\nimport { NotAllowedError, toError } from '@backstage/errors';\n\nimport {\n AuthService,\n AuditorService,\n BackstageCredentials,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n AuthMetadata,\n KubernetesClustersSupplier,\n KubernetesFetcher,\n KubernetesObjectsProvider,\n KubernetesRouterFactory,\n KubernetesServiceLocator,\n} from '@backstage/plugin-kubernetes-node';\nimport { addResourceRoutesToRouter } from '../routes/resourcesRoutes';\nimport { ObjectsByEntityRequest } from '../types/types';\nimport { KubernetesProxy } from './KubernetesProxy';\nimport { requirePermission } from '../auth/requirePermission';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';\nimport { resolveProxyMiddlewareCacheOptions } from './ProxyMiddlewareCache';\n\nexport interface KubernetesEnvironment {\n logger: LoggerService;\n config: Config;\n catalog: CatalogService;\n discovery: DiscoveryService;\n permissions: PermissionEvaluator;\n auth: AuthService;\n httpAuth: HttpAuthService;\n auditor: AuditorService;\n authStrategyMap: { [key: string]: AuthenticationStrategy };\n fetcher: KubernetesFetcher;\n clusterSupplier: KubernetesClustersSupplier;\n serviceLocator: KubernetesServiceLocator;\n objectsProvider: KubernetesObjectsProvider;\n customRouter?: KubernetesRouterFactory;\n}\n\nexport class KubernetesRouter {\n static create(env: KubernetesEnvironment) {\n return new KubernetesRouter(env);\n }\n\n protected readonly env: KubernetesEnvironment;\n\n constructor(env: KubernetesEnvironment) {\n this.env = env;\n }\n\n public async getRouter() {\n const {\n logger,\n config,\n permissions,\n authStrategyMap,\n clusterSupplier,\n objectsProvider,\n catalog,\n discovery,\n httpAuth,\n customRouter,\n } = this.env;\n\n logger.info('Initializing Kubernetes backend');\n\n if (!config.has('kubernetes')) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error('Kubernetes configuration is missing');\n }\n logger.warn(\n 'Failed to initialize kubernetes backend: kubernetes config is missing',\n );\n return Router();\n }\n\n await this.warnForClustersWithSkipTLSVerify(logger, clusterSupplier);\n\n const proxy = this.buildProxy(\n logger,\n clusterSupplier,\n discovery,\n httpAuth,\n authStrategyMap,\n );\n\n return (\n customRouter?.({\n getDefault: () =>\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n ),\n objectsProvider,\n clusterSupplier,\n authStrategyMap,\n }) ??\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n )\n );\n }\n\n private buildProxy(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n discovery: DiscoveryService,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): KubernetesProxy {\n const authStrategy = new DispatchStrategy({\n authStrategyMap,\n });\n const middlewareCacheConfig = this.env.config.getOptionalConfig(\n 'kubernetes.proxy.middlewareCache',\n );\n const { ttlMs, maxSize } = resolveProxyMiddlewareCacheOptions({\n ttlMs: middlewareCacheConfig?.getOptionalNumber('ttl.milliseconds'),\n maxSize: middlewareCacheConfig?.getOptionalNumber('maxSize'),\n });\n\n return new KubernetesProxy({\n logger,\n clusterSupplier,\n authStrategy,\n discovery,\n httpAuth,\n auditor: this.env.auditor,\n middlewareCache: { ttlMs, maxSize },\n });\n }\n\n private buildDefaultRouter(\n objectsProvider: KubernetesObjectsProvider,\n clusterSupplier: KubernetesClustersSupplier,\n catalog: CatalogService,\n proxy: KubernetesProxy,\n permissionApi: PermissionEvaluator,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): express.Router {\n const logger = this.env.logger;\n const auditor = this.env.auditor;\n const router = Router();\n router.use('/proxy', proxy.createRequestHandler({ permissionApi }));\n router.use(express.json());\n router.use(\n createPermissionIntegrationRouter({\n permissions: kubernetesPermissions,\n }),\n );\n\n // @deprecated\n router.post('/services/:serviceId', async (req, res) => {\n const serviceId = req.params.serviceId;\n const requestBody: ObjectsByEntityRequest = req.body;\n let entityRef: string | undefined;\n if (requestBody?.entity) {\n try {\n entityRef = stringifyEntityRef(requestBody.entity);\n } catch {\n entityRef = undefined;\n }\n }\n\n const auditorEvent = await auditor.createEvent({\n eventId: 'resource-fetch',\n request: req,\n meta: { queryType: 'services', entityRef, serviceId },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n\n const credentials = await httpAuth.credentials(req);\n\n let resolvedEntity = requestBody?.entity;\n if (requestBody?.entity) {\n if (!entityRef) {\n throw new NotAllowedError('Invalid entity reference');\n }\n const parsedRef = parseEntityRef(entityRef);\n const catalogEntity = await catalog.getEntityByRef(parsedRef, {\n credentials,\n });\n if (!catalogEntity) {\n throw new NotAllowedError(`Entity not found, ${entityRef}`);\n }\n resolvedEntity = catalogEntity;\n }\n\n const response = await objectsProvider.getKubernetesObjectsByEntity(\n {\n entity: resolvedEntity,\n auth: requestBody?.auth || {},\n },\n { credentials },\n );\n res.json(response);\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event resource-fetch (services)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n if (e instanceof NotAllowedError) {\n throw e;\n }\n logger.error(\n `action=retrieveObjectsByServiceId service=${serviceId}, error: ${err}`,\n );\n if (!res.headersSent) {\n res.status(500).json({ error: err.message });\n }\n }\n });\n\n router.get('/clusters', async (req, res) => {\n const auditorEvent = await auditor.createEvent({\n eventId: 'cluster-fetch',\n request: req,\n meta: { queryType: 'list' },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesClustersReadPermission,\n httpAuth,\n req,\n );\n const credentials = await httpAuth.credentials(req);\n const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {\n credentials,\n });\n res.json({\n items: clusterDetails.map(cd => {\n const oidcTokenProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];\n const authProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n const strategy = authStrategyMap[authProvider];\n let auth: AuthMetadata = {};\n if (strategy) {\n auth = strategy.presentAuthMetadata(cd.authMetadata);\n }\n\n return {\n name: cd.name,\n title: cd.title,\n dashboardUrl: cd.dashboardUrl,\n authProvider,\n ...(oidcTokenProvider && { oidcTokenProvider }),\n ...(auth && Object.keys(auth).length !== 0 && { auth }),\n };\n }),\n });\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event cluster-fetch (list)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n throw e;\n }\n });\n\n addResourceRoutesToRouter(\n router,\n catalog,\n objectsProvider,\n httpAuth,\n permissionApi,\n auditor,\n logger,\n );\n\n return router;\n }\n\n private async fetchClusterDetails(\n clusterSupplier: KubernetesClustersSupplier,\n options: { credentials: BackstageCredentials },\n ) {\n const clusterDetails = await clusterSupplier.getClusters(options);\n\n this.env.logger.debug(\n `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`,\n );\n\n return clusterDetails;\n }\n\n private async warnForClustersWithSkipTLSVerify(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n ): Promise<void> {\n try {\n const credentials = await this.env.auth.getOwnServiceCredentials();\n const clusters = await clusterSupplier.getClusters({ credentials });\n\n for (const cluster of clusters) {\n if (cluster.skipTLSVerify) {\n logger.warn(\n `Cluster '${cluster.name}' is configured with skipTLSVerify: true; TLS certificate verification is disabled for Kubernetes API traffic to this cluster`,\n );\n }\n }\n } catch (error) {\n logger.warn(\n `Failed to log skipTLSVerify warnings at startup: ${\n toError(error).message\n }`,\n );\n }\n }\n}\n"],"names":["Router","DispatchStrategy","resolveProxyMiddlewareCacheOptions","KubernetesProxy","express","createPermissionIntegrationRouter","kubernetesPermissions","stringifyEntityRef","requirePermission","kubernetesResourcesReadPermission","NotAllowedError","parseEntityRef","kubernetesClustersReadPermission","ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER","ANNOTATION_KUBERNETES_AUTH_PROVIDER","addResourceRoutesToRouter","toError"],"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.
|
|
3
|
+
"version": "0.21.9",
|
|
4
4
|
"description": "A Backstage backend plugin that integrates towards Kubernetes",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -70,6 +70,7 @@
|
|
|
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"
|