1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Convert the rest of shell sessions to be DI-ed

- This is a prerequesit for using the new
  createKubeJsonApiForClusterInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-09 14:58:21 -04:00
parent 558dbddeb8
commit a0e15c453f
13 changed files with 233 additions and 201 deletions

View File

@ -4,15 +4,15 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { shellApiRequest } from "./shell-api-request"; 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 shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable"; import clusterManagerInjectable from "../../../cluster/manager.injectable";
import openShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
const shellApiRequestInjectable = getInjectable({ const shellApiRequestInjectable = getInjectable({
id: "shell-api-request", id: "shell-api-request",
instantiate: (di) => shellApiRequest({ instantiate: (di) => shellApiRequest({
createShellSession: di.inject(createShellSessionInjectable), openShellSession: di.inject(openShellSessionInjectable),
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate, authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
clusterManager: di.inject(clusterManagerInjectable), clusterManager: di.inject(clusterManagerInjectable),
}), }),

View File

@ -4,28 +4,20 @@
*/ */
import logger from "../../../logger"; import logger from "../../../logger";
import type WebSocket from "ws";
import { Server as WebSocketServer } from "ws"; import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types"; import type { ProxyApiRequestArgs } from "../types";
import type { ClusterManager } from "../../../cluster/manager"; import type { ClusterManager } from "../../../cluster/manager";
import URLParse from "url-parse"; import URLParse from "url-parse";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import type { OpenShellSession } from "../../../shell-session/create-shell-session.injectable";
interface Dependencies { interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean; authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
openShellSession: OpenShellSession;
createShellSession: (args: {
webSocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;
}) => { open: () => Promise<void> };
clusterManager: ClusterManager; clusterManager: ClusterManager;
} }
export const shellApiRequest = ({ createShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => { export const shellApiRequest = ({ openShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = clusterManager.getClusterForRequest(req); const cluster = clusterManager.getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true); const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
@ -37,10 +29,8 @@ export const shellApiRequest = ({ createShellSession, authenticateRequest, clust
const ws = new WebSocketServer({ noServer: true }); const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (webSocket) => { ws.handleUpgrade(req, socket, head, (websocket) => {
const shell = createShellSession({ webSocket, cluster, tabId, nodeName }); openShellSession({ websocket, cluster, tabId, nodeName })
shell.open()
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error)); .catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
}); });
}; };

View File

@ -5,25 +5,31 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type WebSocket from "ws"; import type WebSocket from "ws";
import localShellSessionInjectable from "./local-shell-session/local-shell-session.injectable"; import openLocalShellSessionInjectable from "./local-shell-session/open.injectable";
import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable"; import openNodeShellSessionInjectable from "./node-shell-session/open.injectable";
interface Args { export interface OpenShellSessionArgs {
webSocket: WebSocket; websocket: WebSocket;
cluster: Cluster; cluster: Cluster;
tabId: string; tabId: string;
nodeName?: string; nodeName?: string;
} }
const createShellSessionInjectable = getInjectable({ export type OpenShellSession = (args: OpenShellSessionArgs) => Promise<void>;
id: "create-shell-session",
instantiate: const openShellSessionInjectable = getInjectable({
(di) => id: "open-shell-session",
({ nodeName, ...rest }: Args) =>
!nodeName instantiate: (di): OpenShellSession => {
? di.inject(localShellSessionInjectable, rest) const openLocalShellSession = di.inject(openLocalShellSessionInjectable);
: di.inject(nodeShellSessionInjectable, { nodeName, ...rest }), const openNodeShellSession = di.inject(openNodeShellSessionInjectable);
return ({ nodeName, ...args }) => (
nodeName
? openNodeShellSession({ nodeName, ...args })
: openLocalShellSession(args)
);
},
}); });
export default createShellSessionInjectable; export default openShellSessionInjectable;

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,27 @@
* 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 type { UserStore } from "../../../common/user-store";
import type { Cluster } from "../../../common/cluster/cluster"; import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import type { ClusterId } from "../../../common/cluster-types";
import { ShellSession } from "../shell-session"; import { ShellSession } from "../shell-session";
import type { Kubectl } from "../../kubectl/kubectl"; import type { ModifyTerminalShellEnv } from "../shell-env-modifier/modify-terminal-shell-env.injectable";
import { baseBinariesDir } from "../../../common/vars";
export interface LocalShellSessionDependencies extends ShellSessionDependencies {
modifyTerminalShellEnv: ModifyTerminalShellEnv;
readonly directoryForBinaries: string;
readonly userStore: UserStore;
}
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(dependencies, args);
} }
protected getPathEntries(): string[] { protected getPathEntries(): string[] {
return [baseBinariesDir.get()]; return [this.dependencies.directoryForBinaries];
} }
protected get cwd(): string | undefined { protected get cwd(): string | undefined {
@ -31,7 +34,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.modifyTerminalShellEnv(this.cluster.id, env);
const shell = env.PTYSHELL; const shell = env.PTYSHELL;
@ -45,16 +48,16 @@ export class LocalShellSession extends ShellSession {
} }
protected async getShellArgs(shell: string): Promise<string[]> { protected async getShellArgs(shell: string): Promise<string[]> {
const pathFromPreferences = UserStore.getInstance().kubectlBinariesPath || this.kubectl.getBundledPath(); const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = UserStore.getInstance().downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
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.directoryForBinaries};$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.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
case "zsh": case "zsh":
return ["--login"]; return ["--login"];
default: default:

View File

@ -0,0 +1,49 @@
/**
* 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 createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import modifyTerminalShellEnvInjectable from "../shell-env-modifier/modify-terminal-shell-env.injectable";
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import type WebSocket from "ws";
export interface OpenLocalShellSessionArgs {
websocket: WebSocket;
cluster: Cluster;
tabId: string;
}
export type OpenLocalShellSession = (args: OpenLocalShellSessionArgs) => Promise<void>;
const openLocalShellSessionInjectable = getInjectable({
id: "open-local-shell-session",
instantiate: (di): OpenLocalShellSession => {
const createKubectl = di.inject(createKubectlInjectable);
const dependencies: LocalShellSessionDependencies = {
directoryForBinaries: di.inject(directoryForBinariesInjectable),
isMac: di.inject(isMacInjectable),
modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
userStore: di.inject(userStoreInjectable),
};
return (args) => {
const kubectl = createKubectl(args.cluster.version);
const session = new LocalShellSession(dependencies, { kubectl, ...args });
return session.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,30 @@
* 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, ShellSessionDependencies } 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 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 nodeName: string;
protected readonly cwd: string | undefined = undefined; protected readonly cwd: string | undefined = undefined;
constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) { constructor(dependencies: ShellSessionDependencies, { nodeName, ...args }: NodeShellSessionArgs) {
super(kubectl, socket, cluster, terminalId); super(dependencies, args);
this.nodeName = nodeName;
} }
public async open() { public async open() {
@ -35,7 +37,7 @@ export class NodeShellSession extends ShellSession {
const cleanup = once(() => { const cleanup = once(() => {
coreApi coreApi
.deleteNamespacedPod(this.podName, "kube-system") .deleteNamespacedPod(this.podName, "kube-system")
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error)); .catch(error => this.dependencies.logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
}); });
this.websocket.once("close", cleanup); this.websocket.once("close", cleanup);
@ -75,7 +77,7 @@ export class NodeShellSession extends ShellSession {
switch (nodeOs) { switch (nodeOs) {
default: default:
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`); this.dependencies.logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
// fallthrough // fallthrough
case "linux": case "linux":
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"); args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
@ -127,7 +129,7 @@ export class NodeShellSession extends ShellSession {
} }
protected waitForRunningPod(kc: KubeConfig): Promise<void> { protected waitForRunningPod(kc: KubeConfig): Promise<void> {
logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`); this.dependencies.logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
new Watch(kc) new Watch(kc)
@ -146,19 +148,19 @@ export class NodeShellSession extends ShellSession {
}, },
// done callback is called if the watch terminates normally // done callback is called if the watch terminates normally
(err) => { (err) => {
logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`); this.dependencies.logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
reject(err); reject(err);
}, },
) )
.then(req => { .then(req => {
setTimeout(() => { setTimeout(() => {
logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`); this.dependencies.logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
req.abort(); req.abort();
reject("Pod creation timed out"); reject("Pod creation timed out");
}, 2 * 60 * 1000); // 2 * 60 * 1000 }, 2 * 60 * 1000); // 2 * 60 * 1000
}) })
.catch(error => { .catch(error => {
logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`); this.dependencies.logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
reject(error); reject(error);
}); });
}); });

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 type { Cluster } from "../../../common/cluster/cluster";
import type WebSocket from "ws";
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import { NodeShellSession } from "./node-shell-session";
import type { ShellSessionDependencies } from "../shell-session";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
export interface NodeShellSessionArgs {
websocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName: string;
}
export type OpenNodeShellSession = (args: NodeShellSessionArgs) => Promise<void>;
const openNodeShellSessionInjectable = getInjectable({
id: "open-node-shell-session",
instantiate: (di): OpenNodeShellSession => {
const createKubectl = di.inject(createKubectlInjectable);
const dependencies: ShellSessionDependencies = {
isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
};
return (args) => {
const kubectl = createKubectl(args.cluster.version);
const session = new NodeShellSession(dependencies, { kubectl, ...args });
return session.open();
};
},
});
export default openNodeShellSessionInjectable;

View File

@ -0,0 +1,49 @@
/**
* 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 type { ClusterId } from "../../../common/cluster-types";
import { isDefined } from "../../../common/utils";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
export type ModifyTerminalShellEnv = (clusterId: ClusterId, env: Partial<Record<string, string>>) => Partial<Record<string, string>>;
const modifyTerminalShellEnvInjectable = getInjectable({
id: "terminal-shell-env-modify",
instantiate: (di): ModifyTerminalShellEnv => {
const extensions = di.inject(mainExtensionsInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const terminalShellEnvModifiers = computed(() => (
extensions.get()
.map((extension) => extension.terminalShellEnvModifier)
.filter(isDefined)
));
return (clusterId, env) => {
const modifiers = terminalShellEnvModifiers.get();
if (modifiers.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 = modifiers.reduce((env, modifier) => modifier(ctx, env), env);
}
return env;
};
},
});
export default modifyTerminalShellEnvInjectable;

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

@ -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 mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import { terminalShellEnvModify } from "./terminal-shell-env-modifiers";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
const terminalShellEnvModifyInjectable = getInjectable({
id: "terminal-shell-env-modify",
instantiate: (di) =>
terminalShellEnvModify({
extensions: di.inject(mainExtensionsInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
export default terminalShellEnvModifyInjectable;

View File

@ -11,14 +11,13 @@ import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { isMac, isWindows } from "../../common/vars";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import * as pty from "node-pty"; import * as pty from "node-pty";
import { appEventBus } from "../../common/app-event-bus/event-bus"; import { appEventBus } from "../../common/app-event-bus/event-bus";
import logger from "../logger";
import { stat } from "fs/promises"; import { stat } from "fs/promises";
import { getOrInsertWith } from "../../common/utils"; import { getOrInsertWith } from "../../common/utils";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels"; import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
import type { Logger } from "../../common/logger";
export class ShellOpenError extends Error { export class ShellOpenError extends Error {
constructor(message: string, options?: ErrorOptions) { constructor(message: string, options?: ErrorOptions) {
@ -104,6 +103,19 @@ export enum WebSocketCloseEvent {
TlsHandshake = 1015, TlsHandshake = 1015,
} }
export interface ShellSessionDependencies {
readonly isWindows: boolean;
readonly isMac: boolean;
readonly logger: Logger;
}
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,6 +142,9 @@ 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 get cwd(): string | undefined;
@ -147,12 +162,15 @@ export abstract class ShellSession {
}) })
)); ));
logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`); this.dependencies.logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
return { shellProcess, resume }; return { shellProcess, resume };
} }
constructor(protected readonly kubectl: Kubectl, protected readonly websocket: WebSocket, protected readonly cluster: Cluster, terminalId: string) { constructor(protected readonly dependencies: ShellSessionDependencies, { kubectl, websocket, cluster, tabId: terminalId }: ShellSessionArgs) {
this.kubectl = kubectl;
this.websocket = websocket;
this.cluster = cluster;
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}:${terminalId}`;
@ -165,7 +183,7 @@ export abstract class ShellSession {
protected async getCwd(env: Record<string, string | undefined>): Promise<string> { protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
const cwdOptions = [this.cwd]; const cwdOptions = [this.cwd];
if (isWindows) { if (this.dependencies.isWindows) {
cwdOptions.push( cwdOptions.push(
env.USERPROFILE, env.USERPROFILE,
os.homedir(), os.homedir(),
@ -177,7 +195,7 @@ export abstract class ShellSession {
os.homedir(), os.homedir(),
); );
if (isMac) { if (this.dependencies.isMac) {
cwdOptions.push("/Users"); cwdOptions.push("/Users");
} else { } else {
cwdOptions.push("/home"); cwdOptions.push("/home");
@ -214,7 +232,7 @@ export abstract class ShellSession {
this.running = true; this.running = true;
shellProcess.onData(data => this.send({ type: TerminalChannels.STDOUT, data })); shellProcess.onData(data => this.send({ type: TerminalChannels.STDOUT, data }));
shellProcess.onExit(({ exitCode }) => { shellProcess.onExit(({ exitCode }) => {
logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`); this.dependencies.logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
// This might already be false because of the kill() within the websocket.on("close") handler // This might already be false because of the kill() within the websocket.on("close") handler
if (this.running) { if (this.running) {
@ -232,11 +250,11 @@ export abstract class ShellSession {
this.websocket this.websocket
.on("message", (rawData: unknown): void => { .on("message", (rawData: unknown): void => {
if (!this.running) { if (!this.running) {
return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`); return void this.dependencies.logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
} }
if (!(rawData instanceof Buffer)) { if (!(rawData instanceof Buffer)) {
return void logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData }); return void this.dependencies.logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
} }
const data = rawData.toString(); const data = rawData.toString();
@ -252,18 +270,18 @@ export abstract class ShellSession {
shellProcess.resize(message.data.width, message.data.height); shellProcess.resize(message.data.width, message.data.height);
break; break;
case TerminalChannels.PING: case TerminalChannels.PING:
logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`); this.dependencies.logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
break; break;
default: default:
logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message); this.dependencies.logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
break; break;
} }
} catch (error) { } catch (error) {
logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error); this.dependencies.logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
} }
}) })
.once("close", code => { .once("close", code => {
logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() }); this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
const stopShellSession = this.running const stopShellSession = this.running
&& ( && (
@ -278,11 +296,11 @@ export abstract class ShellSession {
this.running = false; this.running = false;
try { try {
logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`); this.dependencies.logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
shellProcess.kill(); shellProcess.kill();
ShellSession.processes.delete(this.terminalId); ShellSession.processes.delete(this.terminalId);
} catch (error) { } catch (error) {
logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error); this.dependencies.logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
} }
} }
}); });
@ -319,7 +337,7 @@ export abstract class ShellSession {
delete env.DEBUG; // don't pass DEBUG into shells delete env.DEBUG; // don't pass DEBUG into shells
if (isWindows) { if (this.dependencies.isWindows) {
env.SystemRoot = process.env.SystemRoot; env.SystemRoot = process.env.SystemRoot;
env.PTYSHELL = shell || "powershell.exe"; env.PTYSHELL = shell || "powershell.exe";
env.PATH = pathStr; env.PATH = pathStr;