fix: restore k3s script one-liner stdout

This commit is contained in:
Codex
2026-05-29 08:53:10 +00:00
parent 6a56234b87
commit 93477a3330
2 changed files with 48 additions and 4 deletions
+40 -4
View File
@@ -1322,7 +1322,7 @@ function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): P
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "sh") {
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
return buildK3sScriptOperation(args.slice(1));
}
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
@@ -1359,7 +1359,7 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script") return { remoteCommand: buildK3sScriptCommand([...targetArgs, ...operationArgs]), requiresStdin: true, invocationKind: "helper" };
if (operation === "script") return buildK3sScriptOperation([...targetArgs, ...operationArgs]);
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", parsed.command]), requiresStdin: false, invocationKind: "helper" };
@@ -1439,7 +1439,11 @@ function buildK3sCommand(providerId: string, args: string[]): string {
}
if (action === "guard") return buildK3sGuardCommand(providerId);
if (action === "exec") return buildK3sExecCommand(args.slice(1));
if (action === "script") return buildK3sScriptCommand(args.slice(1));
if (action === "script") {
const parsed = buildK3sScriptOperation(args.slice(1));
if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly");
return parsed.remoteCommand;
}
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
if (action === "kubectl") {
const kubectlArgs = args.slice(1);
@@ -1511,8 +1515,15 @@ function buildK3sExecCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sScriptCommand(args: string[]): string {
function buildK3sScriptOperation(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
if (parsed.shell === null && parsed.command.length > 0) {
return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper" };
}
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
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>");
@@ -1531,6 +1542,31 @@ function buildK3sScriptCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command");
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");
const command = parsed.command.length === 1 ? ["sh", "-c", parsed.command[0] ?? ""] : parsed.command;
if (parsed.namespace === null && parsed.resource === null) {
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");
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
}
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>");
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "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>");
@@ -475,6 +475,14 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
assertCondition(routeControlScript.parsed.requiresStdin === true, "k3s control-plane script operation must stream local stdin", routeControlScript);
assertCondition(routeControlScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'bash' '-s' '--' 'arg'", "D601:k3s script must inject native kubeconfig without manual export", routeControlScript);
const routeControlScriptOneLiner = parseSshInvocation("D601:k3s", ["script", "--", "echo k3s-script-ok"]);
assertCondition(routeControlScriptOneLiner.parsed.requiresStdin === false, "k3s control-plane script -- one-liner must not wait for stdin", routeControlScriptOneLiner);
assertCondition(routeControlScriptOneLiner.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'sh' '-c' 'echo k3s-script-ok'", "k3s control-plane script -- one-liner must run through the native kubeconfig shell path", routeControlScriptOneLiner);
const routePodScriptOneLiner = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["script", "--", "echo pod-script-ok"]);
assertCondition(routePodScriptOneLiner.parsed.requiresStdin === false, "k3s workload script -- one-liner must not wait for stdin", routePodScriptOneLiner);
assertCondition(routePodScriptOneLiner.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-c' 'echo pod-script-ok'", "k3s workload script -- one-liner must run as sh -c inside the workload", routePodScriptOneLiner);
const topLevelScriptSeparator = extractRemoteCliOptions(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]);
assertCondition(JSON.stringify(topLevelScriptSeparator.args) === JSON.stringify(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]), "top-level remote option parser must preserve command-local -- after the command starts", topLevelScriptSeparator);
const topLevelScriptInvocation = parseSshInvocation(topLevelScriptSeparator.args[1] as string, topLevelScriptSeparator.args.slice(2));