Skip to content

Commit efcc8ae

Browse files
committed
fix(runtime): wire packaged intake sandbox
1 parent 16d37c4 commit efcc8ae

6 files changed

Lines changed: 759 additions & 21 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { join } from "node:path";
2+
3+
import type { SandboxLaunch } from "@siteops/release-manifest/intake";
4+
import { describe, expect, it } from "vitest";
5+
6+
import {
7+
FIXED_ARCHIVE_IMAGE,
8+
PackagedIntakeRuntimeError,
9+
createFixedArchiveDockerSandboxRunner,
10+
type DockerCommand,
11+
type DockerCommandExecutor,
12+
type DockerCommandResult,
13+
} from "../docker-intake-sandbox.js";
14+
import { createSandboxLauncher } from "../intake-sandbox.js";
15+
16+
const CONTAINER_ID = "a".repeat(64);
17+
const CONTAINER_NAME = "siteops-intake-00000000-0000-4000-8000-000000000001";
18+
const FIXED_ARCHIVE_SCRIPT = [
19+
"set -eu",
20+
"umask 077",
21+
"mkdir /workspace/staged",
22+
"cp -R /source/. /workspace/staged/",
23+
"find /workspace/staged -exec touch -t 197001010000.00 {} +",
24+
"find /workspace/staged -type d -exec chmod 0555 {} +",
25+
"find /workspace/staged -type f -exec chmod 0444 {} +",
26+
"cd /workspace/staged",
27+
"find . -mindepth 1 -type f -print0 |",
28+
" LC_ALL=C sort -z |",
29+
" cpio -o -0 -H newc -R 10001:10001 --ignore-devno --renumber-inodes > /output/release.cpio",
30+
].join("\n");
31+
32+
const launch = (root: string): SandboxLaunch => ({
33+
network: "none",
34+
privileged: false,
35+
sourceMount: "read_only",
36+
workspacePath: join(root, "workspace"),
37+
sourcePath: join(root, "workspace", "source"),
38+
outputPath: join(root, "workspace", "output"),
39+
});
40+
41+
describe("fixed-archive Docker intake sandbox", () => {
42+
it("uses only the fixed image and archive command under the bounded plan", async () => {
43+
const commands: DockerCommand[] = [];
44+
const executor: DockerCommandExecutor = {
45+
execute: (command: DockerCommand): Promise<DockerCommandResult> => {
46+
commands.push(command);
47+
return Promise.resolve({
48+
stdout: command.args[0] === "create" ? `${CONTAINER_ID}\n` : "",
49+
stderr: "",
50+
});
51+
},
52+
};
53+
const root = "/var/lib/siteops/intake/workspaces/intake-safe";
54+
const sandboxLaunch = launch(root);
55+
const runner = createFixedArchiveDockerSandboxRunner(
56+
executor,
57+
() => CONTAINER_NAME,
58+
);
59+
60+
const receipt = await createSandboxLauncher(runner)(sandboxLaunch);
61+
62+
expect(commands).toEqual([
63+
{
64+
executable: "docker",
65+
args: [
66+
"create",
67+
"--name",
68+
CONTAINER_NAME,
69+
"--pull",
70+
"never",
71+
"--network",
72+
"none",
73+
"--user",
74+
"10001:10001",
75+
"--read-only",
76+
"--cap-drop",
77+
"ALL",
78+
"--security-opt",
79+
"no-new-privileges:true",
80+
"--cpus",
81+
"1",
82+
"--memory",
83+
"268435456",
84+
"--pids-limit",
85+
"64",
86+
"--mount",
87+
`type=bind,src=${sandboxLaunch.sourcePath},dst=/source,readonly`,
88+
"--tmpfs",
89+
"/tmp:rw,noexec,nosuid,nodev,size=8388608,uid=10001,gid=10001,mode=1777",
90+
"--tmpfs",
91+
"/workspace:rw,noexec,nosuid,nodev,size=33554432,uid=10001,gid=10001,mode=0700",
92+
"--tmpfs",
93+
"/output:rw,noexec,nosuid,nodev,size=33554432,uid=10001,gid=10001,mode=0700",
94+
"--entrypoint",
95+
"/bin/sh",
96+
FIXED_ARCHIVE_IMAGE,
97+
"-ceu",
98+
FIXED_ARCHIVE_SCRIPT,
99+
],
100+
timeoutMs: 60_000,
101+
},
102+
{
103+
executable: "docker",
104+
args: ["start", "--attach", CONTAINER_NAME],
105+
timeoutMs: 60_000,
106+
},
107+
{
108+
executable: "docker",
109+
args: [
110+
"cp",
111+
`${CONTAINER_NAME}:/output/release.cpio`,
112+
join(sandboxLaunch.outputPath, "release.cpio"),
113+
],
114+
timeoutMs: 60_000,
115+
},
116+
{
117+
executable: "docker",
118+
args: ["rm", "--force", CONTAINER_NAME],
119+
timeoutMs: 60_000,
120+
},
121+
]);
122+
expect(receipt).toEqual({
123+
artifactPath: join(sandboxLaunch.outputPath, "release.cpio"),
124+
resolvedDependencies: [
125+
{
126+
uri: "https://registry-1.docker.io/v2/library/alpine",
127+
digest:
128+
"sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f",
129+
},
130+
],
131+
});
132+
});
133+
134+
it("fails closed with a typed error when Docker is unavailable", async () => {
135+
const unavailable = Object.assign(new Error("spawn docker ENOENT"), {
136+
code: "ENOENT",
137+
});
138+
let invocationCount = 0;
139+
const executor: DockerCommandExecutor = {
140+
execute: () => {
141+
invocationCount += 1;
142+
return Promise.reject(unavailable);
143+
},
144+
};
145+
const runner = createFixedArchiveDockerSandboxRunner(
146+
executor,
147+
() => CONTAINER_NAME,
148+
);
149+
150+
const result = createSandboxLauncher(runner)(launch("/tmp/unavailable"));
151+
152+
await expect(result).rejects.toMatchObject({
153+
name: "PackagedIntakeRuntimeError",
154+
code: "docker_unavailable",
155+
operation: "create",
156+
});
157+
expect(invocationCount).toBe(1);
158+
});
159+
160+
it("removes the container after denied execution", async () => {
161+
const commands: DockerCommand[] = [];
162+
const executor: DockerCommandExecutor = {
163+
execute: (command: DockerCommand): Promise<DockerCommandResult> => {
164+
commands.push(command);
165+
if (command.args[0] === "create") {
166+
return Promise.resolve({
167+
stdout: `${CONTAINER_ID}\n`,
168+
stderr: "",
169+
});
170+
}
171+
if (command.args[0] === "start") {
172+
return Promise.reject(
173+
new Error(
174+
"permission denied while trying to connect to the Docker daemon socket",
175+
),
176+
);
177+
}
178+
return Promise.resolve({ stdout: "", stderr: "" });
179+
},
180+
};
181+
const runner = createFixedArchiveDockerSandboxRunner(
182+
executor,
183+
() => CONTAINER_NAME,
184+
);
185+
186+
const result = createSandboxLauncher(runner)(launch("/tmp/denied"));
187+
188+
await expect(result).rejects.toEqual(
189+
expect.objectContaining<Partial<PackagedIntakeRuntimeError>>({
190+
name: "PackagedIntakeRuntimeError",
191+
code: "docker_access_denied",
192+
operation: "start",
193+
}),
194+
);
195+
expect(commands.map(({ args }) => args[0])).toEqual([
196+
"create",
197+
"start",
198+
"rm",
199+
]);
200+
});
201+
202+
it("reports cleanup failure as typed and never retries without isolation", async () => {
203+
const executor: DockerCommandExecutor = {
204+
execute: (command: DockerCommand): Promise<DockerCommandResult> => {
205+
if (command.args[0] === "create") {
206+
return Promise.resolve({
207+
stdout: `${CONTAINER_ID}\n`,
208+
stderr: "",
209+
});
210+
}
211+
if (command.args[0] === "rm") {
212+
return Promise.reject(new Error("Docker cleanup failed"));
213+
}
214+
return Promise.resolve({ stdout: "", stderr: "" });
215+
},
216+
};
217+
const runner = createFixedArchiveDockerSandboxRunner(
218+
executor,
219+
() => CONTAINER_NAME,
220+
);
221+
222+
const result = createSandboxLauncher(runner)(launch("/tmp/cleanup"));
223+
224+
await expect(result).rejects.toMatchObject({
225+
name: "PackagedIntakeRuntimeError",
226+
code: "sandbox_cleanup_failed",
227+
operation: "cleanup",
228+
});
229+
});
230+
231+
it("force-removes the preassigned container after an ambiguous create timeout", async () => {
232+
const commands: DockerCommand[] = [];
233+
const executor: DockerCommandExecutor = {
234+
execute: (command: DockerCommand): Promise<DockerCommandResult> => {
235+
commands.push(command);
236+
if (command.args[0] === "create") {
237+
return Promise.reject(new Error("Docker create timed out"));
238+
}
239+
if (command.args[0] === "rm") {
240+
return Promise.reject(
241+
new Error(`No such container: ${CONTAINER_NAME}`),
242+
);
243+
}
244+
return Promise.resolve({ stdout: "", stderr: "" });
245+
},
246+
};
247+
const runner = createFixedArchiveDockerSandboxRunner(
248+
executor,
249+
() => CONTAINER_NAME,
250+
);
251+
252+
const result = createSandboxLauncher(runner)(launch("/tmp/create-timeout"));
253+
254+
await expect(result).rejects.toMatchObject({
255+
name: "PackagedIntakeRuntimeError",
256+
code: "sandbox_execution_failed",
257+
operation: "create",
258+
});
259+
expect(commands).toHaveLength(2);
260+
expect(commands[0]?.args).toContain("--name");
261+
expect(commands[0]?.args).toContain(CONTAINER_NAME);
262+
expect(commands[1]).toEqual({
263+
executable: "docker",
264+
args: ["rm", "--force", CONTAINER_NAME],
265+
timeoutMs: 60_000,
266+
});
267+
});
268+
});

0 commit comments

Comments
 (0)