import * as WebSocket from "ws"; import * as pty from "node-pty"; import { ShellSession } from "./shell-session"; import { v4 as uuid } from "uuid"; import * as k8s from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node"; import { Cluster } from "./cluster"; import logger from "./logger"; import { appEventBus } from "../common/event-bus"; export class NodeShellSession extends ShellSession { protected nodeName: string; protected podId: string; protected kc: KubeConfig; constructor(socket: WebSocket, cluster: Cluster, nodeName: string) { super(socket, cluster); this.nodeName = nodeName; this.podId = `node-shell-${uuid()}`; this.kc = cluster.getProxyKubeconfig(); } public async open() { const shell = await this.kubectl.getPath(); let args = []; if (this.createNodeShellPod(this.podId, this.nodeName)) { await this.waitForRunningPod(this.podId).catch(() => { this.exit(1001); }); } args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]; const shellEnv = await this.getCachedShellEnv(); this.shellProcess = pty.spawn(shell, args, { cols: 80, cwd: this.cwd() || shellEnv["HOME"], env: shellEnv, name: "xterm-256color", rows: 30, }); this.running = true; this.pipeStdout(); this.pipeStdin(); this.closeWebsocketOnProcessExit(); this.exitProcessOnWebsocketClose(); appEventBus.emit({name: "node-shell", action: "open"}); } protected exit(code = 1000) { if (this.podId) { this.deleteNodeShellPod(); } super.exit(code); } protected async createNodeShellPod(podId: string, nodeName: string) { const kc = this.getKubeConfig(); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const pod = { metadata: { name: podId, namespace: "kube-system" }, spec: { restartPolicy: "Never", terminationGracePeriodSeconds: 0, hostPID: true, hostIPC: true, hostNetwork: true, tolerations: [{ operator: "Exists" }], containers: [{ name: "shell", image: "docker.io/alpine:3.12", securityContext: { privileged: true, }, command: ["nsenter"], args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"] }], nodeSelector: { "kubernetes.io/hostname": nodeName } } } as k8s.V1Pod; await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => { logger.error(error); return false; }); return true; } protected getKubeConfig() { if (this.kc) { return this.kc; } this.kc = new k8s.KubeConfig(); this.kc.loadFromFile(this.kubeconfigPath); return this.kc; } protected waitForRunningPod(podId: string) { return new Promise(async (resolve, reject) => { const kc = this.getKubeConfig(); const watch = new k8s.Watch(kc); const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {}, // callback is called for each received object. (type, obj) => { if (obj.metadata.name == podId && obj.status.phase === "Running") { resolve(true); } }, // done callback is called if the watch terminates normally (err) => { logger.error(err); reject(false); } ); setTimeout(() => { req.abort(); reject(false); }, 120 * 1000); }); } protected deleteNodeShellPod() { const kc = this.getKubeConfig(); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); k8sApi.deleteNamespacedPod(this.podId, "kube-system"); } } export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise { let shell: ShellSession; if (nodeName) { shell = new NodeShellSession(socket, cluster, nodeName); } else { shell = new ShellSession(socket, cluster); } shell.open(); return shell; }