@backstage/plugin-kubernetes-backend 0.21.7-next.0 → 0.21.7
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 +28 -0
- package/config.schema.json +24 -0
- package/dist/index.d.ts +29 -3
- package/dist/package.json.cjs.js +1 -1
- package/dist/plugin.cjs.js +5 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/routes/resourcesRoutes.cjs.js +75 -39
- package/dist/routes/resourcesRoutes.cjs.js.map +1 -1
- package/dist/service/KubernetesProxy.cjs.js +177 -64
- package/dist/service/KubernetesProxy.cjs.js.map +1 -1
- package/dist/service/KubernetesRouter.cjs.js +116 -40
- package/dist/service/KubernetesRouter.cjs.js.map +1 -1
- package/dist/service/ProxyAuditSession.cjs.js +128 -0
- package/dist/service/ProxyAuditSession.cjs.js.map +1 -0
- package/dist/service/ProxyMiddlewareCache.cjs.js +69 -0
- package/dist/service/ProxyMiddlewareCache.cjs.js.map +1 -0
- package/package.json +18 -18
|
@@ -5,55 +5,169 @@ var pluginKubernetesCommon = require('@backstage/plugin-kubernetes-common');
|
|
|
5
5
|
var pluginPermissionCommon = require('@backstage/plugin-permission-common');
|
|
6
6
|
var httpProxyMiddleware = require('http-proxy-middleware');
|
|
7
7
|
var fs = require('fs-extra');
|
|
8
|
+
var ProxyMiddlewareCache = require('./ProxyMiddlewareCache.cjs.js');
|
|
9
|
+
var ProxyAuditSession = require('./ProxyAuditSession.cjs.js');
|
|
8
10
|
|
|
9
11
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
12
|
|
|
11
13
|
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
12
14
|
|
|
15
|
+
const PROXY_PREPARED_TARGET = /* @__PURE__ */ Symbol("kubernetesProxyPreparedTarget");
|
|
13
16
|
const HEADER_KUBERNETES_CLUSTER = "Backstage-Kubernetes-Cluster";
|
|
14
17
|
const HEADER_KUBERNETES_AUTH = "Backstage-Kubernetes-Authorization";
|
|
15
18
|
class KubernetesProxy {
|
|
16
|
-
|
|
19
|
+
middlewareCache;
|
|
17
20
|
logger;
|
|
18
21
|
clusterSupplier;
|
|
19
22
|
authStrategy;
|
|
20
23
|
httpAuth;
|
|
24
|
+
auditor;
|
|
21
25
|
constructor(options) {
|
|
22
26
|
this.logger = options.logger;
|
|
23
27
|
this.clusterSupplier = options.clusterSupplier;
|
|
24
28
|
this.authStrategy = options.authStrategy;
|
|
25
29
|
this.httpAuth = options.httpAuth;
|
|
30
|
+
this.auditor = options.auditor;
|
|
31
|
+
this.middlewareCache = new ProxyMiddlewareCache.ProxyMiddlewareCache(
|
|
32
|
+
ProxyMiddlewareCache.resolveProxyMiddlewareCacheOptions(options.middlewareCache)
|
|
33
|
+
);
|
|
26
34
|
}
|
|
27
35
|
createRequestHandler(options) {
|
|
28
36
|
const { permissionApi } = options;
|
|
29
37
|
return async (req, res, next) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const clusterNameHeader = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
|
|
39
|
+
const clusterName = typeof clusterNameHeader === "string" && clusterNameHeader.length > 0 ? clusterNameHeader : "unknown";
|
|
40
|
+
const path = req.path || req.url.split("?")[0] || "";
|
|
41
|
+
let clientDisconnected = false;
|
|
42
|
+
const onSocketClose = () => {
|
|
43
|
+
clientDisconnected = true;
|
|
44
|
+
};
|
|
45
|
+
req.socket.once("close", onSocketClose);
|
|
46
|
+
let auditSession;
|
|
47
|
+
let finishListenerAttached = false;
|
|
48
|
+
try {
|
|
49
|
+
auditSession = await ProxyAuditSession.ProxyAuditSession.start(this.auditor, {
|
|
50
|
+
req,
|
|
51
|
+
clusterName,
|
|
52
|
+
method: req.method,
|
|
53
|
+
path,
|
|
54
|
+
logger: this.logger
|
|
55
|
+
});
|
|
56
|
+
finishListenerAttached = await this.authorizeAndDispatch(
|
|
57
|
+
req,
|
|
58
|
+
res,
|
|
59
|
+
next,
|
|
60
|
+
permissionApi,
|
|
61
|
+
auditSession,
|
|
62
|
+
onSocketClose,
|
|
63
|
+
() => clientDisconnected
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
req.socket.removeListener("close", onSocketClose);
|
|
67
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
68
|
+
if (auditSession && !finishListenerAttached) {
|
|
69
|
+
auditSession.finalize(err);
|
|
34
70
|
}
|
|
35
|
-
|
|
36
|
-
const auth = authorizeResponse[0];
|
|
37
|
-
if (auth.result === pluginPermissionCommon.AuthorizeResult.DENY) {
|
|
38
|
-
res.status(403).json({ error: new errors.NotAllowedError("Unauthorized") });
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const middleware = await this.getMiddleware(req);
|
|
42
|
-
if (req.header("connection")?.toLowerCase() === "upgrade" && req.header("upgrade")?.toLowerCase() === "websocket") {
|
|
43
|
-
middleware.upgrade(req, req.socket, void 0);
|
|
44
|
-
} else {
|
|
45
|
-
middleware(req, res, next);
|
|
71
|
+
throw error;
|
|
46
72
|
}
|
|
47
73
|
};
|
|
48
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Authorizes the request, resolves the target cluster, and dispatches
|
|
77
|
+
* to either the HTTP or WebSocket proxy path. Returns `true` when
|
|
78
|
+
* HTTP finish listeners have been attached.
|
|
79
|
+
*/
|
|
80
|
+
async authorizeAndDispatch(req, res, next, permissionApi, auditSession, onSocketClose, isClientDisconnected) {
|
|
81
|
+
const authorizeResponse = await permissionApi.authorize(
|
|
82
|
+
[{ permission: pluginKubernetesCommon.kubernetesProxyPermission }],
|
|
83
|
+
{ credentials: await this.httpAuth.credentials(req) }
|
|
84
|
+
);
|
|
85
|
+
if (authorizeResponse[0].result === pluginPermissionCommon.AuthorizeResult.DENY) {
|
|
86
|
+
req.socket.removeListener("close", onSocketClose);
|
|
87
|
+
auditSession.finalize(new errors.NotAllowedError("Unauthorized"));
|
|
88
|
+
res.status(403).json({
|
|
89
|
+
error: errors.serializeError(new errors.NotAllowedError("Unauthorized"))
|
|
90
|
+
});
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (isClientDisconnected()) {
|
|
94
|
+
auditSession.finalize(
|
|
95
|
+
new Error("Client disconnected before proxy dispatch")
|
|
96
|
+
);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const prepared = await this.prepareProxyTarget(req);
|
|
100
|
+
auditSession.setResolvedClusterName(prepared.cluster.name);
|
|
101
|
+
req[PROXY_PREPARED_TARGET] = prepared;
|
|
102
|
+
const middleware = this.getOrCreateMiddleware(prepared.cluster);
|
|
103
|
+
if (isClientDisconnected()) {
|
|
104
|
+
auditSession.finalize(
|
|
105
|
+
new Error("Client disconnected before proxy dispatch")
|
|
106
|
+
);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return KubernetesProxy.dispatchToProxy(
|
|
110
|
+
req,
|
|
111
|
+
res,
|
|
112
|
+
next,
|
|
113
|
+
middleware,
|
|
114
|
+
auditSession,
|
|
115
|
+
onSocketClose
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolves the cluster, credentials, and target URL for a proxy request
|
|
120
|
+
* before dispatching to the middleware. This ensures all async preparation
|
|
121
|
+
* completes under the caller's try/catch, avoiding unhandled rejections
|
|
122
|
+
* from http-proxy-middleware's fire-and-forget upgrade() path.
|
|
123
|
+
*/
|
|
124
|
+
async prepareProxyTarget(req) {
|
|
125
|
+
const cluster = await this.getClusterForRequest(req);
|
|
126
|
+
const url = new URL(cluster.url);
|
|
127
|
+
const { bufferFromFileOrString } = await import('@kubernetes/client-node');
|
|
128
|
+
const target = {
|
|
129
|
+
protocol: url.protocol,
|
|
130
|
+
host: url.hostname,
|
|
131
|
+
port: url.port,
|
|
132
|
+
ca: bufferFromFileOrString(cluster.caFile, cluster.caData)?.toString()
|
|
133
|
+
};
|
|
134
|
+
const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase("en-US")];
|
|
135
|
+
if (typeof authHeader === "string") {
|
|
136
|
+
req.headers.authorization = authHeader;
|
|
137
|
+
} else {
|
|
138
|
+
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
139
|
+
req.headers
|
|
140
|
+
);
|
|
141
|
+
const credential = await this.authStrategy.getCredential(
|
|
142
|
+
cluster,
|
|
143
|
+
authObj
|
|
144
|
+
);
|
|
145
|
+
if (credential.type === "bearer token") {
|
|
146
|
+
req.headers.authorization = `Bearer ${credential.token}`;
|
|
147
|
+
} else if (credential.type === "x509 client certificate") {
|
|
148
|
+
target.key = credential.key;
|
|
149
|
+
target.cert = credential.cert;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const requestPath = req.originalUrl || req.url || "";
|
|
153
|
+
const rewrittenPath = requestPath.replace(
|
|
154
|
+
new RegExp(
|
|
155
|
+
`^${(req.baseUrl || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`
|
|
156
|
+
),
|
|
157
|
+
url.pathname || ""
|
|
158
|
+
);
|
|
159
|
+
return { proxyTarget: target, rewrittenPath, cluster };
|
|
160
|
+
}
|
|
49
161
|
// We create one middleware per remote cluster and hold on to them, because
|
|
50
162
|
// the secure property isn't possible to decide on a per-request basis with a
|
|
51
|
-
// single middleware instance
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
163
|
+
// single middleware instance. Target resolution and path rewriting are handled
|
|
164
|
+
// per-request via the PROXY_PREPARED_TARGET symbol stashed on each req by
|
|
165
|
+
// prepareProxyTarget. Entries are refreshed after a TTL or when cluster details
|
|
166
|
+
// change.
|
|
167
|
+
getOrCreateMiddleware(cluster) {
|
|
168
|
+
let middleware = this.middlewareCache.get(cluster);
|
|
55
169
|
if (!middleware) {
|
|
56
|
-
const logger = this.logger.child({ cluster:
|
|
170
|
+
const logger = this.logger.child({ cluster: cluster.name });
|
|
57
171
|
middleware = httpProxyMiddleware.createProxyMiddleware({
|
|
58
172
|
logProvider: () => ({
|
|
59
173
|
log: logger.info.bind(logger),
|
|
@@ -62,55 +176,27 @@ class KubernetesProxy {
|
|
|
62
176
|
warn: logger.warn.bind(logger),
|
|
63
177
|
error: logger.error.bind(logger)
|
|
64
178
|
}),
|
|
65
|
-
ws
|
|
66
|
-
|
|
179
|
+
// ws must be false to prevent http-proxy-middleware from auto-subscribing
|
|
180
|
+
// to the server's 'upgrade' event, which would bypass Express routing and
|
|
181
|
+
// skip audit event creation for WebSocket requests.
|
|
182
|
+
ws: false,
|
|
183
|
+
secure: !cluster.skipTLSVerify,
|
|
67
184
|
changeOrigin: true,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const url = new URL(cluster.url);
|
|
71
|
-
return path.replace(
|
|
72
|
-
new RegExp(`^${originalReq.baseUrl}`),
|
|
73
|
-
url.pathname || ""
|
|
74
|
-
);
|
|
185
|
+
router: (req) => {
|
|
186
|
+
return req[PROXY_PREPARED_TARGET]?.proxyTarget;
|
|
75
187
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const url = new URL(cluster.url);
|
|
79
|
-
const { bufferFromFileOrString } = await import('@kubernetes/client-node');
|
|
80
|
-
const target = {
|
|
81
|
-
protocol: url.protocol,
|
|
82
|
-
host: url.hostname,
|
|
83
|
-
port: url.port,
|
|
84
|
-
ca: bufferFromFileOrString(
|
|
85
|
-
cluster.caFile,
|
|
86
|
-
cluster.caData
|
|
87
|
-
)?.toString()
|
|
88
|
-
};
|
|
89
|
-
const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase("en-US")];
|
|
90
|
-
if (typeof authHeader === "string") {
|
|
91
|
-
req.headers.authorization = authHeader;
|
|
92
|
-
} else {
|
|
93
|
-
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
94
|
-
req.headers
|
|
95
|
-
);
|
|
96
|
-
const credential = await this.getClusterForRequest(req).then((cd) => {
|
|
97
|
-
return this.authStrategy.getCredential(cd, authObj);
|
|
98
|
-
});
|
|
99
|
-
if (credential.type === "bearer token") {
|
|
100
|
-
req.headers.authorization = `Bearer ${credential.token}`;
|
|
101
|
-
} else if (credential.type === "x509 client certificate") {
|
|
102
|
-
target.key = credential.key;
|
|
103
|
-
target.cert = credential.cert;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return target;
|
|
188
|
+
pathRewrite: (_path, req) => {
|
|
189
|
+
return req[PROXY_PREPARED_TARGET]?.rewrittenPath ?? _path;
|
|
107
190
|
},
|
|
108
191
|
onError: (error, req, res) => {
|
|
109
192
|
const wrappedError = new errors.ForwardedError(
|
|
110
|
-
`Cluster '${
|
|
193
|
+
`Cluster '${cluster.name}' request error`,
|
|
111
194
|
error
|
|
112
195
|
);
|
|
113
196
|
logger.error("Kubernetes proxy error", wrappedError);
|
|
197
|
+
if (typeof res.status !== "function") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
114
200
|
const body = {
|
|
115
201
|
error: errors.serializeError(wrappedError, {
|
|
116
202
|
includeStack: process.env.NODE_ENV === "development"
|
|
@@ -119,12 +205,39 @@ class KubernetesProxy {
|
|
|
119
205
|
response: { statusCode: 500 }
|
|
120
206
|
};
|
|
121
207
|
res.status(500).json(body);
|
|
122
|
-
}
|
|
208
|
+
},
|
|
209
|
+
onProxyReqWs: ProxyAuditSession.ProxyAuditSession.handleWebSocketProxyReq
|
|
123
210
|
});
|
|
124
|
-
this.
|
|
211
|
+
this.middlewareCache.set(cluster, middleware);
|
|
125
212
|
}
|
|
126
213
|
return middleware;
|
|
127
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Branches to WebSocket upgrade or HTTP middleware dispatch and wires
|
|
217
|
+
* up the appropriate audit listeners. Returns `true` when HTTP finish
|
|
218
|
+
* listeners have been attached (used by the caller's catch block to
|
|
219
|
+
* avoid double-finalizing).
|
|
220
|
+
*/
|
|
221
|
+
static dispatchToProxy(req, res, next, middleware, auditSession, onSocketClose) {
|
|
222
|
+
const isWebSocketUpgrade = (req.header("connection") ?? "").toLowerCase().split(",").some((t) => t.trim() === "upgrade") && req.header("upgrade")?.toLowerCase() === "websocket";
|
|
223
|
+
if (isWebSocketUpgrade) {
|
|
224
|
+
auditSession.prepareForWebSocketUpgrade(req, onSocketClose);
|
|
225
|
+
try {
|
|
226
|
+
middleware.upgrade(req, req.socket, void 0);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
req.socket.removeListener("close", onSocketClose);
|
|
229
|
+
auditSession.finalize(
|
|
230
|
+
error instanceof Error ? error : new Error(String(error))
|
|
231
|
+
);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
req.socket.removeListener("close", onSocketClose);
|
|
237
|
+
auditSession.attachToHttpResponse(res);
|
|
238
|
+
middleware(req, res, next);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
128
241
|
async getClusterForRequest(req) {
|
|
129
242
|
const { KubeConfig } = await import('@kubernetes/client-node');
|
|
130
243
|
const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesProxy.cjs.js","sources":["../../src/service/KubernetesProxy.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 */\nimport {\n ErrorResponseBody,\n ForwardedError,\n NotAllowedError,\n NotFoundError,\n serializeError,\n} from '@backstage/errors';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n kubernetesProxyPermission,\n KubernetesRequestAuth,\n} from '@backstage/plugin-kubernetes-common';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport type { Cluster } from '@kubernetes/client-node';\nimport { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware';\nimport fs from 'fs-extra';\n\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\n\nimport type { Request } from 'express';\nimport { IncomingHttpHeaders } from 'node:http';\nimport {\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\n\n/**\n * The header that is used to specify the cluster name.\n *\n * @public\n */\nexport const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';\n\n/**\n * The header that is used to specify the Authentication Authorities token.\n * e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.\n * @public\n */\nexport const HEADER_KUBERNETES_AUTH: string =\n 'Backstage-Kubernetes-Authorization';\n\n/**\n * The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().\n *\n * @public\n */\nexport type KubernetesProxyCreateRequestHandlerOptions = {\n permissionApi: PermissionsService;\n};\n\n/**\n * Options accepted as a parameter by the KubernetesProxy\n *\n * @public\n */\nexport type KubernetesProxyOptions = {\n logger: LoggerService;\n clusterSupplier: KubernetesClustersSupplier;\n authStrategy: AuthenticationStrategy;\n discovery: DiscoveryService;\n httpAuth: HttpAuthService;\n};\n\n/**\n * A proxy that routes requests to the Kubernetes API.\n *\n * @public\n */\nexport class KubernetesProxy {\n private readonly middlewareForClusterName = new Map<string, RequestHandler>();\n private readonly logger: LoggerService;\n private readonly clusterSupplier: KubernetesClustersSupplier;\n private readonly authStrategy: AuthenticationStrategy;\n private readonly httpAuth: HttpAuthService;\n\n constructor(options: KubernetesProxyOptions) {\n this.logger = options.logger;\n this.clusterSupplier = options.clusterSupplier;\n this.authStrategy = options.authStrategy;\n this.httpAuth = options.httpAuth;\n }\n\n public createRequestHandler(\n options: KubernetesProxyCreateRequestHandlerOptions,\n ): RequestHandler {\n const { permissionApi } = options;\n return async (req, res, next) => {\n const authorizeResponse = await permissionApi.authorize(\n [{ permission: kubernetesProxyPermission }],\n {\n credentials: await this.httpAuth.credentials(req),\n },\n );\n const auth = authorizeResponse[0];\n\n if (auth.result === AuthorizeResult.DENY) {\n res.status(403).json({ error: new NotAllowedError('Unauthorized') });\n return;\n }\n\n const middleware = await this.getMiddleware(req);\n\n // If req is an upgrade handshake, use middleware upgrade instead of http request handler https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade\n if (\n req.header('connection')?.toLowerCase() === 'upgrade' &&\n req.header('upgrade')?.toLowerCase() === 'websocket'\n ) {\n // Missing the `head`, since it's optional we pass undefined to avoid type issues\n middleware.upgrade!(req, req.socket, undefined);\n } else {\n middleware(req, res, next);\n }\n };\n }\n\n // We create one middleware per remote cluster and hold on to them, because\n // the secure property isn't possible to decide on a per-request basis with a\n // single middleware instance - and we don't expect it to change over time.\n private async getMiddleware(originalReq: Request): Promise<RequestHandler> {\n const originalCluster = await this.getClusterForRequest(originalReq);\n let middleware = this.middlewareForClusterName.get(originalCluster.name);\n if (!middleware) {\n const logger = this.logger.child({ cluster: originalCluster.name });\n middleware = createProxyMiddleware({\n logProvider: () => ({\n log: logger.info.bind(logger),\n debug: logger.debug.bind(logger),\n info: logger.info.bind(logger),\n warn: logger.warn.bind(logger),\n error: logger.error.bind(logger),\n }),\n ws: true,\n secure: !originalCluster.skipTLSVerify,\n changeOrigin: true,\n pathRewrite: async (path, req) => {\n // Re-evaluate the cluster on each request, in case it has changed\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n return path.replace(\n new RegExp(`^${originalReq.baseUrl}`),\n url.pathname || '',\n );\n },\n router: async req => {\n // Re-evaluate the cluster on each request, in case it has changed\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n\n const { bufferFromFileOrString } = await import(\n '@kubernetes/client-node'\n );\n\n const target: any = {\n protocol: url.protocol,\n host: url.hostname,\n port: url.port,\n ca: bufferFromFileOrString(\n cluster.caFile,\n cluster.caData,\n )?.toString(),\n };\n\n const authHeader =\n req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase('en-US')];\n if (typeof authHeader === 'string') {\n req.headers.authorization = authHeader;\n } else {\n // Map Backstage-Kubernetes-Authorization-X-X headers to a KubernetesRequestAuth object\n const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(\n req.headers,\n );\n\n const credential = await this.getClusterForRequest(req).then(cd => {\n return this.authStrategy.getCredential(cd, authObj);\n });\n\n if (credential.type === 'bearer token') {\n req.headers.authorization = `Bearer ${credential.token}`;\n } else if (credential.type === 'x509 client certificate') {\n target.key = credential.key;\n target.cert = credential.cert;\n }\n }\n\n return target;\n },\n onError: (error, req, res) => {\n const wrappedError = new ForwardedError(\n `Cluster '${originalCluster.name}' request error`,\n error,\n );\n\n logger.error('Kubernetes proxy error', wrappedError);\n\n const body: ErrorResponseBody = {\n error: serializeError(wrappedError, {\n includeStack: process.env.NODE_ENV === 'development',\n }),\n request: { method: req.method, url: req.originalUrl },\n response: { statusCode: 500 },\n };\n res.status(500).json(body);\n },\n });\n this.middlewareForClusterName.set(originalCluster.name, middleware);\n }\n return middleware;\n }\n\n private async getClusterForRequest(req: Request): Promise<ClusterDetails> {\n const { KubeConfig } = await import('@kubernetes/client-node');\n\n const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusters = await this.clusterSupplier.getClusters({\n credentials: await this.httpAuth.credentials(req),\n });\n\n if (!clusters || clusters.length <= 0) {\n throw new NotFoundError(`No Clusters configured`);\n }\n\n const hasClusterNameHeader =\n typeof clusterName === 'string' && clusterName.length > 0;\n\n let cluster: ClusterDetails | undefined;\n\n if (hasClusterNameHeader) {\n cluster = clusters.find(c => c.name === clusterName);\n } else if (clusters.length === 1) {\n cluster = clusters.at(0);\n }\n\n if (!cluster) {\n throw new NotFoundError(`Cluster '${clusterName}' not found`);\n }\n\n const authProvider =\n cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (\n authProvider === 'serviceAccount' &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH) &&\n !cluster.authMetadata.serviceAccountToken\n ) {\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster;\n\n const url = new URL(clusterFromKubeConfig.server);\n cluster.url = clusterFromKubeConfig.server;\n if (url.protocol === 'https:') {\n cluster.caFile = clusterFromKubeConfig.caFile;\n }\n }\n\n return cluster;\n }\n\n private static authHeadersToKubernetesRequestAuth(\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n return Object.keys(originalHeaders)\n .filter(header => header.startsWith('backstage-kubernetes-authorization'))\n .map(header =>\n KubernetesProxy.headerToDictionary(header, originalHeaders),\n )\n .filter(headerAsDic => Object.keys(headerAsDic).length !== 0)\n .reduce(KubernetesProxy.combineHeaders, {});\n }\n\n private static headerToDictionary(\n header: string,\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n const obj: KubernetesRequestAuth = {};\n const headerSplit = header.split('-');\n if (headerSplit.length >= 4) {\n const framework = headerSplit[3].toLowerCase();\n if (headerSplit.length >= 5) {\n const provider = headerSplit.slice(4).join('-').toLowerCase();\n obj[framework] = { [provider]: originalHeaders[header] };\n } else {\n obj[framework] = originalHeaders[header];\n }\n }\n return obj;\n }\n\n private static combineHeaders(\n authObj: any,\n header: any,\n ): KubernetesRequestAuth {\n const framework = Object.keys(header)[0];\n\n if (authObj[framework]) {\n authObj[framework] = {\n ...authObj[framework],\n ...header[framework],\n };\n } else {\n authObj[framework] = header[framework];\n }\n\n return authObj;\n }\n}\n"],"names":["kubernetesProxyPermission","AuthorizeResult","NotAllowedError","createProxyMiddleware","ForwardedError","serializeError","NotFoundError","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fs","SERVICEACCOUNT_CA_PATH"],"mappings":";;;;;;;;;;;;AAqDO,MAAM,yBAAA,GAAoC;AAO1C,MAAM,sBAAA,GACX;AA6BK,MAAM,eAAA,CAAgB;AAAA,EACV,wBAAA,uBAA+B,GAAA,EAA4B;AAAA,EAC3D,MAAA;AAAA,EACA,eAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,kBAAkB,OAAA,CAAQ,eAAA;AAC/B,IAAA,IAAA,CAAK,eAAe,OAAA,CAAQ,YAAA;AAC5B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AAAA,EAC1B;AAAA,EAEO,qBACL,OAAA,EACgB;AAChB,IAAA,MAAM,EAAE,eAAc,GAAI,OAAA;AAC1B,IAAA,OAAO,OAAO,GAAA,EAAK,GAAA,EAAK,IAAA,KAAS;AAC/B,MAAA,MAAM,iBAAA,GAAoB,MAAM,aAAA,CAAc,SAAA;AAAA,QAC5C,CAAC,EAAE,UAAA,EAAYA,gDAAA,EAA2B,CAAA;AAAA,QAC1C;AAAA,UACE,WAAA,EAAa,MAAM,IAAA,CAAK,QAAA,CAAS,YAAY,GAAG;AAAA;AAClD,OACF;AACA,MAAA,MAAM,IAAA,GAAO,kBAAkB,CAAC,CAAA;AAEhC,MAAA,IAAI,IAAA,CAAK,MAAA,KAAWC,sCAAA,CAAgB,IAAA,EAAM;AACxC,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,OAAO,IAAIC,sBAAA,CAAgB,cAAc,CAAA,EAAG,CAAA;AACnE,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,aAAA,CAAc,GAAG,CAAA;AAG/C,MAAA,IACE,GAAA,CAAI,MAAA,CAAO,YAAY,CAAA,EAAG,WAAA,EAAY,KAAM,SAAA,IAC5C,GAAA,CAAI,MAAA,CAAO,SAAS,CAAA,EAAG,WAAA,OAAkB,WAAA,EACzC;AAEA,QAAA,UAAA,CAAW,OAAA,CAAS,GAAA,EAAK,GAAA,CAAI,MAAA,EAAQ,MAAS,CAAA;AAAA,MAChD,CAAA,MAAO;AACL,QAAA,UAAA,CAAW,GAAA,EAAK,KAAK,IAAI,CAAA;AAAA,MAC3B;AAAA,IACF,CAAA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAAc,WAAA,EAA+C;AACzE,IAAA,MAAM,eAAA,GAAkB,MAAM,IAAA,CAAK,oBAAA,CAAqB,WAAW,CAAA;AACnE,IAAA,IAAI,UAAA,GAAa,IAAA,CAAK,wBAAA,CAAyB,GAAA,CAAI,gBAAgB,IAAI,CAAA;AACvE,IAAA,IAAI,CAAC,UAAA,EAAY;AACf,MAAA,MAAM,MAAA,GAAS,KAAK,MAAA,CAAO,KAAA,CAAM,EAAE,OAAA,EAAS,eAAA,CAAgB,MAAM,CAAA;AAClE,MAAA,UAAA,GAAaC,yCAAA,CAAsB;AAAA,QACjC,aAAa,OAAO;AAAA,UAClB,GAAA,EAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,UAC5B,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,UAC/B,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,UAC7B,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,UAC7B,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM;AAAA,SACjC,CAAA;AAAA,QACA,EAAA,EAAI,IAAA;AAAA,QACJ,MAAA,EAAQ,CAAC,eAAA,CAAgB,aAAA;AAAA,QACzB,YAAA,EAAc,IAAA;AAAA,QACd,WAAA,EAAa,OAAO,IAAA,EAAM,GAAA,KAAQ;AAEhC,UAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,oBAAA,CAAqB,GAAG,CAAA;AACnD,UAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,UAAA,OAAO,IAAA,CAAK,OAAA;AAAA,YACV,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,WAAA,CAAY,OAAO,CAAA,CAAE,CAAA;AAAA,YACpC,IAAI,QAAA,IAAY;AAAA,WAClB;AAAA,QACF,CAAA;AAAA,QACA,MAAA,EAAQ,OAAM,GAAA,KAAO;AAEnB,UAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,oBAAA,CAAqB,GAAG,CAAA;AACnD,UAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAE/B,UAAA,MAAM,EAAE,sBAAA,EAAuB,GAAI,MAAM,OACvC,yBACF,CAAA;AAEA,UAAA,MAAM,MAAA,GAAc;AAAA,YAClB,UAAU,GAAA,CAAI,QAAA;AAAA,YACd,MAAM,GAAA,CAAI,QAAA;AAAA,YACV,MAAM,GAAA,CAAI,IAAA;AAAA,YACV,EAAA,EAAI,sBAAA;AAAA,cACF,OAAA,CAAQ,MAAA;AAAA,cACR,OAAA,CAAQ;AAAA,eACP,QAAA;AAAS,WACd;AAEA,UAAA,MAAM,aACJ,GAAA,CAAI,OAAA,CAAQ,sBAAA,CAAuB,iBAAA,CAAkB,OAAO,CAAC,CAAA;AAC/D,UAAA,IAAI,OAAO,eAAe,QAAA,EAAU;AAClC,YAAA,GAAA,CAAI,QAAQ,aAAA,GAAgB,UAAA;AAAA,UAC9B,CAAA,MAAO;AAEL,YAAA,MAAM,UAAU,eAAA,CAAgB,kCAAA;AAAA,cAC9B,GAAA,CAAI;AAAA,aACN;AAEA,YAAA,MAAM,aAAa,MAAM,IAAA,CAAK,qBAAqB,GAAG,CAAA,CAAE,KAAK,CAAA,EAAA,KAAM;AACjE,cAAA,OAAO,IAAA,CAAK,YAAA,CAAa,aAAA,CAAc,EAAA,EAAI,OAAO,CAAA;AAAA,YACpD,CAAC,CAAA;AAED,YAAA,IAAI,UAAA,CAAW,SAAS,cAAA,EAAgB;AACtC,cAAA,GAAA,CAAI,OAAA,CAAQ,aAAA,GAAgB,CAAA,OAAA,EAAU,UAAA,CAAW,KAAK,CAAA,CAAA;AAAA,YACxD,CAAA,MAAA,IAAW,UAAA,CAAW,IAAA,KAAS,yBAAA,EAA2B;AACxD,cAAA,MAAA,CAAO,MAAM,UAAA,CAAW,GAAA;AACxB,cAAA,MAAA,CAAO,OAAO,UAAA,CAAW,IAAA;AAAA,YAC3B;AAAA,UACF;AAEA,UAAA,OAAO,MAAA;AAAA,QACT,CAAA;AAAA,QACA,OAAA,EAAS,CAAC,KAAA,EAAO,GAAA,EAAK,GAAA,KAAQ;AAC5B,UAAA,MAAM,eAAe,IAAIC,qBAAA;AAAA,YACvB,CAAA,SAAA,EAAY,gBAAgB,IAAI,CAAA,eAAA,CAAA;AAAA,YAChC;AAAA,WACF;AAEA,UAAA,MAAA,CAAO,KAAA,CAAM,0BAA0B,YAAY,CAAA;AAEnD,UAAA,MAAM,IAAA,GAA0B;AAAA,YAC9B,KAAA,EAAOC,sBAAe,YAAA,EAAc;AAAA,cAClC,YAAA,EAAc,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa;AAAA,aACxC,CAAA;AAAA,YACD,SAAS,EAAE,MAAA,EAAQ,IAAI,MAAA,EAAQ,GAAA,EAAK,IAAI,WAAA,EAAY;AAAA,YACpD,QAAA,EAAU,EAAE,UAAA,EAAY,GAAA;AAAI,WAC9B;AACA,UAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAAA,QAC3B;AAAA,OACD,CAAA;AACD,MAAA,IAAA,CAAK,wBAAA,CAAyB,GAAA,CAAI,eAAA,CAAgB,IAAA,EAAM,UAAU,CAAA;AAAA,IACpE;AACA,IAAA,OAAO,UAAA;AAAA,EACT;AAAA,EAEA,MAAc,qBAAqB,GAAA,EAAuC;AACxE,IAAA,MAAM,EAAE,UAAA,EAAW,GAAI,MAAM,OAAO,yBAAyB,CAAA;AAE7D,IAAA,MAAM,WAAA,GAAc,GAAA,CAAI,OAAA,CAAQ,yBAAA,CAA0B,aAAa,CAAA;AACvE,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,eAAA,CAAgB,WAAA,CAAY;AAAA,MACtD,WAAA,EAAa,MAAM,IAAA,CAAK,QAAA,CAAS,YAAY,GAAG;AAAA,KACjD,CAAA;AAED,IAAA,IAAI,CAAC,QAAA,IAAY,QAAA,CAAS,MAAA,IAAU,CAAA,EAAG;AACrC,MAAA,MAAM,IAAIC,qBAAc,CAAA,sBAAA,CAAwB,CAAA;AAAA,IAClD;AAEA,IAAA,MAAM,oBAAA,GACJ,OAAO,WAAA,KAAgB,QAAA,IAAY,YAAY,MAAA,GAAS,CAAA;AAE1D,IAAA,IAAI,OAAA;AAEJ,IAAA,IAAI,oBAAA,EAAsB;AACxB,MAAA,OAAA,GAAU,QAAA,CAAS,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,WAAW,CAAA;AAAA,IACrD,CAAA,MAAA,IAAW,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG;AAChC,MAAA,OAAA,GAAU,QAAA,CAAS,GAAG,CAAC,CAAA;AAAA,IACzB;AAEA,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,IAAIA,oBAAA,CAAc,CAAA,SAAA,EAAY,WAAW,CAAA,WAAA,CAAa,CAAA;AAAA,IAC9D;AAEA,IAAA,MAAM,YAAA,GACJ,OAAA,CAAQ,YAAA,CAAaC,0DAAmC,CAAA;AAE1D,IAAA,IACE,YAAA,KAAiB,oBACjBC,mBAAA,CAAG,cAAA,CAAeC,6CAAsB,CAAA,IACxC,CAAC,OAAA,CAAQ,YAAA,CAAa,mBAAA,EACtB;AACA,MAAA,MAAM,EAAA,GAAK,IAAI,UAAA,EAAW;AAC1B,MAAA,EAAA,CAAG,eAAA,EAAgB;AACnB,MAAA,MAAM,qBAAA,GAAwB,GAAG,iBAAA,EAAkB;AAEnD,MAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,qBAAA,CAAsB,MAAM,CAAA;AAChD,MAAA,OAAA,CAAQ,MAAM,qBAAA,CAAsB,MAAA;AACpC,MAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC7B,QAAA,OAAA,CAAQ,SAAS,qBAAA,CAAsB,MAAA;AAAA,MACzC;AAAA,IACF;AAEA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,OAAe,mCACb,eAAA,EACuB;AACvB,IAAA,OAAO,MAAA,CAAO,IAAA,CAAK,eAAe,CAAA,CAC/B,MAAA,CAAO,YAAU,MAAA,CAAO,UAAA,CAAW,oCAAoC,CAAC,CAAA,CACxE,GAAA;AAAA,MAAI,CAAA,MAAA,KACH,eAAA,CAAgB,kBAAA,CAAmB,MAAA,EAAQ,eAAe;AAAA,KAC5D,CACC,MAAA,CAAO,CAAA,WAAA,KAAe,MAAA,CAAO,KAAK,WAAW,CAAA,CAAE,MAAA,KAAW,CAAC,CAAA,CAC3D,MAAA,CAAO,eAAA,CAAgB,cAAA,EAAgB,EAAE,CAAA;AAAA,EAC9C;AAAA,EAEA,OAAe,kBAAA,CACb,MAAA,EACA,eAAA,EACuB;AACvB,IAAA,MAAM,MAA6B,EAAC;AACpC,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA;AACpC,IAAA,IAAI,WAAA,CAAY,UAAU,CAAA,EAAG;AAC3B,MAAA,MAAM,SAAA,GAAY,WAAA,CAAY,CAAC,CAAA,CAAE,WAAA,EAAY;AAC7C,MAAA,IAAI,WAAA,CAAY,UAAU,CAAA,EAAG;AAC3B,QAAA,MAAM,QAAA,GAAW,YAAY,KAAA,CAAM,CAAC,EAAE,IAAA,CAAK,GAAG,EAAE,WAAA,EAAY;AAC5D,QAAA,GAAA,CAAI,SAAS,IAAI,EAAE,CAAC,QAAQ,GAAG,eAAA,CAAgB,MAAM,CAAA,EAAE;AAAA,MACzD,CAAA,MAAO;AACL,QAAA,GAAA,CAAI,SAAS,CAAA,GAAI,eAAA,CAAgB,MAAM,CAAA;AAAA,MACzC;AAAA,IACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA,EAEA,OAAe,cAAA,CACb,OAAA,EACA,MAAA,EACuB;AACvB,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,IAAA,CAAK,MAAM,EAAE,CAAC,CAAA;AAEvC,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,OAAA,CAAQ,SAAS,CAAA,GAAI;AAAA,QACnB,GAAG,QAAQ,SAAS,CAAA;AAAA,QACpB,GAAG,OAAO,SAAS;AAAA,OACrB;AAAA,IACF,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,SAAS,CAAA,GAAI,MAAA,CAAO,SAAS,CAAA;AAAA,IACvC;AAEA,IAAA,OAAO,OAAA;AAAA,EACT;AACF;;;;;;"}
|
|
1
|
+
{"version":3,"file":"KubernetesProxy.cjs.js","sources":["../../src/service/KubernetesProxy.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 */\nimport {\n ErrorResponseBody,\n ForwardedError,\n NotAllowedError,\n NotFoundError,\n serializeError,\n} from '@backstage/errors';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n kubernetesProxyPermission,\n KubernetesRequestAuth,\n} from '@backstage/plugin-kubernetes-common';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport type { Cluster } from '@kubernetes/client-node';\nimport { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware';\nimport fs from 'fs-extra';\n\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\n\nimport {\n ProxyMiddlewareCache,\n resolveProxyMiddlewareCacheOptions,\n} from './ProxyMiddlewareCache';\n\nimport type { NextFunction, Request, Response } from 'express';\nimport { IncomingHttpHeaders } from 'node:http';\nimport {\n AuditorService,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport { ProxyAuditSession } from './ProxyAuditSession';\n\nconst PROXY_PREPARED_TARGET = Symbol('kubernetesProxyPreparedTarget');\n\ntype PreparedProxyTarget = {\n proxyTarget: any;\n rewrittenPath: string;\n cluster: ClusterDetails;\n};\n\n/**\n * The header that is used to specify the cluster name.\n *\n * @public\n */\nexport const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';\n\n/**\n * The header that is used to specify the Authentication Authorities token.\n * e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.\n * @public\n */\nexport const HEADER_KUBERNETES_AUTH: string =\n 'Backstage-Kubernetes-Authorization';\n\n/**\n * The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().\n *\n * @public\n */\nexport type KubernetesProxyCreateRequestHandlerOptions = {\n permissionApi: PermissionsService;\n};\n\n/**\n * Options accepted as a parameter by the KubernetesProxy\n *\n * @public\n */\nexport type KubernetesProxyOptions = {\n logger: LoggerService;\n clusterSupplier: KubernetesClustersSupplier;\n authStrategy: AuthenticationStrategy;\n discovery: DiscoveryService;\n httpAuth: HttpAuthService;\n auditor?: AuditorService;\n middlewareCache?: {\n ttlMs?: number;\n maxSize?: number;\n };\n};\n\n/**\n * A proxy that routes requests to the Kubernetes API.\n *\n * @public\n */\nexport class KubernetesProxy {\n private readonly middlewareCache: ProxyMiddlewareCache;\n private readonly logger: LoggerService;\n private readonly clusterSupplier: KubernetesClustersSupplier;\n private readonly authStrategy: AuthenticationStrategy;\n private readonly httpAuth: HttpAuthService;\n private readonly auditor?: AuditorService;\n\n constructor(options: KubernetesProxyOptions) {\n this.logger = options.logger;\n this.clusterSupplier = options.clusterSupplier;\n this.authStrategy = options.authStrategy;\n this.httpAuth = options.httpAuth;\n this.auditor = options.auditor;\n this.middlewareCache = new ProxyMiddlewareCache(\n resolveProxyMiddlewareCacheOptions(options.middlewareCache),\n );\n }\n\n public createRequestHandler(\n options: KubernetesProxyCreateRequestHandlerOptions,\n ): RequestHandler {\n const { permissionApi } = options;\n return async (req, res, next) => {\n const clusterNameHeader =\n req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusterName =\n typeof clusterNameHeader === 'string' && clusterNameHeader.length > 0\n ? clusterNameHeader\n : 'unknown';\n const path = req.path || req.url.split('?')[0] || '';\n\n let clientDisconnected = false;\n const onSocketClose = () => {\n clientDisconnected = true;\n };\n req.socket.once('close', onSocketClose);\n\n let auditSession: ProxyAuditSession | undefined;\n let finishListenerAttached = false;\n try {\n auditSession = await ProxyAuditSession.start(this.auditor, {\n req,\n clusterName,\n method: req.method,\n path,\n logger: this.logger,\n });\n\n finishListenerAttached = await this.authorizeAndDispatch(\n req,\n res,\n next,\n permissionApi,\n auditSession,\n onSocketClose,\n () => clientDisconnected,\n );\n } catch (error) {\n req.socket.removeListener('close', onSocketClose);\n const err = error instanceof Error ? error : new Error(String(error));\n if (auditSession && !finishListenerAttached) {\n auditSession.finalize(err);\n }\n throw error;\n }\n };\n }\n\n /**\n * Authorizes the request, resolves the target cluster, and dispatches\n * to either the HTTP or WebSocket proxy path. Returns `true` when\n * HTTP finish listeners have been attached.\n */\n private async authorizeAndDispatch(\n req: Request,\n res: Response,\n next: NextFunction,\n permissionApi: PermissionsService,\n auditSession: ProxyAuditSession,\n onSocketClose: () => void,\n isClientDisconnected: () => boolean,\n ): Promise<boolean> {\n const authorizeResponse = await permissionApi.authorize(\n [{ permission: kubernetesProxyPermission }],\n { credentials: await this.httpAuth.credentials(req) },\n );\n\n if (authorizeResponse[0].result === AuthorizeResult.DENY) {\n req.socket.removeListener('close', onSocketClose);\n auditSession.finalize(new NotAllowedError('Unauthorized'));\n res.status(403).json({\n error: serializeError(new NotAllowedError('Unauthorized')),\n });\n return false;\n }\n\n if (isClientDisconnected()) {\n auditSession.finalize(\n new Error('Client disconnected before proxy dispatch'),\n );\n return false;\n }\n\n const prepared = await this.prepareProxyTarget(req);\n auditSession.setResolvedClusterName(prepared.cluster.name);\n (req as any)[PROXY_PREPARED_TARGET] = prepared;\n const middleware = this.getOrCreateMiddleware(prepared.cluster);\n\n if (isClientDisconnected()) {\n auditSession.finalize(\n new Error('Client disconnected before proxy dispatch'),\n );\n return false;\n }\n\n return KubernetesProxy.dispatchToProxy(\n req,\n res,\n next,\n middleware,\n auditSession,\n onSocketClose,\n );\n }\n\n /**\n * Resolves the cluster, credentials, and target URL for a proxy request\n * before dispatching to the middleware. This ensures all async preparation\n * completes under the caller's try/catch, avoiding unhandled rejections\n * from http-proxy-middleware's fire-and-forget upgrade() path.\n */\n private async prepareProxyTarget(req: Request): Promise<PreparedProxyTarget> {\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n\n const { bufferFromFileOrString } = await import('@kubernetes/client-node');\n\n const target: any = {\n protocol: url.protocol,\n host: url.hostname,\n port: url.port,\n ca: bufferFromFileOrString(cluster.caFile, cluster.caData)?.toString(),\n };\n\n const authHeader =\n req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase('en-US')];\n if (typeof authHeader === 'string') {\n req.headers.authorization = authHeader;\n } else {\n const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(\n req.headers,\n );\n\n const credential = await this.authStrategy.getCredential(\n cluster,\n authObj,\n );\n\n if (credential.type === 'bearer token') {\n req.headers.authorization = `Bearer ${credential.token}`;\n } else if (credential.type === 'x509 client certificate') {\n target.key = credential.key;\n target.cert = credential.cert;\n }\n }\n\n const requestPath = req.originalUrl || req.url || '';\n const rewrittenPath = requestPath.replace(\n new RegExp(\n `^${(req.baseUrl || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`,\n ),\n url.pathname || '',\n );\n\n return { proxyTarget: target, rewrittenPath, cluster };\n }\n\n // We create one middleware per remote cluster and hold on to them, because\n // the secure property isn't possible to decide on a per-request basis with a\n // single middleware instance. Target resolution and path rewriting are handled\n // per-request via the PROXY_PREPARED_TARGET symbol stashed on each req by\n // prepareProxyTarget. Entries are refreshed after a TTL or when cluster details\n // change.\n private getOrCreateMiddleware(cluster: ClusterDetails): RequestHandler {\n let middleware = this.middlewareCache.get(cluster);\n if (!middleware) {\n const logger = this.logger.child({ cluster: cluster.name });\n middleware = createProxyMiddleware({\n logProvider: () => ({\n log: logger.info.bind(logger),\n debug: logger.debug.bind(logger),\n info: logger.info.bind(logger),\n warn: logger.warn.bind(logger),\n error: logger.error.bind(logger),\n }),\n // ws must be false to prevent http-proxy-middleware from auto-subscribing\n // to the server's 'upgrade' event, which would bypass Express routing and\n // skip audit event creation for WebSocket requests.\n ws: false,\n secure: !cluster.skipTLSVerify,\n changeOrigin: true,\n router: req => {\n return (req as any)[PROXY_PREPARED_TARGET]?.proxyTarget;\n },\n pathRewrite: (_path, req) => {\n return (req as any)[PROXY_PREPARED_TARGET]?.rewrittenPath ?? _path;\n },\n onError: (error, req, res) => {\n const wrappedError = new ForwardedError(\n `Cluster '${cluster.name}' request error`,\n error,\n );\n\n logger.error('Kubernetes proxy error', wrappedError);\n\n if (typeof (res as { status?: unknown }).status !== 'function') {\n return;\n }\n\n const body: ErrorResponseBody = {\n error: serializeError(wrappedError, {\n includeStack: process.env.NODE_ENV === 'development',\n }),\n request: { method: req.method, url: req.originalUrl },\n response: { statusCode: 500 },\n };\n res.status(500).json(body);\n },\n onProxyReqWs: ProxyAuditSession.handleWebSocketProxyReq,\n });\n this.middlewareCache.set(cluster, middleware);\n }\n return middleware;\n }\n\n /**\n * Branches to WebSocket upgrade or HTTP middleware dispatch and wires\n * up the appropriate audit listeners. Returns `true` when HTTP finish\n * listeners have been attached (used by the caller's catch block to\n * avoid double-finalizing).\n */\n private static dispatchToProxy(\n req: Request,\n res: Response,\n next: NextFunction,\n middleware: RequestHandler,\n auditSession: ProxyAuditSession,\n onSocketClose: () => void,\n ): boolean {\n const isWebSocketUpgrade =\n (req.header('connection') ?? '')\n .toLowerCase()\n .split(',')\n .some(t => t.trim() === 'upgrade') &&\n req.header('upgrade')?.toLowerCase() === 'websocket';\n\n if (isWebSocketUpgrade) {\n auditSession.prepareForWebSocketUpgrade(req, onSocketClose);\n\n try {\n middleware.upgrade!(req, req.socket, undefined);\n } catch (error) {\n req.socket.removeListener('close', onSocketClose);\n auditSession.finalize(\n error instanceof Error ? error : new Error(String(error)),\n );\n throw error;\n }\n return false;\n }\n\n // HTTP path: remove the early close listener; HTTP audit listeners\n // on the response take over from here.\n req.socket.removeListener('close', onSocketClose);\n auditSession.attachToHttpResponse(res);\n middleware(req, res, next);\n return true;\n }\n\n private async getClusterForRequest(req: Request): Promise<ClusterDetails> {\n const { KubeConfig } = await import('@kubernetes/client-node');\n\n const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusters = await this.clusterSupplier.getClusters({\n credentials: await this.httpAuth.credentials(req),\n });\n\n if (!clusters || clusters.length <= 0) {\n throw new NotFoundError(`No Clusters configured`);\n }\n\n const hasClusterNameHeader =\n typeof clusterName === 'string' && clusterName.length > 0;\n\n let cluster: ClusterDetails | undefined;\n\n if (hasClusterNameHeader) {\n cluster = clusters.find(c => c.name === clusterName);\n } else if (clusters.length === 1) {\n cluster = clusters.at(0);\n }\n\n if (!cluster) {\n throw new NotFoundError(`Cluster '${clusterName}' not found`);\n }\n\n const authProvider =\n cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (\n authProvider === 'serviceAccount' &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH) &&\n !cluster.authMetadata.serviceAccountToken\n ) {\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster;\n\n const url = new URL(clusterFromKubeConfig.server);\n cluster.url = clusterFromKubeConfig.server;\n if (url.protocol === 'https:') {\n cluster.caFile = clusterFromKubeConfig.caFile;\n }\n }\n\n return cluster;\n }\n\n private static authHeadersToKubernetesRequestAuth(\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n return Object.keys(originalHeaders)\n .filter(header => header.startsWith('backstage-kubernetes-authorization'))\n .map(header =>\n KubernetesProxy.headerToDictionary(header, originalHeaders),\n )\n .filter(headerAsDic => Object.keys(headerAsDic).length !== 0)\n .reduce(KubernetesProxy.combineHeaders, {});\n }\n\n private static headerToDictionary(\n header: string,\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n const obj: KubernetesRequestAuth = {};\n const headerSplit = header.split('-');\n if (headerSplit.length >= 4) {\n const framework = headerSplit[3].toLowerCase();\n if (headerSplit.length >= 5) {\n const provider = headerSplit.slice(4).join('-').toLowerCase();\n obj[framework] = { [provider]: originalHeaders[header] };\n } else {\n obj[framework] = originalHeaders[header];\n }\n }\n return obj;\n }\n\n private static combineHeaders(\n authObj: any,\n header: any,\n ): KubernetesRequestAuth {\n const framework = Object.keys(header)[0];\n\n if (authObj[framework]) {\n authObj[framework] = {\n ...authObj[framework],\n ...header[framework],\n };\n } else {\n authObj[framework] = header[framework];\n }\n\n return authObj;\n }\n}\n"],"names":["ProxyMiddlewareCache","resolveProxyMiddlewareCacheOptions","ProxyAuditSession","kubernetesProxyPermission","AuthorizeResult","NotAllowedError","serializeError","createProxyMiddleware","ForwardedError","NotFoundError","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fs","SERVICEACCOUNT_CA_PATH"],"mappings":"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"}
|
|
@@ -12,9 +12,12 @@ require('@azure/identity');
|
|
|
12
12
|
require('@google-cloud/container');
|
|
13
13
|
var DispatchStrategy = require('../auth/DispatchStrategy.cjs.js');
|
|
14
14
|
require('fs-extra');
|
|
15
|
+
var errors = require('@backstage/errors');
|
|
15
16
|
var resourcesRoutes = require('../routes/resourcesRoutes.cjs.js');
|
|
16
17
|
var KubernetesProxy = require('./KubernetesProxy.cjs.js');
|
|
17
18
|
var requirePermission = require('../auth/requirePermission.cjs.js');
|
|
19
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
20
|
+
var ProxyMiddlewareCache = require('./ProxyMiddlewareCache.cjs.js');
|
|
18
21
|
|
|
19
22
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
20
23
|
|
|
@@ -52,6 +55,7 @@ class KubernetesRouter {
|
|
|
52
55
|
);
|
|
53
56
|
return Router__default.default();
|
|
54
57
|
}
|
|
58
|
+
await this.warnForClustersWithSkipTLSVerify(logger, clusterSupplier);
|
|
55
59
|
const proxy = this.buildProxy(
|
|
56
60
|
logger,
|
|
57
61
|
clusterSupplier,
|
|
@@ -86,16 +90,26 @@ class KubernetesRouter {
|
|
|
86
90
|
const authStrategy = new DispatchStrategy.DispatchStrategy({
|
|
87
91
|
authStrategyMap
|
|
88
92
|
});
|
|
93
|
+
const middlewareCacheConfig = this.env.config.getOptionalConfig(
|
|
94
|
+
"kubernetes.proxy.middlewareCache"
|
|
95
|
+
);
|
|
96
|
+
const { ttlMs, maxSize } = ProxyMiddlewareCache.resolveProxyMiddlewareCacheOptions({
|
|
97
|
+
ttlMs: middlewareCacheConfig?.getOptionalNumber("ttl.milliseconds"),
|
|
98
|
+
maxSize: middlewareCacheConfig?.getOptionalNumber("maxSize")
|
|
99
|
+
});
|
|
89
100
|
return new KubernetesProxy.KubernetesProxy({
|
|
90
101
|
logger,
|
|
91
102
|
clusterSupplier,
|
|
92
103
|
authStrategy,
|
|
93
104
|
discovery,
|
|
94
|
-
httpAuth
|
|
105
|
+
httpAuth,
|
|
106
|
+
auditor: this.env.auditor,
|
|
107
|
+
middlewareCache: { ttlMs, maxSize }
|
|
95
108
|
});
|
|
96
109
|
}
|
|
97
110
|
buildDefaultRouter(objectsProvider, clusterSupplier, catalog, proxy, permissionApi, httpAuth, authStrategyMap) {
|
|
98
111
|
const logger = this.env.logger;
|
|
112
|
+
const auditor = this.env.auditor;
|
|
99
113
|
const router = Router__default.default();
|
|
100
114
|
router.use("/proxy", proxy.createRequestHandler({ permissionApi }));
|
|
101
115
|
router.use(express__default.default.json());
|
|
@@ -105,67 +119,112 @@ class KubernetesRouter {
|
|
|
105
119
|
})
|
|
106
120
|
);
|
|
107
121
|
router.post("/services/:serviceId", async (req, res) => {
|
|
108
|
-
await requirePermission.requirePermission(
|
|
109
|
-
permissionApi,
|
|
110
|
-
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
111
|
-
httpAuth,
|
|
112
|
-
req
|
|
113
|
-
);
|
|
114
122
|
const serviceId = req.params.serviceId;
|
|
115
123
|
const requestBody = req.body;
|
|
124
|
+
let entityRef;
|
|
125
|
+
if (requestBody?.entity) {
|
|
126
|
+
try {
|
|
127
|
+
entityRef = catalogModel.stringifyEntityRef(requestBody.entity);
|
|
128
|
+
} catch {
|
|
129
|
+
entityRef = void 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const auditorEvent = await auditor.createEvent({
|
|
133
|
+
eventId: "resource-fetch",
|
|
134
|
+
request: req,
|
|
135
|
+
meta: { queryType: "services", entityRef, serviceId }
|
|
136
|
+
});
|
|
116
137
|
try {
|
|
138
|
+
await requirePermission.requirePermission(
|
|
139
|
+
permissionApi,
|
|
140
|
+
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
141
|
+
httpAuth,
|
|
142
|
+
req
|
|
143
|
+
);
|
|
117
144
|
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
|
118
145
|
{
|
|
119
|
-
entity: requestBody
|
|
120
|
-
auth: requestBody
|
|
146
|
+
entity: requestBody?.entity,
|
|
147
|
+
auth: requestBody?.auth || {}
|
|
121
148
|
},
|
|
122
149
|
{ credentials: await httpAuth.credentials(req) }
|
|
123
150
|
);
|
|
124
151
|
res.json(response);
|
|
152
|
+
auditorEvent.success().catch(
|
|
153
|
+
(error) => logger.error(
|
|
154
|
+
"Failed to emit audit event resource-fetch (services)",
|
|
155
|
+
error
|
|
156
|
+
)
|
|
157
|
+
);
|
|
125
158
|
} catch (e) {
|
|
159
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
160
|
+
await auditorEvent.fail({ error: err });
|
|
161
|
+
if (e instanceof errors.NotAllowedError) {
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
126
164
|
logger.error(
|
|
127
|
-
`action=retrieveObjectsByServiceId service=${serviceId}, error
|
|
165
|
+
`action=retrieveObjectsByServiceId service=${serviceId}, error: ${err}`
|
|
128
166
|
);
|
|
129
|
-
res.
|
|
167
|
+
if (!res.headersSent) {
|
|
168
|
+
res.status(500).json({ error: err.message });
|
|
169
|
+
}
|
|
130
170
|
}
|
|
131
171
|
});
|
|
132
172
|
router.get("/clusters", async (req, res) => {
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
req
|
|
138
|
-
);
|
|
139
|
-
const credentials = await httpAuth.credentials(req);
|
|
140
|
-
const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {
|
|
141
|
-
credentials
|
|
142
|
-
});
|
|
143
|
-
res.json({
|
|
144
|
-
items: clusterDetails.map((cd) => {
|
|
145
|
-
const oidcTokenProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
|
|
146
|
-
const authProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
147
|
-
const strategy = authStrategyMap[authProvider];
|
|
148
|
-
let auth = {};
|
|
149
|
-
if (strategy) {
|
|
150
|
-
auth = strategy.presentAuthMetadata(cd.authMetadata);
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
name: cd.name,
|
|
154
|
-
title: cd.title,
|
|
155
|
-
dashboardUrl: cd.dashboardUrl,
|
|
156
|
-
authProvider,
|
|
157
|
-
...oidcTokenProvider && { oidcTokenProvider },
|
|
158
|
-
...auth && Object.keys(auth).length !== 0 && { auth }
|
|
159
|
-
};
|
|
160
|
-
})
|
|
173
|
+
const auditorEvent = await auditor.createEvent({
|
|
174
|
+
eventId: "cluster-fetch",
|
|
175
|
+
request: req,
|
|
176
|
+
meta: { queryType: "list" }
|
|
161
177
|
});
|
|
178
|
+
try {
|
|
179
|
+
await requirePermission.requirePermission(
|
|
180
|
+
permissionApi,
|
|
181
|
+
pluginKubernetesCommon.kubernetesClustersReadPermission,
|
|
182
|
+
httpAuth,
|
|
183
|
+
req
|
|
184
|
+
);
|
|
185
|
+
const credentials = await httpAuth.credentials(req);
|
|
186
|
+
const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {
|
|
187
|
+
credentials
|
|
188
|
+
});
|
|
189
|
+
res.json({
|
|
190
|
+
items: clusterDetails.map((cd) => {
|
|
191
|
+
const oidcTokenProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
|
|
192
|
+
const authProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
193
|
+
const strategy = authStrategyMap[authProvider];
|
|
194
|
+
let auth = {};
|
|
195
|
+
if (strategy) {
|
|
196
|
+
auth = strategy.presentAuthMetadata(cd.authMetadata);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name: cd.name,
|
|
200
|
+
title: cd.title,
|
|
201
|
+
dashboardUrl: cd.dashboardUrl,
|
|
202
|
+
authProvider,
|
|
203
|
+
...oidcTokenProvider && { oidcTokenProvider },
|
|
204
|
+
...auth && Object.keys(auth).length !== 0 && { auth }
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
auditorEvent.success().catch(
|
|
209
|
+
(error) => logger.error(
|
|
210
|
+
"Failed to emit audit event cluster-fetch (list)",
|
|
211
|
+
error
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
216
|
+
await auditorEvent.fail({ error: err });
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
162
219
|
});
|
|
163
220
|
resourcesRoutes.addResourceRoutesToRouter(
|
|
164
221
|
router,
|
|
165
222
|
catalog,
|
|
166
223
|
objectsProvider,
|
|
167
224
|
httpAuth,
|
|
168
|
-
permissionApi
|
|
225
|
+
permissionApi,
|
|
226
|
+
auditor,
|
|
227
|
+
logger
|
|
169
228
|
);
|
|
170
229
|
return router;
|
|
171
230
|
}
|
|
@@ -176,6 +235,23 @@ class KubernetesRouter {
|
|
|
176
235
|
);
|
|
177
236
|
return clusterDetails;
|
|
178
237
|
}
|
|
238
|
+
async warnForClustersWithSkipTLSVerify(logger, clusterSupplier) {
|
|
239
|
+
try {
|
|
240
|
+
const credentials = await this.env.auth.getOwnServiceCredentials();
|
|
241
|
+
const clusters = await clusterSupplier.getClusters({ credentials });
|
|
242
|
+
for (const cluster of clusters) {
|
|
243
|
+
if (cluster.skipTLSVerify) {
|
|
244
|
+
logger.warn(
|
|
245
|
+
`Cluster '${cluster.name}' is configured with skipTLSVerify: true; TLS certificate verification is disabled for Kubernetes API traffic to this cluster`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.warn(
|
|
251
|
+
`Failed to log skipTLSVerify warnings at startup: ${errors.toError(error).message}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
179
255
|
}
|
|
180
256
|
|
|
181
257
|
exports.KubernetesRouter = KubernetesRouter;
|