feat: split k3s ssh routes from operations

This commit is contained in:
Codex
2026-05-25 06:24:17 +00:00
parent 64c82b5be8
commit 61492b4db5
5 changed files with 463 additions and 70 deletions
+387 -42
View File
@@ -5,6 +5,8 @@ export interface ParsedSshArgs {
remoteCommand: string | null;
requiresStdin: boolean;
invocationKind: SshInvocationKind;
stdinPrefix?: string;
stdinSuffix?: string;
}
export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like";
@@ -38,11 +40,12 @@ export interface SshFailureHint {
const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
const d601NativeKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const k3sRouteEntries = new Set([
const legacyK3sOperationRouteSegments = new Set([
"guard",
"kubectl",
"exec",
"script",
"apply-patch",
"logs",
"get",
"describe",
@@ -200,6 +203,266 @@ if __name__ == "__main__":
main()
`;
const remoteShApplyPatchSource = String.raw`#!/bin/sh
set -eu
die() {
printf 'apply_patch: %s\n' "$*" >&2
exit 1
}
mk_temp() {
mktemp "${"$"}{TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1
}
ensure_parent() {
case "$1" in
*/*)
dir=${"$"}{1%/*}
[ -n "$dir" ] || dir=/
mkdir -p "$dir"
;;
esac
}
read_text_preserve_newlines() {
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1"
printf '%s' "${"$"}{text%"$marker"}"
}
write_file() {
target=$1
source=$2
ensure_parent "$target"
cp "$source" "$target" || die "failed to write $target"
}
replace_once() {
target=$1
search_file=$2
replace_file=$3
[ -e "$target" ] || die "file not found: $target"
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target"
old=${"$"}{old%"$marker"}
search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search"
search=${"$"}{search%"$marker"}
replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement"
replacement=${"$"}{replacement%"$marker"}
if [ -z "$search" ]; then
new=$replacement$old
else
case "$old" in
*"$search"*)
prefix=${"$"}{old%%"$search"*}
suffix=${"$"}{old#*"$search"}
new=$prefix$replacement$suffix
;;
*)
die "hunk context not found in $target"
;;
esac
fi
out=$(mk_temp)
printf '%s' "$new" > "$out" || die "failed to render patched file"
write_file "$target" "$out"
rm -f "$out"
}
apply_update() {
target=$1
body=$2
in_hunk=0
search_file=
replace_file=
changed=0
finish_hunk() {
[ "$in_hunk" = 1 ] || return 0
replace_once "$target" "$search_file" "$replace_file"
rm -f "$search_file" "$replace_file"
search_file=
replace_file=
in_hunk=0
changed=1
}
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
"*** End of File"*)
continue
;;
"@@"*)
finish_hunk
search_file=$(mk_temp)
replace_file=$(mk_temp)
in_hunk=1
continue
;;
esac
[ "$in_hunk" = 1 ] || die "expected hunk header in $target"
case "$line" in
" "*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
printf '%s\n' "$text" >> "$replace_file"
;;
"-"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
;;
"+"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$replace_file"
;;
"\\ No newline at end of file")
;;
*)
die "bad hunk line in $target: $line"
;;
esac
done < "$body"
finish_hunk
[ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target"
}
is_top_header() {
case "$1" in
"*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch")
return 0
;;
*)
return 1
;;
esac
}
pushed=0
pushed_line=
next_patch_line() {
if [ "$pushed" = 1 ]; then
line=$pushed_line
pushed=0
pushed_line=
return 0
fi
if IFS= read -r line; then
return 0
fi
[ -n "${"$"}{line:-}" ]
}
push_patch_line() {
pushed_line=$1
pushed=1
}
parse_add_file() {
target=$1
[ ! -e "$target" ] || die "file already exists: $target"
tmp=$(mk_temp)
while next_patch_line; do
if is_top_header "$line"; then
push_patch_line "$line"
break
fi
case "$line" in
"+"*)
printf '%s\n' "${"$"}{line#?}" >> "$tmp"
;;
*)
rm -f "$tmp"
die "add file lines must start with + for $target"
;;
esac
done
write_file "$target" "$tmp"
rm -f "$tmp"
}
parse_update_file() {
target=$1
move_to=
body=$(mk_temp)
if next_patch_line; then
case "$line" in
"*** Move to: "*)
move_to=${"$"}{line#"*** Move to: "}
;;
*)
push_patch_line "$line"
;;
esac
fi
while next_patch_line; do
if is_top_header "$line"; then
push_patch_line "$line"
break
fi
case "$line" in
"*** Move to: "*)
rm -f "$body"
die "move marker must appear before update hunks"
;;
*)
printf '%s\n' "$line" >> "$body"
;;
esac
done
if [ -s "$body" ]; then
apply_update "$target" "$body"
elif [ -z "$move_to" ] && [ ! -e "$target" ]; then
rm -f "$body"
die "file not found: $target"
fi
rm -f "$body"
if [ -n "$move_to" ]; then
[ -e "$target" ] || die "file not found: $target"
[ ! -e "$move_to" ] || die "target file already exists: $move_to"
ensure_parent "$move_to"
mv "$target" "$move_to" || die "failed to move $target to $move_to"
fi
}
main() {
if [ "${"$"}{1:-}" = "-h" ] || [ "${"$"}{1:-}" = "--help" ]; then
printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin\n'
return 0
fi
next_patch_line || die "patch must start with *** Begin Patch"
[ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch"
while next_patch_line; do
case "$line" in
"*** End Patch")
printf 'Done!\n'
return 0
;;
"*** Add File: "*)
parse_add_file "${"$"}{line#"*** Add File: "}"
;;
"*** Delete File: "*)
target=${"$"}{line#"*** Delete File: "}
rm -f "$target" || die "failed to delete $target"
;;
"*** Update File: "*)
parse_update_file "${"$"}{line#"*** Update File: "}"
;;
*)
die "unexpected patch line: $line"
;;
esac
done
die "missing *** End Patch"
}
main "$@"
`;
const remoteGlobSource = String.raw`#!/usr/bin/env python3
import argparse
import glob
@@ -552,6 +815,7 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "k3s") {
if (args[1] === "apply-patch") return buildK3sApplyPatchCommand(args.slice(2));
return { remoteCommand: buildK3sCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
}
if (argvQuotedSshSubcommands.has(subcommand)) {
@@ -598,19 +862,8 @@ export function parseSshRoute(target: string): ParsedSshRoute {
return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, raw: target };
}
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
const [first, second, third, fourth, ...extra] = rest;
if (first && k3sRouteEntries.has(first)) {
if (extra.length > 0) throw new Error("ssh k3s command route supports at most provider:k3s:entry:namespace:resource:container");
return {
providerId,
plane: "k3s",
entry: first,
namespace: second && second.length > 0 ? second : null,
resource: third && third.length > 0 ? third : null,
container: fourth && fourth.length > 0 ? fourth : null,
raw: target,
};
}
const [first, second, third, fourth] = rest;
if (first && legacyK3sOperationRouteSegments.has(first)) throw new Error(k3sOperationInRouteMessage(target, first, second, third));
if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container");
return {
providerId,
@@ -623,6 +876,14 @@ export function parseSshRoute(target: string): ParsedSshRoute {
};
}
function k3sOperationInRouteMessage(target: string, operation: string, namespace: string | undefined, resource: string | undefined): string {
if (operation === "kubectl") return `ssh k3s route must locate a target only; use "ssh D601:k3s kubectl ..." instead of "${target}"`;
if (operation === "script" && namespace === undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s script <<'SCRIPT'" instead of "${target}"`;
if (operation === "guard") return `ssh k3s route must locate a target only; use "ssh D601:k3s guard" or "ssh D601:k3s" instead of "${target}"`;
if (namespace !== undefined && resource !== undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s:${namespace}:${resource} ${operation} ..." instead of "${target}"`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh D601:k3s ${operation} ..."`;
}
function shellArgv(args: string[]): string {
return args.map(shellQuote).join(" ");
}
@@ -729,41 +990,57 @@ function buildFindCommand(args: string[]): string {
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
if (route.entry === null && route.namespace === null && route.resource === null) {
const k3sArgs = args.length === 0 ? ["guard"] : args;
return { remoteCommand: buildK3sCommand(k3sArgs), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "guard") {
if (args.length > 0) throw new Error("ssh route D601:k3s:guard does not accept extra arguments");
return { remoteCommand: buildD601K3sGuardCommand(), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "kubectl") {
if (args.length === 0) throw new Error("ssh route D601:k3s:kubectl requires kubectl arguments");
return { remoteCommand: buildK3sCommand(["kubectl", ...args]), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "logs") {
return { remoteCommand: buildK3sLogsCommand([...k3sRouteTargetArgs(route), ...args]), requiresStdin: false, invocationKind: "helper" };
}
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" };
return parseK3sControlPlaneOperation(args);
}
if (route.namespace === null || route.resource === null) {
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<deployment|pod/resource>");
}
return parseK3sTargetOperation(route, args);
}
function parseK3sControlPlaneOperation(args: string[]): ParsedSshArgs {
const operation = args[0] ?? "guard";
if (operation === "apply-patch" || operation === "patch") {
throw new Error("ssh D601:k3s apply-patch requires a workload route: ssh D601:k3s:<namespace>:<workload> apply-patch");
}
if (operation === "script" || operation === "sh") {
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
if (operation === "guard") {
if (args.length > 1) throw new Error("ssh D601:k3s guard does not accept extra arguments");
return { remoteCommand: buildD601K3sGuardCommand(), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sCommand(args), requiresStdin: false, invocationKind: "helper" };
}
function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const targetArgs = k3sRouteTargetArgs(route);
if (args.length === 0) {
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", "get", "-n", route.namespace, normalizeK3sRouteResource(route.resource), "-o", "wide"]),
remoteCommand: buildK3sTargetObjectCommand("get", route, ["-o", "wide"]),
requiresStdin: false,
invocationKind: "helper",
};
}
return { remoteCommand: buildK3sExecCommand([...k3sRouteTargetArgs(route), ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
const operation = args[0] ?? "";
const operationArgs = args.slice(1);
if (operation === "apply-patch" || operation === "patch") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]);
if (operation === "script") return { remoteCommand: buildK3sScriptCommand([...targetArgs, ...operationArgs]), requiresStdin: true, invocationKind: "helper" };
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
if (operation === "get" || operation === "describe") {
return { remoteCommand: buildK3sTargetObjectCommand(operation, route, operationArgs), requiresStdin: false, invocationKind: "helper" };
}
if (operation === "kubectl") throw new Error("ssh k3s kubectl is a control-plane operation; use ssh D601:k3s kubectl ...");
if (operation === "exec") {
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
}
function buildK3sTargetObjectCommand(action: "get" | "describe", route: ParsedSshRoute, args: string[]): string {
if (route.namespace === null || route.resource === null) throw new Error(`ssh k3s ${action} target requires namespace and workload route`);
if (args.includes("--follow") || args.includes("-f")) throw new Error(`ssh k3s target ${action} does not support follow mode`);
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
}
function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
@@ -853,8 +1130,9 @@ function buildK3sExecCommand(args: string[]): string {
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.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed);
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s script target 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 = [
@@ -873,6 +1151,45 @@ function buildK3sScriptCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s apply-patch", { requireCommand: false, allowCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s apply-patch requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s apply-patch requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.tty) throw new Error("ssh k3s apply-patch does not support --tty; stdin is reserved for the patch body");
if (parsed.stdin) throw new Error("ssh k3s apply-patch does not accept --stdin; stdin is always the patch body");
if (parsed.shell !== null) throw new Error("ssh k3s apply-patch does not accept --shell");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s apply-patch does not accept kubectl log options");
const kubectlArgs = [
"exec",
"-i",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
"--",
"sh",
"-s",
"--",
...parsed.command,
];
const wrapper = podApplyPatchStdinWrapper();
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]),
requiresStdin: true,
invocationKind: "helper",
stdinPrefix: wrapper.prefix,
stdinSuffix: wrapper.suffix,
};
}
function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body");
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
const shell = parsed.shell ?? "sh";
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, shell, "-s", "--", ...parsed.command]);
}
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>");
@@ -1026,6 +1343,25 @@ function buildShellStdinCommand(args: string[]): string {
return shellArgv([shell, "-s", "--", ...scriptArgs]);
}
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
if (remoteShApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker");
return {
prefix: [
"set -eu",
'UNIDESK_SSH_TOOL_DIR="${UNIDESK_SSH_TOOL_DIR:-/tmp/unidesk-ssh-tools}"',
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
`cat > "$UNIDESK_SSH_TOOL_DIR/apply_patch" <<'${toolMarker}'`,
remoteShApplyPatchSource.trimEnd(),
toolMarker,
'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"',
`"$UNIDESK_SSH_TOOL_DIR/apply_patch" "$@" <<'${patchMarker}'`,
].join("\n") + "\n",
suffix: `\n${patchMarker}\n`,
};
}
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"';
@@ -1274,7 +1610,16 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const rawMode = parsed.remoteCommand === null && process.stdin.isTTY;
if (rawMode) process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.pipe(child.stdin);
if (parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined) {
if (parsed.stdinPrefix) child.stdin.write(parsed.stdinPrefix);
process.stdin.pipe(child.stdin, { end: false });
process.stdin.once("end", () => {
if (parsed.stdinSuffix) child.stdin.write(parsed.stdinSuffix);
child.stdin.end();
});
} else {
process.stdin.pipe(child.stdin);
}
let stderrTail = "";
const appendStderrTail = (chunk: Buffer | string): void => {
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;