fix: clarify k3s route container syntax
This commit is contained in:
+103
-16
@@ -49,6 +49,11 @@ export interface ParsedSshInvocation {
|
||||
parsed: ParsedSshArgs;
|
||||
}
|
||||
|
||||
interface EffectiveApplyPatchV2Invocation {
|
||||
invocation: ParsedSshInvocation;
|
||||
argv: string[];
|
||||
}
|
||||
|
||||
export interface SshCaptureResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
@@ -149,7 +154,6 @@ export const sshUserToolPathPrelude = [
|
||||
"export PATH",
|
||||
].join("\n");
|
||||
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
|
||||
const k3sPodRoutePrefixes = ["pod:", "po:", "pods:"];
|
||||
const legacyK3sOperationRouteSegments = new Set([
|
||||
"guard",
|
||||
"kubectl",
|
||||
@@ -1031,6 +1035,7 @@ export function parseSshRoute(target: string): ParsedSshRoute {
|
||||
return hostSshRoute(providerId, target, workspace);
|
||||
}
|
||||
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
|
||||
rejectK3sSlashKindRouteSegments(target, rest);
|
||||
const normalizedRest = normalizeK3sRouteRestSegments(rest);
|
||||
const [first, second, third, fourth] = normalizedRest;
|
||||
const operationInRoute = [first, second, third].map((segment) => segment === undefined ? undefined : routeSegmentHead(segment)).find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment));
|
||||
@@ -1414,25 +1419,48 @@ function normalizeK3sRouteRestSegments(rest: string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const current = rest[index] ?? "";
|
||||
const podPrefix = k3sPodRoutePrefixes.find((prefix) => current === prefix.slice(0, -1) || current.startsWith(prefix));
|
||||
if (podPrefix === undefined) {
|
||||
const kindRoute = k3sColonKindRoute(current);
|
||||
if (kindRoute === null) {
|
||||
normalized.push(current);
|
||||
continue;
|
||||
}
|
||||
if (current === podPrefix.slice(0, -1)) {
|
||||
if (kindRoute.name === null) {
|
||||
const next = rest[index + 1];
|
||||
if (next === undefined || next.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
|
||||
normalized.push(`pod/${next}`);
|
||||
if (next === undefined || next.length === 0) throw new Error(`ssh k3s ${kindRoute.kind}: route requires a resource name after ${kindRoute.kind}:`);
|
||||
normalized.push(`${kindRoute.kind}/${next}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const podName = current.slice(podPrefix.length);
|
||||
if (podName.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
|
||||
normalized.push(`pod/${podName}`);
|
||||
normalized.push(`${kindRoute.kind}/${kindRoute.name}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function rejectK3sSlashKindRouteSegments(target: string, rest: string[]): void {
|
||||
const resourceSegment = rest[1];
|
||||
if (resourceSegment === undefined || resourceSegment.length === 0) return;
|
||||
const slashIndex = resourceSegment.indexOf("/");
|
||||
if (slashIndex < 0) return;
|
||||
const head = resourceSegment.slice(0, slashIndex);
|
||||
if (!isK3sResourceKindAlias(head)) return;
|
||||
throw new Error(
|
||||
`ssh k3s route segment "${resourceSegment}" in "${target}" uses "/" between Kubernetes route parts. ` +
|
||||
`":" is the distributed route separator and "/" is only for the in-container filesystem cwd. ` +
|
||||
`Use "trans <provider>:k3s:<namespace>:${head}:<name>[:<container>] ..." for ${head} routes, ` +
|
||||
`and use "--cwd /path" or a route cwd suffix only for filesystem paths.`
|
||||
);
|
||||
}
|
||||
|
||||
function k3sColonKindRoute(value: string): { kind: string; name: string | null } | null {
|
||||
const colonIndex = value.indexOf(":");
|
||||
const head = colonIndex < 0 ? value : value.slice(0, colonIndex);
|
||||
if (!isK3sResourceKindAlias(head)) return null;
|
||||
if (colonIndex < 0) return { kind: head, name: null };
|
||||
const name = value.slice(colonIndex + 1);
|
||||
if (name.length === 0) throw new Error(`ssh k3s ${head}: route requires a resource name after ${head}:`);
|
||||
return { kind: head, name };
|
||||
}
|
||||
|
||||
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("/");
|
||||
@@ -1714,6 +1742,55 @@ function k3sRouteExecOperationArgs(args: string[]): string[] {
|
||||
throw new Error("ssh k3s target exec operation requires a command to exec");
|
||||
}
|
||||
|
||||
function effectiveApplyPatchV2Invocation(invocation: ParsedSshInvocation, argv: string[]): EffectiveApplyPatchV2Invocation {
|
||||
if (invocation.route.plane !== "k3s" || isApplyPatchV2HelpArgs(argv)) return { invocation, argv };
|
||||
let container: string | null = null;
|
||||
let workspace: string | null = null;
|
||||
const remaining: string[] = [];
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index] ?? "";
|
||||
if (arg === "--container" || arg === "-c") {
|
||||
container = k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const containerValue = k3sEqualsOptionValue(arg, "--container", `ssh ${invocation.route.raw} apply-patch`);
|
||||
if (containerValue !== null) {
|
||||
container = containerValue;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--workdir" || arg === "--cwd") {
|
||||
workspace = k3sWorkspaceValue(k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`), `ssh ${invocation.route.raw} apply-patch ${arg}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const workdirValue = k3sEqualsOptionValue(arg, "--workdir", `ssh ${invocation.route.raw} apply-patch`) ?? k3sEqualsOptionValue(arg, "--cwd", `ssh ${invocation.route.raw} apply-patch`);
|
||||
if (workdirValue !== null) {
|
||||
workspace = k3sWorkspaceValue(workdirValue, `ssh ${invocation.route.raw} apply-patch ${arg.split("=", 1)[0]}`);
|
||||
continue;
|
||||
}
|
||||
remaining.push(arg);
|
||||
}
|
||||
|
||||
const route = invocation.route;
|
||||
if (container !== null && route.container !== null && container !== route.container) {
|
||||
throw new Error("ssh k3s apply-patch container can be specified once, either in the route :<container> segment or with --container");
|
||||
}
|
||||
|
||||
return {
|
||||
invocation: {
|
||||
...invocation,
|
||||
route: {
|
||||
...route,
|
||||
container: route.container ?? container,
|
||||
workspace: combineK3sRouteWorkspace(route.workspace, workspace),
|
||||
},
|
||||
},
|
||||
argv: remaining,
|
||||
};
|
||||
}
|
||||
|
||||
function buildK3sCommand(providerId: string, args: string[]): string {
|
||||
const action = args[0] ?? "";
|
||||
if (action.length === 0 || action === "--help" || action === "-h" || action === "help") {
|
||||
@@ -2109,7 +2186,16 @@ function k3sWorkspaceValue(value: string, option: string): string {
|
||||
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];
|
||||
const cwdScript = [
|
||||
"if ! cd \"$1\"; then",
|
||||
"rc=$?",
|
||||
"printf 'UNIDESK_K3S_CWD_HINT cwd=%s message=%s\\n' \"$1\" '/... is interpreted as remote cwd, not container; use :<container> or --container <container> for container selection.' >&2",
|
||||
"exit \"$rc\"",
|
||||
"fi",
|
||||
"shift",
|
||||
"exec \"$@\"",
|
||||
].join("\n");
|
||||
return ["sh", "-c", cwdScript, "unidesk-cwd", parsed.workspace, ...command];
|
||||
}
|
||||
|
||||
function normalizeK3sResource(value: string): string {
|
||||
@@ -3130,18 +3216,19 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
||||
});
|
||||
}
|
||||
if (operationName === "apply-patch") {
|
||||
const executor: ApplyPatchV2Executor = invocation.route.plane === "win"
|
||||
? { fs: createWindowsApplyPatchFileSystem(config, invocation) }
|
||||
: { run: (command, input) => runSshCaptureCommand(config, invocation, command, input) };
|
||||
const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
|
||||
const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
|
||||
? { fs: createWindowsApplyPatchFileSystem(config, applyPatch.invocation) }
|
||||
: { run: (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input) };
|
||||
return await runApplyPatchV2({
|
||||
executor,
|
||||
stdin: process.stdin,
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
argv: normalizedArgs.slice(1),
|
||||
argv: applyPatch.argv,
|
||||
timing: {
|
||||
providerId: invocation.providerId,
|
||||
route: invocation.route.raw,
|
||||
providerId: applyPatch.invocation.providerId,
|
||||
route: applyPatch.invocation.route.raw,
|
||||
transport: "backend-core-broker",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user