1
0
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:
Sebastian Malton 2022-05-16 16:44:17 -04:00
parent fb4dca8e58
commit c3f956675a
30 changed files with 462 additions and 424 deletions

View File

@ -4,12 +4,10 @@
*/
import "../common/ipc/cluster";
import type http from "http";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../common/cluster/cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, isErrnoException } from "../common/utils";
import { isErrnoException } from "../common/utils";
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc";
@ -260,27 +258,6 @@ export class ClusterManager {
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) {

View File

@ -72,6 +72,8 @@ export class Kubectl {
version = new SemVer(Kubectl.bundledKubectlVersion);
}
console.log(`Parsed ${clusterVersion} as ${version}`);
const fromMajorMinor = kubectlMap.get(`${version.major}.${version.minor}`);
/**

View 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;

View File

@ -8,27 +8,27 @@ const lensProxyPortInjectable = getInjectable({
id: "lens-proxy-port",
instantiate: () => {
let _portNumber: number;
let portNumber: number;
return {
get: () => {
if (!_portNumber) {
if (!portNumber) {
throw new Error(
"Tried to access port number of LensProxy while it has not been set yet.",
);
}
return _portNumber;
return portNumber;
},
set: (portNumber: number) => {
if (_portNumber) {
set: (port: number) => {
if (port) {
throw new Error(
"Tried to set port number for LensProxy when it has already been set.",
);
}
_portNumber = portNumber;
portNumber = port;
},
};
},

View File

@ -4,32 +4,24 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { LensProxy } from "./lens-proxy";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import routerInjectable from "../router/router.injectable";
import httpProxy from "http-proxy";
import clusterManagerInjectable from "../cluster-manager.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell/api-request.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({
id: "lens-proxy",
instantiate: (di) => {
const clusterManager = di.inject(clusterManagerInjectable);
const router = di.inject(routerInjectable);
const shellApiRequest = di.inject(shellApiRequestInjectable);
const proxy = httpProxy.createProxy();
const lensProxyPort = di.inject(lensProxyPortInjectable);
return new LensProxy({
router,
proxy,
kubeApiUpgradeRequest,
shellApiRequest,
getClusterForRequest: clusterManager.getClusterForRequest,
lensProxyPort,
});
},
instantiate: (di) => new LensProxy({
router: di.inject(routerInjectable),
proxy: httpProxy.createProxy(),
kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable),
shellApiRequest: di.inject(shellApiRequestInjectable),
getClusterForRequest: di.inject(getClusterForRequestInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable),
}),
});
export default lensProxyInjectable;

View File

@ -11,21 +11,20 @@ import { apiPrefix, apiKubePrefix, contentSecurityPolicy } from "../../common/va
import type { Router } from "../router/router";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import logger from "../logger";
import type { Cluster } from "../../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions";
import { appEventBus } from "../../common/app-event-bus/event-bus";
import { getBoolean } from "../utils/parse-query";
import assert from "assert";
import type { SetRequired } from "type-fest";
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
import type { GetClusterForRequest } from "./get-cluster-for-request.injectable";
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
export type ProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise<void>;
interface Dependencies {
getClusterForRequest: GetClusterForRequest;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
shellApiRequest: ProxyApiRequest;
kubeApiUpgradeRequest: ProxyApiRequest;
router: Router;
proxy: httpProxy;
lensProxyPort: { set: (portNumber: number) => void };

View File

@ -2,5 +2,4 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./kube-api-upgrade-request";
export * from "./types";

View File

@ -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();
});
}

View File

@ -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;

View File

@ -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;

View File

@ -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));
});
};

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -3,24 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import path from "path";
import { UserStore } from "../../../common/user-store";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import type { TerminalShellEnvModify } from "../shell-env-modifier/terminal-shell-env-modify.injectable";
import type { ShellSessionArgs } 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 {
ShellType = "shell";
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string | undefined>) => Record<string, string | undefined>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, websocket, cluster, terminalId);
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
super(args);
}
protected getPathEntries(): string[] {
return [baseBinariesDir.get()];
return [this.dependencies.baseBundeledBinariesDirectory];
}
protected get cwd(): string | undefined {
@ -31,7 +33,7 @@ export class LocalShellSession extends ShellSession {
let env = await this.getCachedShellEnv();
// extensions can modify the env
env = this.shellEnvModify(this.cluster.id, env);
env = this.dependencies.terminalShellEnvModify(this.cluster.id, env);
const shell = env.PTYSHELL;
@ -50,11 +52,11 @@ export class LocalShellSession extends ShellSession {
switch(path.basename(shell)) {
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":
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
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":
return ["--login"];
default:

View File

@ -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;

View File

@ -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;

View File

@ -3,28 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type WebSocket from "ws";
import { v4 as uuid } from "uuid";
import { Watch, CoreV1Api } 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 { get, once } from "lodash";
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
import logger from "../../logger";
import type { Kubectl } from "../../kubectl/kubectl";
import { TerminalChannels } from "../../../common/terminal/channels";
export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string;
}
export class NodeShellSession extends ShellSession {
ShellType = "node-shell";
protected readonly podName = `node-shell-${uuid()}`;
protected readonly cwd: string | undefined = undefined;
protected readonly nodeName: string;
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, socket, cluster, terminalId);
constructor({ nodeName, ...args }: NodeShellSessionArgs) {
super(args);
this.nodeName = nodeName;
}
public async open() {

View File

@ -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;

View 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;

View File

@ -9,4 +9,4 @@ export interface ShellEnvContext {
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>>;

View File

@ -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;

View File

@ -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;
};

View File

@ -4,18 +4,39 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import { terminalShellEnvModify } from "./terminal-shell-env-modifiers";
import type { ClusterId } from "../../../common/cluster-types";
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({
id: "terminal-shell-env-modify",
instantiate: (di) =>
terminalShellEnvModify({
extensions: di.inject(mainExtensionsInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
instantiate: (di): TerminalShellEnvModify => {
const terminalShellEnvModifiers = di.inject(terminalShellEnvModifiersInjectable);
const entityRegistry = 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;

View File

@ -104,6 +104,13 @@ export enum WebSocketCloseEvent {
TlsHandshake = 1015,
}
export interface ShellSessionArgs {
kubectl: Kubectl;
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
export abstract class ShellSession {
abstract readonly ShellType: string;
@ -130,8 +137,11 @@ export abstract class ShellSession {
protected readonly kubectlBinDirP: Promise<string>;
protected readonly kubeconfigPathP: Promise<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 } {
const resume = ShellSession.processes.has(this.terminalId);
@ -152,10 +162,13 @@ export abstract class ShellSession {
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.kubectlBinDirP = this.kubectl.binDir();
this.terminalId = `${cluster.id}:${terminalId}`;
this.terminalId = `${cluster.id}:${tabId}`;
}
protected send(message: TerminalMessage): void {

View 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;