paperclip/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts

239 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { Readable, Writable } from "node:stream";
import type { PassThrough } from "node:stream";
// Mock the k8s client so `execInPod` runs against a scripted WebSocket exec: the
// fake `Exec.exec` streams stdout chunks into the provided PassThrough and, for
// the success path, reports an exit status. This exercises the host-side stdout
// accumulation cap without a real cluster.
type StatusCb = (status: {
status: string;
details?: { causes?: { reason?: string; message?: string }[] };
}) => void;
let scriptedExec: (
stdout: PassThrough,
stderr: PassThrough,
statusCb: StatusCb,
stdin: PassThrough | null,
) => void = () => undefined;
vi.mock("@kubernetes/client-node", () => {
class Exec {
constructor(_kc: unknown) {}
async exec(
_namespace: string,
_podName: string,
_containerName: string,
_command: string[],
stdout: PassThrough,
stderr: PassThrough,
stdin: PassThrough | null,
_tty: boolean,
statusCb: StatusCb,
) {
// Defer so the caller has wired its stream listeners first.
setImmediate(() => scriptedExec(stdout, stderr, statusCb, stdin));
return { close() {} };
}
}
return { Exec };
});
const { execInPod, execInPodStreaming } = await import("../../src/pod-exec.js");
const KC = {} as never;
describe("execInPod stdout cap", () => {
it("fails closed when pod stdout exceeds the cap", async () => {
scriptedExec = (stdout) => {
// Emit more than the cap in a single chunk; the host must reject.
stdout.write(Buffer.alloc(64, 0x41));
};
await expect(
execInPod(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], undefined, 5_000, 16),
).rejects.toThrow(/cap|exceeded/i);
});
it("accepts stdout within the cap and returns the accumulated output", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stdout.write(Buffer.from("hello", "utf-8"));
stdout.end();
stderr.end();
statusCb({ status: "Success" });
};
const result = await execInPod(
KC,
"ns",
"pod",
"agent",
["/bin/sh", "-c", ":"],
undefined,
5_000,
1024,
);
expect(result).toEqual({ exitCode: 0, stdout: "hello", stderr: "" });
});
it("leaves stdout unbounded when no cap is provided", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stdout.write(Buffer.alloc(4096, 0x42));
stdout.end();
stderr.end();
statusCb({ status: "Success" });
};
const result = await execInPod(
KC,
"ns",
"pod",
"agent",
["/bin/sh", "-c", ":"],
undefined,
5_000,
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toHaveLength(4096);
});
});
describe("execInPod stderr cap", () => {
it("fails closed when pod stderr exceeds the cap", async () => {
// stderr is equally pod-controlled: a malicious pod that floods stderr must
// not grow the host accumulator without bound. Same DoS class as stdout.
scriptedExec = (_stdout, stderr) => {
stderr.write(Buffer.alloc(64, 0x45));
};
await expect(
// maxStdoutBytes generous, maxStderrBytes = 16 -> stderr must trip.
execInPod(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], undefined, 5_000, 1024, 16),
).rejects.toThrow(/stderr.*cap|cap.*stderr|exceeded/i);
});
it("accepts stderr within the cap and returns the accumulated output", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stderr.write(Buffer.from("warn", "utf-8"));
stdout.end();
stderr.end();
statusCb({ status: "Success" });
};
const result = await execInPod(
KC,
"ns",
"pod",
"agent",
["/bin/sh", "-c", ":"],
undefined,
5_000,
1024,
1024,
);
expect(result).toEqual({ exitCode: 0, stdout: "", stderr: "warn" });
});
it("leaves stderr unbounded when no cap is provided", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stderr.write(Buffer.alloc(4096, 0x46));
stdout.end();
stderr.end();
statusCb({ status: "Success" });
};
const result = await execInPod(
KC,
"ns",
"pod",
"agent",
["/bin/sh", "-c", ":"],
undefined,
5_000,
1024,
);
expect(result.exitCode).toBe(0);
expect(result.stderr).toHaveLength(4096);
});
});
describe("execInPodStreaming", () => {
it("streams the command's stdout into the caller sink and resolves with exit code + stderr", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stdout.write(Buffer.from("chunk-one;"));
stdout.write(Buffer.from("chunk-two"));
stdout.end();
stderr.write(Buffer.from("warn"));
stderr.end();
statusCb({ status: "Success" });
};
const chunks: Buffer[] = [];
const sink = new Writable({
write(chunk: Buffer, _enc, cb) {
chunks.push(Buffer.from(chunk));
cb();
},
});
const result = await execInPodStreaming(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], {
stdout: sink,
timeoutMs: 5_000,
});
expect(result).toEqual({ exitCode: 0, stderr: "warn" });
expect(Buffer.concat(chunks).toString("utf-8")).toBe("chunk-one;chunk-two");
});
it("streams the caller stdin Readable into the pod command", async () => {
let received = Buffer.alloc(0);
scriptedExec = (stdout, stderr, statusCb, stdin) => {
if (stdin) {
stdin.on("data", (chunk: Buffer) => {
received = Buffer.concat([received, chunk]);
});
}
// Resolve once the source has been fully piped through.
setImmediate(() => {
stdout.end();
stderr.end();
statusCb({ status: "Success" });
});
};
const source = Readable.from([Buffer.from("payload-bytes")]);
const result = await execInPodStreaming(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], {
stdin: source,
timeoutMs: 5_000,
});
expect(result.exitCode).toBe(0);
expect(received.toString("utf-8")).toBe("payload-bytes");
});
it("fails closed when the pod floods stderr past the cap", async () => {
scriptedExec = (_stdout, stderr) => {
stderr.write(Buffer.alloc(64, 0x45));
};
const sink = new Writable({ write(_c, _e, cb) { cb(); } });
await expect(
execInPodStreaming(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], {
stdout: sink,
timeoutMs: 5_000,
maxStderrBytes: 16,
}),
).rejects.toThrow(/stderr.*cap|exceeded/i);
});
it("fails closed when the caller sink errors (e.g. a streamed-bytes disk guard trips)", async () => {
scriptedExec = (stdout, stderr, statusCb) => {
stdout.write(Buffer.alloc(4096, 0x42));
stdout.end();
stderr.end();
statusCb({ status: "Success" });
};
// A sink that rejects any write, standing in for the file-sync disk guard.
const sink = new Writable({
write(_chunk, _enc, cb) {
cb(new Error("streamed-output disk guard tripped"));
},
});
await expect(
execInPodStreaming(KC, "ns", "pod", "agent", ["/bin/sh", "-c", ":"], {
stdout: sink,
timeoutMs: 5_000,
}),
).rejects.toThrow(/disk guard/i);
});
});