feat: v2
This commit is contained in:
+78
-37
@@ -1,18 +1,66 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { getPodContainerName, getPodForDeployment } from "../lib/shared";
|
||||
import { execClient } from "../lib/k8s";
|
||||
import { ctx } from "../lib/context";
|
||||
import {
|
||||
openExecSession,
|
||||
type ExecApiSession,
|
||||
type OpenExecOptions,
|
||||
} from "../lib/exec-api";
|
||||
|
||||
function prepareInteractiveStdin(): (() => void) | undefined {
|
||||
if (!process.stdin.isTTY) return;
|
||||
export type ExecSessionOpener = typeof openExecSession;
|
||||
|
||||
process.stdin.setRawMode?.(true);
|
||||
process.stdin.resume();
|
||||
return () => {
|
||||
process.stdin.setRawMode?.(false);
|
||||
process.stdin.pause();
|
||||
function terminalSize(): { columns?: number; rows?: number } {
|
||||
return {
|
||||
columns: process.stdout.columns,
|
||||
rows: process.stdout.rows,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runExec(
|
||||
project: string,
|
||||
deployment: string,
|
||||
command: string[],
|
||||
signal: AbortSignal,
|
||||
opener: ExecSessionOpener = openExecSession,
|
||||
options?: OpenExecOptions,
|
||||
): Promise<number> {
|
||||
if (!deployment) throw new Error("Deployment name is required");
|
||||
if (command.length === 0) throw new Error("Command is required");
|
||||
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
||||
const session: ExecApiSession = await opener(
|
||||
project,
|
||||
{ deployment, command, tty, ...(tty ? terminalSize() : {}) },
|
||||
signal,
|
||||
options,
|
||||
);
|
||||
const onData = (chunk: Buffer | string) =>
|
||||
session.sendStdin(
|
||||
typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk,
|
||||
);
|
||||
const onEnd = () => session.sendStdin(new Uint8Array(), true);
|
||||
const onResize = () => {
|
||||
const { columns, rows } = terminalSize();
|
||||
if (columns && rows) session.resize(columns, rows);
|
||||
};
|
||||
process.stdin.on("data", onData);
|
||||
process.stdin.once("end", onEnd);
|
||||
if (tty) process.stdout.on("resize", onResize);
|
||||
try {
|
||||
for await (const frame of session) {
|
||||
if (frame.type === "stdout") process.stdout.write(frame.data);
|
||||
else if (frame.type === "stderr") process.stderr.write(frame.data);
|
||||
else if (frame.type === "error") throw new Error(frame.message);
|
||||
else return frame.exitCode;
|
||||
}
|
||||
if (signal.aborted) return 0;
|
||||
throw new Error("Exec connection closed without an exit status");
|
||||
} finally {
|
||||
process.stdin.off("data", onData);
|
||||
process.stdin.off("end", onEnd);
|
||||
process.stdout.off("resize", onResize);
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const exec = defineCommand({
|
||||
meta: {
|
||||
name: "exec",
|
||||
@@ -22,39 +70,32 @@ export const exec = defineCommand({
|
||||
const [deployment, ...command] = args._;
|
||||
if (!deployment) throw new Error("Deployment name is required");
|
||||
if (command.length === 0) throw new Error("Command is required");
|
||||
|
||||
const pod = await getPodForDeployment(deployment);
|
||||
if (pod.status?.phase !== "Running") {
|
||||
throw new Error(
|
||||
`Deployment ${deployment} has no running pod to exec into (${pod.metadata?.name ?? "unknown pod"} is ${pod.status?.phase ?? "not ready"})`,
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const abort = () => controller.abort();
|
||||
process.on("SIGINT", abort);
|
||||
process.on("SIGTERM", abort);
|
||||
const raw = Boolean(process.stdin.isTTY);
|
||||
if (raw) {
|
||||
process.stdin.setRawMode?.(true);
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
const namespace = pod.metadata?.namespace ?? "default";
|
||||
const podName = pod.metadata?.name;
|
||||
if (!podName) throw new Error(`No pod name found for deployment ${deployment}`);
|
||||
|
||||
const restoreStdin = prepareInteractiveStdin();
|
||||
|
||||
try {
|
||||
const socket = await execClient.exec(
|
||||
namespace,
|
||||
podName,
|
||||
getPodContainerName(pod),
|
||||
const exitCode = await runExec(
|
||||
ctx().project,
|
||||
deployment,
|
||||
command,
|
||||
process.stdout,
|
||||
process.stderr,
|
||||
process.stdin,
|
||||
Boolean(process.stdin.isTTY),
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.onclose = () => resolve();
|
||||
socket.onerror = (event: { error?: unknown }) =>
|
||||
reject(event.error ?? new Error("Exec failed"));
|
||||
});
|
||||
if (exitCode !== 0) process.exitCode = exitCode;
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) throw error;
|
||||
} finally {
|
||||
restoreStdin?.();
|
||||
process.off("SIGINT", abort);
|
||||
process.off("SIGTERM", abort);
|
||||
if (raw) {
|
||||
process.stdin.setRawMode?.(false);
|
||||
process.stdin.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user