feat: add ssh workspace routes

This commit is contained in:
Codex
2026-05-25 07:20:34 +00:00
parent 28674bc1f5
commit 0b14d582d1
5 changed files with 132 additions and 19 deletions
+100 -16
View File
@@ -18,6 +18,7 @@ export interface ParsedSshRoute {
namespace: string | null;
resource: string | null;
container: string | null;
workspace: string | null;
raw: string;
}
@@ -40,6 +41,7 @@ export interface SshFailureHint {
const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
const nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
const legacyK3sOperationRouteSegments = new Set([
"guard",
"kubectl",
@@ -859,28 +861,94 @@ export function parseSshInvocation(target: string, args: string[]): ParsedSshInv
export function parseSshRoute(target: string): ParsedSshRoute {
if (!target) throw new Error("ssh requires provider id, for example: bun scripts/cli.ts ssh D601");
const parts = target.split(":");
const [providerId, plane, ...rest] = parts;
const firstColon = target.indexOf(":");
if (firstColon < 0) {
return hostSshRoute(target, target, null);
}
const providerId = target.slice(0, firstColon);
const tail = target.slice(firstColon + 1);
if (!providerId) throw new Error("ssh route requires a provider id before ':'");
if (tail.length === 0) {
return hostSshRoute(providerId, target, null);
}
if (tail.startsWith("/")) {
return hostSshRoute(providerId, target, tail);
}
const [plane, ...rest] = tail.split(":");
if (plane === undefined || plane.length === 0 || plane === "host") {
return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, raw: target };
const workspace = rest.length > 0 ? rest.join(":") : null;
if (workspace !== null && !workspace.startsWith("/")) throw new Error("ssh host workspace route requires an absolute path after provider:host:");
return hostSshRoute(providerId, target, workspace);
}
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
const [first, second, third, fourth] = rest;
const operationInRoute = [first, second, third].find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment));
const operationInRoute = [first, second, third].map((segment) => segment === undefined ? undefined : routeSegmentHead(segment)).find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment));
if (operationInRoute !== undefined) throw new Error(k3sOperationInRouteMessage(target, operationInRoute));
if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container");
const targetRoute = parseK3sRouteTargetSegments(second ?? null, third ?? null);
return {
providerId,
plane: "k3s",
entry: null,
namespace: first && first.length > 0 ? first : null,
resource: second && second.length > 0 ? second : null,
container: third && third.length > 0 ? third : null,
resource: targetRoute.resource,
container: targetRoute.container,
workspace: targetRoute.workspace,
raw: target,
};
}
function hostSshRoute(providerId: string, raw: string, workspace: string | null): ParsedSshRoute {
return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, workspace, raw };
}
function routeSegmentHead(segment: string): string {
return segment.split("/")[0] ?? segment;
}
function parseK3sRouteTargetSegments(rawResource: string | null, rawContainer: string | null): { resource: string | null; container: string | null; workspace: string | null } {
const resourceParts = splitK3sResourceWorkspace(rawResource);
const containerParts = splitK3sContainerWorkspace(rawContainer);
return {
resource: resourceParts.resource,
container: containerParts.container,
workspace: combineK3sRouteWorkspace(resourceParts.workspace, containerParts.workspace),
};
}
function splitK3sResourceWorkspace(value: string | null): { resource: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { resource: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { resource: value, workspace: null };
if (isK3sResourceKindAlias(parts[0] ?? "") && (parts[1] ?? "").length > 0) {
const workspaceParts = parts.slice(2);
return {
resource: `${parts[0]}/${parts[1]}`,
workspace: workspaceParts.length > 0 ? `/${workspaceParts.join("/")}` : null,
};
}
return {
resource: parts[0] ?? value,
workspace: parts.length > 1 ? `/${parts.slice(1).join("/")}` : null,
};
}
function splitK3sContainerWorkspace(value: string | null): { container: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { container: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { container: value, workspace: null };
return { container: parts[0] ?? value, workspace: `/${parts.slice(1).join("/")}` };
}
function combineK3sRouteWorkspace(first: string | null, second: string | null): string | null {
if (first !== null && second !== null && first !== second) throw new Error("ssh k3s route workspace can be specified once, either after the workload or after the container");
return first ?? second;
}
function isK3sResourceKindAlias(value: string): boolean {
return k3sResourceKindAliases.has(value);
}
function k3sOperationInRouteMessage(target: string, operation: string): string {
const providerId = target.split(":")[0] || "<provider>";
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
@@ -1053,6 +1121,7 @@ function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
"--namespace", route.namespace,
"--resource", normalizeK3sRouteResource(route.resource),
...(route.container === null ? [] : ["--container", route.container]),
...(route.workspace === null ? [] : ["--workdir", route.workspace]),
];
}
@@ -1108,6 +1177,7 @@ interface K3sTargetOptions {
namespace: string | null;
resource: string | null;
container: string | null;
workspace: string | null;
stdin: boolean;
tty: boolean;
shell: string | null;
@@ -1134,7 +1204,7 @@ function buildK3sExecCommand(args: string[]): string {
...(parsed.tty ? ["-t"] : []),
...parsed.kubectlOptions,
"--",
...parsed.command,
...withK3sWorkspace(parsed, parsed.command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
@@ -1154,10 +1224,7 @@ function buildK3sScriptCommand(args: string[]): string {
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
shell,
"-s",
"--",
...parsed.command,
...withK3sWorkspace(parsed, [shell, "-s", "--", ...parsed.command]),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
@@ -1177,10 +1244,7 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
"--",
"sh",
"-s",
"--",
...parsed.command,
...withK3sWorkspace(parsed, ["sh", "-s", "--", ...parsed.command]),
];
const wrapper = podApplyPatchStdinWrapper();
return {
@@ -1196,6 +1260,7 @@ 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.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
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=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
@@ -1206,6 +1271,7 @@ function buildK3sLogsCommand(args: string[]): string {
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s logs requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.stdin || parsed.tty) throw new Error("ssh k3s logs does not support --stdin or --tty");
if (parsed.workspace !== null) throw new Error("ssh k3s logs does not accept --workdir");
const kubectlArgs = [
"logs",
"-n", parsed.namespace,
@@ -1220,6 +1286,7 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: Par
let namespace: string | null = null;
let resource: string | null = null;
let container: string | null = null;
let workspace: string | null = null;
let stdin = false;
let tty = false;
let shell: string | null = null;
@@ -1262,6 +1329,11 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: Par
index += 1;
continue;
}
if (arg === "--workdir" || arg === "--cwd") {
workspace = k3sWorkspaceValue(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
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}`);
@@ -1303,7 +1375,7 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: Par
if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- <command> [args...]`);
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 };
return { namespace, resource, container, workspace, stdin, tty, shell, command, kubectlOptions };
}
function k3sOptionValue(args: string[], index: number, option: string): string {
@@ -1312,6 +1384,17 @@ function k3sOptionValue(args: string[], index: number, option: string): string {
return value;
}
function k3sWorkspaceValue(value: string, option: string): string {
if (!value.startsWith("/")) throw new Error(`${option} must be an absolute pod workspace path`);
return value;
}
function withK3sWorkspace(parsed: K3sTargetOptions, command: string[]): string[] {
if (parsed.workspace === null) return command;
if (command.length === 0) throw new Error("ssh k3s workspace route requires a command to execute");
return ["sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", parsed.workspace, ...command];
}
function normalizeK3sResource(value: string): string {
if (value.includes("/")) return value;
return `pod/${value}`;
@@ -1588,6 +1671,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand),
cwd: invocation.route.plane === "host" ? invocation.route.workspace ?? undefined : undefined,
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null,
openTimeoutMs,