mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cleanup shell sessions
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
fb4dca8e58
commit
c3f956675a
@ -4,12 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "../common/ipc/cluster";
|
import "../common/ipc/cluster";
|
||||||
import type http from "http";
|
|
||||||
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||||
import type { Cluster } from "../common/cluster/cluster";
|
import type { Cluster } from "../common/cluster/cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { isErrnoException } from "../common/utils";
|
||||||
import { getClusterIdFromHost, isErrnoException } from "../common/utils";
|
|
||||||
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
@ -260,27 +258,6 @@ export class ClusterManager {
|
|||||||
cluster.disconnect();
|
cluster.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getClusterForRequest = (req: http.IncomingMessage): Cluster | undefined => {
|
|
||||||
if (!req.headers.host) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
|
||||||
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
|
|
||||||
const clusterId = req.url.split("/")[1];
|
|
||||||
const cluster = this.dependencies.store.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.dependencies.store.getById(getClusterIdFromHost(req.headers.host));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function catalogEntityFromCluster(cluster: Cluster) {
|
export function catalogEntityFromCluster(cluster: Cluster) {
|
||||||
|
|||||||
@ -72,6 +72,8 @@ export class Kubectl {
|
|||||||
version = new SemVer(Kubectl.bundledKubectlVersion);
|
version = new SemVer(Kubectl.bundledKubectlVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Parsed ${clusterVersion} as ${version}`);
|
||||||
|
|
||||||
const fromMajorMinor = kubectlMap.get(`${version.major}.${version.minor}`);
|
const fromMajorMinor = kubectlMap.get(`${version.major}.${version.minor}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
42
src/main/lens-proxy/get-cluster-for-request.injectable.ts
Normal file
42
src/main/lens-proxy/get-cluster-for-request.injectable.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IncomingMessage } from "http";
|
||||||
|
import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
|
||||||
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import { getClusterIdFromHost } from "../../common/utils";
|
||||||
|
import { apiKubePrefix } from "../../common/vars";
|
||||||
|
|
||||||
|
export type GetClusterForRequest = (req: IncomingMessage) => Cluster | undefined;
|
||||||
|
|
||||||
|
const getClusterForRequestInjectable = getInjectable({
|
||||||
|
id: "get-cluster-for-request",
|
||||||
|
instantiate: (di): GetClusterForRequest => {
|
||||||
|
const store = di.inject(clusterStoreInjectable);
|
||||||
|
|
||||||
|
return (req) => {
|
||||||
|
if (!req.headers.host) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
|
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
|
||||||
|
const clusterId = req.url.split("/")[1];
|
||||||
|
const cluster = store.getById(clusterId);
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.getById(getClusterIdFromHost(req.headers.host));
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getClusterForRequestInjectable;
|
||||||
@ -8,27 +8,27 @@ const lensProxyPortInjectable = getInjectable({
|
|||||||
id: "lens-proxy-port",
|
id: "lens-proxy-port",
|
||||||
|
|
||||||
instantiate: () => {
|
instantiate: () => {
|
||||||
let _portNumber: number;
|
let portNumber: number;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: () => {
|
get: () => {
|
||||||
if (!_portNumber) {
|
if (!portNumber) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Tried to access port number of LensProxy while it has not been set yet.",
|
"Tried to access port number of LensProxy while it has not been set yet.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _portNumber;
|
return portNumber;
|
||||||
},
|
},
|
||||||
|
|
||||||
set: (portNumber: number) => {
|
set: (port: number) => {
|
||||||
if (_portNumber) {
|
if (port) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Tried to set port number for LensProxy when it has already been set.",
|
"Tried to set port number for LensProxy when it has already been set.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_portNumber = portNumber;
|
portNumber = port;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,32 +4,24 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
|
||||||
import routerInjectable from "../router/router.injectable";
|
import routerInjectable from "../router/router.injectable";
|
||||||
import httpProxy from "http-proxy";
|
import httpProxy from "http-proxy";
|
||||||
import clusterManagerInjectable from "../cluster-manager.injectable";
|
import shellApiRequestInjectable from "./proxy-functions/shell/api-request.injectable";
|
||||||
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
|
||||||
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
|
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
|
||||||
|
import getClusterForRequestInjectable from "./get-cluster-for-request.injectable";
|
||||||
|
import kubeApiUpgradeRequestInjectable from "./proxy-functions/kube/api-upgrade-request.injectable";
|
||||||
|
|
||||||
const lensProxyInjectable = getInjectable({
|
const lensProxyInjectable = getInjectable({
|
||||||
id: "lens-proxy",
|
id: "lens-proxy",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => new LensProxy({
|
||||||
const clusterManager = di.inject(clusterManagerInjectable);
|
router: di.inject(routerInjectable),
|
||||||
const router = di.inject(routerInjectable);
|
proxy: httpProxy.createProxy(),
|
||||||
const shellApiRequest = di.inject(shellApiRequestInjectable);
|
kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable),
|
||||||
const proxy = httpProxy.createProxy();
|
shellApiRequest: di.inject(shellApiRequestInjectable),
|
||||||
const lensProxyPort = di.inject(lensProxyPortInjectable);
|
getClusterForRequest: di.inject(getClusterForRequestInjectable),
|
||||||
|
lensProxyPort: di.inject(lensProxyPortInjectable),
|
||||||
return new LensProxy({
|
}),
|
||||||
router,
|
|
||||||
proxy,
|
|
||||||
kubeApiUpgradeRequest,
|
|
||||||
shellApiRequest,
|
|
||||||
getClusterForRequest: clusterManager.getClusterForRequest,
|
|
||||||
lensProxyPort,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default lensProxyInjectable;
|
export default lensProxyInjectable;
|
||||||
|
|||||||
@ -11,21 +11,20 @@ import { apiPrefix, apiKubePrefix, contentSecurityPolicy } from "../../common/va
|
|||||||
import type { Router } from "../router/router";
|
import type { Router } from "../router/router";
|
||||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||||
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
||||||
import { getBoolean } from "../utils/parse-query";
|
import { getBoolean } from "../utils/parse-query";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import type { SetRequired } from "type-fest";
|
import type { SetRequired } from "type-fest";
|
||||||
|
import type { GetClusterForRequest } from "./get-cluster-for-request.injectable";
|
||||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
|
||||||
|
|
||||||
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
||||||
|
export type ProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
getClusterForRequest: GetClusterForRequest;
|
getClusterForRequest: GetClusterForRequest;
|
||||||
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
shellApiRequest: ProxyApiRequest;
|
||||||
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
kubeApiUpgradeRequest: ProxyApiRequest;
|
||||||
router: Router;
|
router: Router;
|
||||||
proxy: httpProxy;
|
proxy: httpProxy;
|
||||||
lensProxyPort: { set: (portNumber: number) => void };
|
lensProxyPort: { set: (portNumber: number) => void };
|
||||||
|
|||||||
@ -2,5 +2,4 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
export * from "./kube-api-upgrade-request";
|
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { chunk } from "lodash";
|
|
||||||
import type { ConnectionOptions } from "tls";
|
|
||||||
import { connect } from "tls";
|
|
||||||
import url from "url";
|
|
||||||
import { apiKubePrefix } from "../../../common/vars";
|
|
||||||
import type { ProxyApiRequestArgs } from "./types";
|
|
||||||
|
|
||||||
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
|
||||||
|
|
||||||
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
|
||||||
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
|
||||||
const pUrl = url.parse(proxyUrl);
|
|
||||||
const connectOpts: ConnectionOptions = {
|
|
||||||
port: pUrl.port ? parseInt(pUrl.port) : undefined,
|
|
||||||
host: pUrl.hostname ?? undefined,
|
|
||||||
ca: proxyCa,
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxySocket = connect(connectOpts, () => {
|
|
||||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
|
||||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
|
||||||
|
|
||||||
for (const [key, value] of chunk(req.rawHeaders, 2)) {
|
|
||||||
if (skipRawHeaders.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySocket.write(`${key}: ${value}\r\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySocket.write("\r\n");
|
|
||||||
proxySocket.write(head);
|
|
||||||
});
|
|
||||||
|
|
||||||
proxySocket.setKeepAlive(true);
|
|
||||||
socket.setKeepAlive(true);
|
|
||||||
proxySocket.setTimeout(0);
|
|
||||||
socket.setTimeout(0);
|
|
||||||
|
|
||||||
proxySocket.on("data", function (chunk) {
|
|
||||||
socket.write(chunk);
|
|
||||||
});
|
|
||||||
proxySocket.on("end", function () {
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
proxySocket.on("error", function () {
|
|
||||||
socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`);
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
socket.on("data", function (chunk) {
|
|
||||||
proxySocket.write(chunk);
|
|
||||||
});
|
|
||||||
socket.on("end", function () {
|
|
||||||
proxySocket.end();
|
|
||||||
});
|
|
||||||
socket.on("error", function () {
|
|
||||||
proxySocket.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chunk, pick } from "lodash";
|
||||||
|
import type { ConnectionOptions } from "tls";
|
||||||
|
import { connect } from "tls";
|
||||||
|
import url from "url";
|
||||||
|
import { apiKubePrefix } from "../../../../common/vars";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import loggerInjectable from "../../../../common/logger.injectable";
|
||||||
|
import type { ProxyApiRequest } from "../../lens-proxy";
|
||||||
|
|
||||||
|
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||||
|
|
||||||
|
const kubeApiUpgradeRequestInjectable = getInjectable({
|
||||||
|
id: "kube-api-upgrade-request",
|
||||||
|
instantiate: (di): ProxyApiRequest => {
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return async ({ req, socket, head, cluster }) => {
|
||||||
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
|
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
||||||
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
|
const pUrl = url.parse(proxyUrl);
|
||||||
|
const connectOpts: ConnectionOptions = {
|
||||||
|
port: pUrl.port ? parseInt(pUrl.port) : undefined,
|
||||||
|
host: pUrl.hostname ?? undefined,
|
||||||
|
ca: proxyCa,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug(`[KUBE-API-UPGRADE]: connecting for clusterId=${cluster.id} for url=${proxyUrl}`, pick(connectOpts, "port", "host"));
|
||||||
|
|
||||||
|
const proxySocket = connect(connectOpts, () => {
|
||||||
|
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
||||||
|
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
||||||
|
|
||||||
|
for (const [key, value] of chunk(req.rawHeaders, 2)) {
|
||||||
|
if (skipRawHeaders.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxySocket.write(`${key}: ${value}\r\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
proxySocket.write("\r\n");
|
||||||
|
proxySocket.write(head);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxySocket.setKeepAlive(true);
|
||||||
|
socket.setKeepAlive(true);
|
||||||
|
proxySocket.setTimeout(0);
|
||||||
|
socket.setTimeout(0);
|
||||||
|
|
||||||
|
proxySocket.on("data", function (chunk) {
|
||||||
|
socket.write(chunk);
|
||||||
|
});
|
||||||
|
proxySocket.on("end", function () {
|
||||||
|
socket.end();
|
||||||
|
});
|
||||||
|
proxySocket.on("error", function () {
|
||||||
|
socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`);
|
||||||
|
socket.end();
|
||||||
|
});
|
||||||
|
socket.on("data", function (chunk) {
|
||||||
|
proxySocket.write(chunk);
|
||||||
|
});
|
||||||
|
socket.on("end", function () {
|
||||||
|
proxySocket.end();
|
||||||
|
});
|
||||||
|
socket.on("error", function () {
|
||||||
|
proxySocket.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default kubeApiUpgradeRequestInjectable;
|
||||||
@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import { shellApiRequest } from "./shell-api-request";
|
|
||||||
import createShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
|
|
||||||
import shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
|
|
||||||
import clusterManagerInjectable from "../../../cluster-manager.injectable";
|
|
||||||
|
|
||||||
const shellApiRequestInjectable = getInjectable({
|
|
||||||
id: "shell-api-request",
|
|
||||||
|
|
||||||
instantiate: (di) => shellApiRequest({
|
|
||||||
createShellSession: di.inject(createShellSessionInjectable),
|
|
||||||
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
|
|
||||||
clusterManager: di.inject(clusterManagerInjectable),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default shellApiRequestInjectable;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import logger from "../../../logger";
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import { Server as WebSocketServer } from "ws";
|
|
||||||
import type { ProxyApiRequestArgs } from "../types";
|
|
||||||
import type { ClusterManager } from "../../../cluster-manager";
|
|
||||||
import URLParse from "url-parse";
|
|
||||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
|
||||||
import type { ClusterId } from "../../../../common/cluster-types";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
|
|
||||||
|
|
||||||
createShellSession: (args: {
|
|
||||||
webSocket: WebSocket;
|
|
||||||
cluster: Cluster;
|
|
||||||
tabId: string;
|
|
||||||
nodeName?: string;
|
|
||||||
}) => { open: () => Promise<void> };
|
|
||||||
|
|
||||||
clusterManager: ClusterManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shellApiRequest = ({ createShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
|
|
||||||
const cluster = clusterManager.getClusterForRequest(req);
|
|
||||||
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
|
|
||||||
|
|
||||||
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
|
|
||||||
socket.write("Invalid shell request");
|
|
||||||
|
|
||||||
return void socket.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = new WebSocketServer({ noServer: true });
|
|
||||||
|
|
||||||
ws.handleUpgrade(req, socket, head, (webSocket) => {
|
|
||||||
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
|
|
||||||
|
|
||||||
shell.open()
|
|
||||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import { ShellRequestAuthenticator } from "./shell-request-authenticator";
|
|
||||||
|
|
||||||
const shellRequestAuthenticatorInjectable = getInjectable({
|
|
||||||
id: "shell-request-authenticator",
|
|
||||||
|
|
||||||
instantiate: () => {
|
|
||||||
const authenticator = new ShellRequestAuthenticator();
|
|
||||||
|
|
||||||
authenticator.init();
|
|
||||||
|
|
||||||
return authenticator;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default shellRequestAuthenticatorInjectable;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getOrInsertMap } from "../../../../../common/utils";
|
|
||||||
import type { ClusterId } from "../../../../../common/cluster-types";
|
|
||||||
import { ipcMainHandle } from "../../../../../common/ipc";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
const randomBytes = promisify(crypto.randomBytes);
|
|
||||||
|
|
||||||
export class ShellRequestAuthenticator {
|
|
||||||
private tokens = new Map<ClusterId, Map<string, Uint8Array>>();
|
|
||||||
|
|
||||||
init() {
|
|
||||||
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
|
|
||||||
const authToken = Uint8Array.from(await randomBytes(128));
|
|
||||||
const forCluster = getOrInsertMap(this.tokens, clusterId);
|
|
||||||
|
|
||||||
forCluster.set(tabId, authToken);
|
|
||||||
|
|
||||||
return authToken;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticates a single use token for creating a new shell
|
|
||||||
* @param clusterId The `ClusterId` for the shell
|
|
||||||
* @param tabId The ID for the shell
|
|
||||||
* @param token The value that is being presented as a one time authentication token
|
|
||||||
* @returns `true` if `token` was valid, false otherwise
|
|
||||||
*/
|
|
||||||
authenticate = (clusterId: ClusterId, tabId: string, token: string | undefined): boolean => {
|
|
||||||
const clusterTokens = this.tokens.get(clusterId);
|
|
||||||
|
|
||||||
if (!clusterTokens || !tabId || !token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authToken = clusterTokens.get(tabId);
|
|
||||||
const buf = Uint8Array.from(Buffer.from(token, "base64"));
|
|
||||||
|
|
||||||
if (authToken instanceof Uint8Array && authToken.length === buf.length && crypto.timingSafeEqual(authToken, buf)) {
|
|
||||||
// remove the token because it is a single use token
|
|
||||||
clusterTokens.delete(tabId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import openShellSessionInjectable from "../../../shell-session/open-shell-session.injectable";
|
||||||
|
import loggerInjectable from "../../../../common/logger.injectable";
|
||||||
|
import type { ProxyApiRequest } from "../../lens-proxy";
|
||||||
|
import getClusterForRequestInjectable from "../../get-cluster-for-request.injectable";
|
||||||
|
import { Server } from "ws";
|
||||||
|
import authenticateRequestInjectable from "./authenticate-request.injectable";
|
||||||
|
|
||||||
|
const shellApiRequestInjectable = getInjectable({
|
||||||
|
id: "shell-api-request",
|
||||||
|
|
||||||
|
instantiate: (di): ProxyApiRequest => {
|
||||||
|
const openShellSession = di.inject(openShellSessionInjectable);
|
||||||
|
const authenticateRequest = di.inject(authenticateRequestInjectable);
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const getClusterForRequest = di.inject(getClusterForRequestInjectable);
|
||||||
|
|
||||||
|
return ({ req, socket, head }) => {
|
||||||
|
const cluster = getClusterForRequest(req);
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const nodeName = searchParams.get("node") || undefined;
|
||||||
|
const shellToken = searchParams.get("shellToken");
|
||||||
|
const tabId = searchParams.get("id");
|
||||||
|
|
||||||
|
if (!tabId || !cluster || !shellToken || !authenticateRequest(cluster.id, tabId, shellToken)) {
|
||||||
|
socket.write("Invalid shell request");
|
||||||
|
socket.end();
|
||||||
|
} else {
|
||||||
|
new Server({ noServer: true })
|
||||||
|
.handleUpgrade(req, socket, head, (websocket) => {
|
||||||
|
openShellSession({ websocket, cluster, tabId, nodeName })
|
||||||
|
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default shellApiRequestInjectable;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import shellRequestAuthenticatorInjectable from "./request-authenticator.injectable";
|
||||||
|
|
||||||
|
const authenticateRequestInjectable = getInjectable({
|
||||||
|
id: "authenticate-request",
|
||||||
|
instantiate: (di) => di.inject(shellRequestAuthenticatorInjectable).authenticate,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authenticateRequestInjectable;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { timingSafeEqual } from "crypto";
|
||||||
|
import type { ClusterId } from "../../../../common/cluster-types";
|
||||||
|
import { getOrInsertMap } from "../../../../common/utils";
|
||||||
|
import randomBytesInjectable from "../../../utils/random-bytes.injectable";
|
||||||
|
|
||||||
|
export interface ShellRequestAuthenticator {
|
||||||
|
authenticate(clusterId: ClusterId, tabId: string, token: string | undefined): boolean;
|
||||||
|
getTokenFor(clusterId: ClusterId, tabId: string): Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shellRequestAuthenticatorInjectable = getInjectable({
|
||||||
|
id: "shell-request-authenticator",
|
||||||
|
|
||||||
|
instantiate: (di): ShellRequestAuthenticator => {
|
||||||
|
const randomBytes = di.inject(randomBytesInjectable);
|
||||||
|
const tokens = new Map<ClusterId, Map<string, Uint8Array>>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticate: (clusterId, tabId, token) => {
|
||||||
|
const clusterTokens = tokens.get(clusterId);
|
||||||
|
|
||||||
|
if (!clusterTokens || !tabId || !token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authToken = clusterTokens.get(tabId);
|
||||||
|
const buf = Uint8Array.from(Buffer.from(token, "base64"));
|
||||||
|
|
||||||
|
if (authToken instanceof Uint8Array && authToken.length === buf.length && timingSafeEqual(authToken, buf)) {
|
||||||
|
// remove the token because it is a single use token
|
||||||
|
clusterTokens.delete(tabId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getTokenFor: async (clusterId, tabId) => {
|
||||||
|
const authToken = Uint8Array.from(await randomBytes(128));
|
||||||
|
const forCluster = getOrInsertMap(tokens, clusterId);
|
||||||
|
|
||||||
|
forCluster.set(tabId, authToken);
|
||||||
|
|
||||||
|
return authToken;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default shellRequestAuthenticatorInjectable;
|
||||||
@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import localShellSessionInjectable from "./local-shell-session/local-shell-session.injectable";
|
|
||||||
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable";
|
|
||||||
|
|
||||||
interface Args {
|
|
||||||
webSocket: WebSocket;
|
|
||||||
cluster: Cluster;
|
|
||||||
tabId: string;
|
|
||||||
nodeName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createShellSessionInjectable = getInjectable({
|
|
||||||
id: "create-shell-session",
|
|
||||||
|
|
||||||
instantiate:
|
|
||||||
(di) =>
|
|
||||||
({ nodeName, ...rest }: Args) =>
|
|
||||||
!nodeName
|
|
||||||
? di.inject(localShellSessionInjectable, rest)
|
|
||||||
: di.inject(nodeShellSessionInjectable, { nodeName, ...rest }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default createShellSessionInjectable;
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import { LocalShellSession } from "./local-shell-session";
|
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
|
||||||
import terminalShellEnvModifiersInjectable from "../shell-env-modifier/terminal-shell-env-modify.injectable";
|
|
||||||
|
|
||||||
interface InstantiationParameter {
|
|
||||||
webSocket: WebSocket;
|
|
||||||
cluster: Cluster;
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localShellSessionInjectable = getInjectable({
|
|
||||||
id: "local-shell-session",
|
|
||||||
|
|
||||||
instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => {
|
|
||||||
const createKubectl = di.inject(createKubectlInjectable);
|
|
||||||
const localShellEnvModify = di.inject(terminalShellEnvModifiersInjectable);
|
|
||||||
|
|
||||||
const kubectl = createKubectl(cluster.version);
|
|
||||||
|
|
||||||
return new LocalShellSession(localShellEnvModify, kubectl, webSocket, cluster, tabId);
|
|
||||||
},
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.transient,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default localShellSessionInjectable;
|
|
||||||
@ -3,24 +3,26 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { TerminalShellEnvModify } from "../shell-env-modifier/terminal-shell-env-modify.injectable";
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ShellSessionArgs } from "../shell-session";
|
||||||
import { ShellSession } from "../shell-session";
|
import { ShellSession } from "../shell-session";
|
||||||
import type { Kubectl } from "../../kubectl/kubectl";
|
|
||||||
import { baseBinariesDir } from "../../../common/vars";
|
export interface LocalShellSessionDependencies {
|
||||||
|
terminalShellEnvModify: TerminalShellEnvModify;
|
||||||
|
readonly baseBundeledBinariesDirectory: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class LocalShellSession extends ShellSession {
|
export class LocalShellSession extends ShellSession {
|
||||||
ShellType = "shell";
|
ShellType = "shell";
|
||||||
|
|
||||||
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
|
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
|
||||||
super(kubectl, websocket, cluster, terminalId);
|
super(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPathEntries(): string[] {
|
protected getPathEntries(): string[] {
|
||||||
return [baseBinariesDir.get()];
|
return [this.dependencies.baseBundeledBinariesDirectory];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get cwd(): string | undefined {
|
protected get cwd(): string | undefined {
|
||||||
@ -31,7 +33,7 @@ export class LocalShellSession extends ShellSession {
|
|||||||
let env = await this.getCachedShellEnv();
|
let env = await this.getCachedShellEnv();
|
||||||
|
|
||||||
// extensions can modify the env
|
// extensions can modify the env
|
||||||
env = this.shellEnvModify(this.cluster.id, env);
|
env = this.dependencies.terminalShellEnvModify(this.cluster.id, env);
|
||||||
|
|
||||||
const shell = env.PTYSHELL;
|
const shell = env.PTYSHELL;
|
||||||
|
|
||||||
@ -50,11 +52,11 @@ export class LocalShellSession extends ShellSession {
|
|||||||
|
|
||||||
switch(path.basename(shell)) {
|
switch(path.basename(shell)) {
|
||||||
case "powershell.exe":
|
case "powershell.exe":
|
||||||
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${baseBinariesDir.get()};$Env:PATH"}`];
|
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.baseBundeledBinariesDirectory};$Env:PATH"}`];
|
||||||
case "bash":
|
case "bash":
|
||||||
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
||||||
case "fish":
|
case "fish":
|
||||||
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${baseBinariesDir.get()}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.baseBundeledBinariesDirectory}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return ["--login"];
|
return ["--login"];
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { LocalShellSessionDependencies } from "./local-shell-session";
|
||||||
|
import { LocalShellSession } from "./local-shell-session";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import type WebSocket from "ws";
|
||||||
|
import terminalShellEnvModifyInjectable from "../shell-env-modifier/terminal-shell-env-modify.injectable";
|
||||||
|
import baseBundeledBinariesDirectoryInjectable from "../../../common/vars/base-bundled-binaries-dir.injectable";
|
||||||
|
import type { Kubectl } from "../../kubectl/kubectl";
|
||||||
|
|
||||||
|
export interface OpenLocalShellSessionArgs {
|
||||||
|
websocket: WebSocket;
|
||||||
|
cluster: Cluster;
|
||||||
|
tabId: string;
|
||||||
|
kubectl: Kubectl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenLocalShellSession = (args: OpenLocalShellSessionArgs) => Promise<void>;
|
||||||
|
|
||||||
|
const openLocalShellSessionInjectable = getInjectable({
|
||||||
|
id: "open-local-shell-session",
|
||||||
|
instantiate: (di): OpenLocalShellSession => {
|
||||||
|
const deps: LocalShellSessionDependencies = {
|
||||||
|
terminalShellEnvModify: di.inject(terminalShellEnvModifyInjectable),
|
||||||
|
baseBundeledBinariesDirectory: di.inject(baseBundeledBinariesDirectoryInjectable),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (args) => new LocalShellSession(deps, args).open();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default openLocalShellSessionInjectable;
|
||||||
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
|
|
||||||
import { NodeShellSession } from "./node-shell-session";
|
|
||||||
|
|
||||||
interface InstantiationParameter {
|
|
||||||
webSocket: WebSocket;
|
|
||||||
cluster: Cluster;
|
|
||||||
tabId: string;
|
|
||||||
nodeName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeShellSessionInjectable = getInjectable({
|
|
||||||
id: "node-shell-session",
|
|
||||||
|
|
||||||
instantiate: (di, { cluster, tabId, webSocket, nodeName }: InstantiationParameter) => {
|
|
||||||
const createKubectl = di.inject(createKubectlInjectable);
|
|
||||||
|
|
||||||
const kubectl = createKubectl(cluster.version);
|
|
||||||
|
|
||||||
return new NodeShellSession(nodeName, kubectl, webSocket, cluster, tabId);
|
|
||||||
},
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.transient,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default nodeShellSessionInjectable;
|
|
||||||
@ -3,28 +3,32 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type WebSocket from "ws";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
import { Watch, CoreV1Api } from "@kubernetes/client-node";
|
||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { ShellSessionArgs } from "../shell-session";
|
||||||
import { ShellOpenError, ShellSession } from "../shell-session";
|
import { ShellOpenError, ShellSession } from "../shell-session";
|
||||||
import { get, once } from "lodash";
|
import { get, once } from "lodash";
|
||||||
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
|
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
|
||||||
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
|
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
import type { Kubectl } from "../../kubectl/kubectl";
|
|
||||||
import { TerminalChannels } from "../../../common/terminal/channels";
|
import { TerminalChannels } from "../../../common/terminal/channels";
|
||||||
|
|
||||||
|
export interface NodeShellSessionArgs extends ShellSessionArgs {
|
||||||
|
nodeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class NodeShellSession extends ShellSession {
|
export class NodeShellSession extends ShellSession {
|
||||||
ShellType = "node-shell";
|
ShellType = "node-shell";
|
||||||
|
|
||||||
protected readonly podName = `node-shell-${uuid()}`;
|
protected readonly podName = `node-shell-${uuid()}`;
|
||||||
|
|
||||||
protected readonly cwd: string | undefined = undefined;
|
protected readonly cwd: string | undefined = undefined;
|
||||||
|
protected readonly nodeName: string;
|
||||||
|
|
||||||
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
|
constructor({ nodeName, ...args }: NodeShellSessionArgs) {
|
||||||
super(kubectl, socket, cluster, terminalId);
|
super(args);
|
||||||
|
this.nodeName = nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import type WebSocket from "ws";
|
||||||
|
import { NodeShellSession } from "./node-shell-session";
|
||||||
|
import type { Kubectl } from "../../kubectl/kubectl";
|
||||||
|
|
||||||
|
export interface OpenNodeShellSessionArgs {
|
||||||
|
websocket: WebSocket;
|
||||||
|
cluster: Cluster;
|
||||||
|
tabId: string;
|
||||||
|
nodeName: string;
|
||||||
|
kubectl: Kubectl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenNodeShellSession = (args: OpenNodeShellSessionArgs) => Promise<void>;
|
||||||
|
|
||||||
|
const openNodeShellSessionInjectable = getInjectable({
|
||||||
|
id: "node-shell-session",
|
||||||
|
|
||||||
|
instantiate: (): OpenNodeShellSession => (args) => new NodeShellSession(args).open(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default openNodeShellSessionInjectable;
|
||||||
39
src/main/shell-session/open-shell-session.injectable.ts
Normal file
39
src/main/shell-session/open-shell-session.injectable.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import type WebSocket from "ws";
|
||||||
|
import openLocalShellSessionInjectable from "./local-shell-session/open-local-shell-session.injectable";
|
||||||
|
import openNodeShellSessionInjectable from "./node-shell-session/open-node-shell-session.injectable";
|
||||||
|
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
|
||||||
|
|
||||||
|
export interface OpenShellSessionArgs {
|
||||||
|
websocket: WebSocket;
|
||||||
|
cluster: Cluster;
|
||||||
|
tabId: string;
|
||||||
|
nodeName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
|
||||||
|
|
||||||
|
const openShellSessionInjectable = getInjectable({
|
||||||
|
id: "open-shell-session",
|
||||||
|
|
||||||
|
instantiate: (di): OpenShellSession => {
|
||||||
|
const openLocalShellSession = di.inject(openLocalShellSessionInjectable);
|
||||||
|
const openNodeShellSession = di.inject(openNodeShellSessionInjectable);
|
||||||
|
const createKubectl = di.inject(createKubectlInjectable);
|
||||||
|
|
||||||
|
return ({ nodeName, cluster, ...args }) => {
|
||||||
|
const kubectl = createKubectl(cluster.version);
|
||||||
|
|
||||||
|
return nodeName
|
||||||
|
? openNodeShellSession({ nodeName, cluster, kubectl, ...args })
|
||||||
|
: openLocalShellSession({ cluster, kubectl, ...args });
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default openShellSessionInjectable;
|
||||||
@ -9,4 +9,4 @@ export interface ShellEnvContext {
|
|||||||
catalogEntity: CatalogEntity;
|
catalogEntity: CatalogEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShellEnvModifier = (ctx: ShellEnvContext, env: Record<string, string | undefined>) => Record<string, string | undefined>;
|
export type ShellEnvModifier = (ctx: ShellEnvContext, env: Partial<Record<string, string>>) => Partial<Record<string, string>>;
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
||||||
|
import { isDefined } from "../../../common/utils";
|
||||||
|
|
||||||
|
const terminalShellEnvModifiersInjectable = getInjectable({
|
||||||
|
id: "terminal-shell-env-modifiers",
|
||||||
|
instantiate: (di) => {
|
||||||
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
|
|
||||||
|
return computed(() => (
|
||||||
|
extensions.get()
|
||||||
|
.map((extension) => extension.terminalShellEnvModifier)
|
||||||
|
.filter(isDefined)
|
||||||
|
));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default terminalShellEnvModifiersInjectable;
|
||||||
@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IComputedValue } from "mobx";
|
|
||||||
import { computed } from "mobx";
|
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
|
||||||
import { isDefined } from "../../../common/utils";
|
|
||||||
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
|
|
||||||
import type { CatalogEntityRegistry } from "../../catalog";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
extensions: IComputedValue<LensMainExtension[]>;
|
|
||||||
catalogEntityRegistry: CatalogEntityRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const terminalShellEnvModify = ({ extensions, catalogEntityRegistry }: Dependencies) =>
|
|
||||||
(clusterId: ClusterId, env: Record<string, string | undefined>) => {
|
|
||||||
const terminalShellEnvModifiers = computed(() => (
|
|
||||||
extensions.get()
|
|
||||||
.map((extension) => extension.terminalShellEnvModifier)
|
|
||||||
.filter(isDefined)
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (terminalShellEnvModifiers.length === 0) {
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entity = catalogEntityRegistry.findById(clusterId);
|
|
||||||
|
|
||||||
if (entity) {
|
|
||||||
const ctx = { catalogEntity: entity };
|
|
||||||
|
|
||||||
// clone it so the passed value is not also modified
|
|
||||||
env = JSON.parse(JSON.stringify(env));
|
|
||||||
env = terminalShellEnvModifiers.reduce((env, modifier) => modifier(ctx, env), env);
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
};
|
|
||||||
@ -4,18 +4,39 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
import { terminalShellEnvModify } from "./terminal-shell-env-modifiers";
|
|
||||||
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
|
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
|
||||||
|
import terminalShellEnvModifiersInjectable from "./terminal-shell-env-modifiers.injectable";
|
||||||
|
|
||||||
|
export type TerminalShellEnvModify = (clusterId: ClusterId, env: Partial<Record<string, string>>) => Partial<Record<string, string>>;
|
||||||
|
|
||||||
const terminalShellEnvModifyInjectable = getInjectable({
|
const terminalShellEnvModifyInjectable = getInjectable({
|
||||||
id: "terminal-shell-env-modify",
|
id: "terminal-shell-env-modify",
|
||||||
|
|
||||||
instantiate: (di) =>
|
instantiate: (di): TerminalShellEnvModify => {
|
||||||
terminalShellEnvModify({
|
const terminalShellEnvModifiers = di.inject(terminalShellEnvModifiersInjectable);
|
||||||
extensions: di.inject(mainExtensionsInjectable),
|
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||||
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
|
|
||||||
}),
|
return (clusterId, env) => {
|
||||||
|
const modifiers = terminalShellEnvModifiers.get();
|
||||||
|
|
||||||
|
if (modifiers.length === 0) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = entityRegistry.findById(clusterId);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
const ctx = { catalogEntity: entity };
|
||||||
|
|
||||||
|
// clone it so the passed value is not also modified
|
||||||
|
env = JSON.parse(JSON.stringify(env));
|
||||||
|
env = modifiers.reduce((env, modifier) => modifier(ctx, env), env);
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default terminalShellEnvModifyInjectable;
|
export default terminalShellEnvModifyInjectable;
|
||||||
|
|||||||
@ -104,6 +104,13 @@ export enum WebSocketCloseEvent {
|
|||||||
TlsHandshake = 1015,
|
TlsHandshake = 1015,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShellSessionArgs {
|
||||||
|
kubectl: Kubectl;
|
||||||
|
websocket: WebSocket;
|
||||||
|
cluster: Cluster;
|
||||||
|
tabId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class ShellSession {
|
export abstract class ShellSession {
|
||||||
abstract readonly ShellType: string;
|
abstract readonly ShellType: string;
|
||||||
|
|
||||||
@ -130,8 +137,11 @@ export abstract class ShellSession {
|
|||||||
protected readonly kubectlBinDirP: Promise<string>;
|
protected readonly kubectlBinDirP: Promise<string>;
|
||||||
protected readonly kubeconfigPathP: Promise<string>;
|
protected readonly kubeconfigPathP: Promise<string>;
|
||||||
protected readonly terminalId: string;
|
protected readonly terminalId: string;
|
||||||
|
protected readonly kubectl: Kubectl;
|
||||||
|
protected readonly websocket: WebSocket;
|
||||||
|
protected readonly cluster: Cluster;
|
||||||
|
|
||||||
protected abstract get cwd(): string | undefined;
|
protected abstract readonly cwd: string | undefined;
|
||||||
|
|
||||||
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string | undefined>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string | undefined>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
|
||||||
const resume = ShellSession.processes.has(this.terminalId);
|
const resume = ShellSession.processes.has(this.terminalId);
|
||||||
@ -152,10 +162,13 @@ export abstract class ShellSession {
|
|||||||
return { shellProcess, resume };
|
return { shellProcess, resume };
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(protected readonly kubectl: Kubectl, protected readonly websocket: WebSocket, protected readonly cluster: Cluster, terminalId: string) {
|
constructor({ cluster, kubectl, tabId, websocket }: ShellSessionArgs) {
|
||||||
|
this.cluster = cluster;
|
||||||
|
this.kubectl = kubectl;
|
||||||
|
this.websocket = websocket;
|
||||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||||
this.kubectlBinDirP = this.kubectl.binDir();
|
this.kubectlBinDirP = this.kubectl.binDir();
|
||||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
this.terminalId = `${cluster.id}:${tabId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected send(message: TerminalMessage): void {
|
protected send(message: TerminalMessage): void {
|
||||||
|
|||||||
15
src/main/utils/random-bytes.injectable.ts
Normal file
15
src/main/utils/random-bytes.injectable.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const randomBytesInjectable = getInjectable({
|
||||||
|
id: "random-bytes",
|
||||||
|
instantiate: () => promisify(randomBytes),
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default randomBytesInjectable;
|
||||||
Loading…
Reference in New Issue
Block a user