feat: add stdin script ssh routes
This commit is contained in:
+82
-8
@@ -42,6 +42,7 @@ const k3sRouteEntries = new Set([
|
||||
"guard",
|
||||
"kubectl",
|
||||
"exec",
|
||||
"script",
|
||||
"logs",
|
||||
"get",
|
||||
"describe",
|
||||
@@ -536,6 +537,9 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
if (subcommand === "py") {
|
||||
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (subcommand === "script" || subcommand === "sh") {
|
||||
return { remoteCommand: buildShellStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (subcommand === "argv" || subcommand === "exec") {
|
||||
const toolArgs = args.slice(1);
|
||||
if (toolArgs.length === 0) throw new Error(`ssh ${subcommand} requires a command`);
|
||||
@@ -742,6 +746,9 @@ function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
if (route.entry === "exec") {
|
||||
return { remoteCommand: buildK3sExecCommand([...k3sRouteTargetArgs(route), ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
if (route.entry === "script") {
|
||||
return { remoteCommand: buildK3sScriptCommand([...k3sRouteTargetArgs(route), ...args]), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (route.entry !== null) {
|
||||
const k3sArgs = [route.entry, ...(route.namespace === null ? [] : [route.namespace]), ...(route.resource === null ? [] : [route.resource]), ...args];
|
||||
return { remoteCommand: buildK3sCommand(k3sArgs), requiresStdin: false, invocationKind: "helper" };
|
||||
@@ -781,6 +788,7 @@ function buildK3sCommand(args: string[]): string {
|
||||
}
|
||||
if (action === "guard") return buildD601K3sGuardCommand();
|
||||
if (action === "exec") return buildK3sExecCommand(args.slice(1));
|
||||
if (action === "script") return buildK3sScriptCommand(args.slice(1));
|
||||
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
|
||||
if (action === "kubectl") {
|
||||
const kubectlArgs = args.slice(1);
|
||||
@@ -805,7 +813,7 @@ function buildD601K3sGuardCommand(): string {
|
||||
"case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac",
|
||||
"printf 'native_k3s_guard=ok\\n'",
|
||||
].join("; ");
|
||||
return shellArgv(["bash", "-lc", script]);
|
||||
return shellArgv(["bash", "-c", script]);
|
||||
}
|
||||
|
||||
interface K3sTargetOptions {
|
||||
@@ -814,10 +822,17 @@ interface K3sTargetOptions {
|
||||
container: string | null;
|
||||
stdin: boolean;
|
||||
tty: boolean;
|
||||
shell: string | null;
|
||||
command: string[];
|
||||
kubectlOptions: string[];
|
||||
}
|
||||
|
||||
interface ParseK3sTargetOptionsOptions {
|
||||
requireCommand: boolean;
|
||||
allowCommand?: boolean;
|
||||
allowShell?: boolean;
|
||||
}
|
||||
|
||||
function buildK3sExecCommand(args: string[]): string {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s exec", { requireCommand: true });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s exec requires --namespace <name>");
|
||||
@@ -836,6 +851,28 @@ function buildK3sExecCommand(args: string[]): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sScriptCommand(args: string[]): string {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s script requires --namespace <name>");
|
||||
if (parsed.resource === null) throw new Error("ssh k3s script requires --deployment <name>, --pod <name> or --resource <type/name>");
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
const kubectlArgs = [
|
||||
"exec",
|
||||
"-i",
|
||||
"-n", parsed.namespace,
|
||||
parsed.resource,
|
||||
...(parsed.container === null ? [] : ["-c", parsed.container]),
|
||||
...parsed.kubectlOptions,
|
||||
"--",
|
||||
shell,
|
||||
"-s",
|
||||
"--",
|
||||
...parsed.command,
|
||||
];
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sLogsCommand(args: string[]): string {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
|
||||
@@ -851,12 +888,13 @@ function buildK3sLogsCommand(args: string[]): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function parseK3sTargetOptions(args: string[], commandName: string, options: { requireCommand: boolean }): K3sTargetOptions {
|
||||
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {
|
||||
let namespace: string | null = null;
|
||||
let resource: string | null = null;
|
||||
let container: string | null = null;
|
||||
let stdin = false;
|
||||
let tty = false;
|
||||
let shell: string | null = null;
|
||||
const kubectlOptions: string[] = [];
|
||||
const command: string[] = [];
|
||||
let afterDoubleDash = false;
|
||||
@@ -896,6 +934,12 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: { r
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--shell") {
|
||||
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--stdin" || arg === "-i") {
|
||||
stdin = true;
|
||||
continue;
|
||||
@@ -930,8 +974,8 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: { r
|
||||
}
|
||||
|
||||
if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- <command> [args...]`);
|
||||
if (!options.requireCommand && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
|
||||
return { namespace, resource, container, stdin, tty, command, kubectlOptions };
|
||||
if (!options.requireCommand && options.allowCommand !== true && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
|
||||
return { namespace, resource, container, stdin, tty, shell, command, kubectlOptions };
|
||||
}
|
||||
|
||||
function k3sOptionValue(args: string[], index: number, option: string): string {
|
||||
@@ -952,6 +996,36 @@ function normalizeK3sRouteResource(value: string): string {
|
||||
return `deployment/${value}`;
|
||||
}
|
||||
|
||||
function k3sScriptShell(value: string, option: string): string {
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${option} must be a shell executable name or path without whitespace`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildShellStdinCommand(args: string[]): string {
|
||||
let shell = "sh";
|
||||
const scriptArgs: string[] = [];
|
||||
let afterDoubleDash = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (afterDoubleDash) {
|
||||
scriptArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--shell") {
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
|
||||
scriptArgs.push(arg);
|
||||
}
|
||||
return shellArgv([shell, "-s", "--", ...scriptArgs]);
|
||||
}
|
||||
|
||||
function buildPythonStdinCommand(args: string[]): string {
|
||||
const pythonArgs = args.map(shellQuote).join(" ");
|
||||
const execArgs = pythonArgs.length > 0 ? ` "$UNIDESK_SSH_PY_FILE" ${pythonArgs}` : ' "$UNIDESK_SSH_PY_FILE"';
|
||||
@@ -1017,8 +1091,8 @@ export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCo
|
||||
providerId: shownProviderId,
|
||||
trigger,
|
||||
exitCode,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer argv form for non-interactive commands.",
|
||||
try: `bun scripts/cli.ts ssh ${shownProviderId} argv bash -lc '<command>'`,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or stdin script passthrough for non-interactive commands.",
|
||||
try: `bun scripts/cli.ts ssh ${shownProviderId} script <<'SCRIPT'`,
|
||||
triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
|
||||
note: "This hint intentionally does not echo the original remote command.",
|
||||
};
|
||||
@@ -1176,7 +1250,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
||||
const payloadJson = JSON.stringify(payload);
|
||||
const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64");
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
"set -eu",
|
||||
`payload=${shellQuote(payloadJson)}`,
|
||||
"if command -v backend-core >/dev/null 2>&1; then",
|
||||
' exec backend-core --ssh-broker "$payload"',
|
||||
@@ -1190,7 +1264,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
||||
"-i",
|
||||
"unidesk-backend-core",
|
||||
"sh",
|
||||
"-lc",
|
||||
"-c",
|
||||
script,
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
|
||||
Reference in New Issue
Block a user