138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { expect } from "@playwright/test";
|
|
|
|
export function dockerInspectExists(containerID: string): boolean {
|
|
const res = spawnSync("docker", ["inspect", containerID], { stdio: "ignore" });
|
|
return res.status === 0;
|
|
}
|
|
|
|
export function dockerRemove(containerID: string): void {
|
|
spawnSync("docker", ["rm", "-f", containerID], { stdio: "ignore" });
|
|
}
|
|
|
|
export function dockerStop(containerID: string): void {
|
|
const res = spawnSync("docker", ["stop", containerID], { stdio: "ignore" });
|
|
if (res.status !== 0) {
|
|
throw new Error(`failed to stop Docker container ${containerID}`);
|
|
}
|
|
}
|
|
|
|
export function dockerState(containerID: string): string {
|
|
const res = spawnSync("docker", ["inspect", "-f", "{{.State.Status}}", containerID], {
|
|
encoding: "utf8",
|
|
});
|
|
if (res.status !== 0) return "missing";
|
|
return res.stdout.trim();
|
|
}
|
|
|
|
export function dockerCurrentBranch(containerID: string): string {
|
|
const res = spawnSync(
|
|
"docker",
|
|
["exec", containerID, "git", "-C", "/workspace", "branch", "--show-current"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
if (res.status !== 0) {
|
|
const diag = spawnSync(
|
|
"docker",
|
|
[
|
|
"exec",
|
|
containerID,
|
|
"sh",
|
|
"-lc",
|
|
"ls -la /workspace; git -C /workspace status --short --branch",
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const logs = spawnSync("docker", ["logs", "--tail", "40", containerID], { encoding: "utf8" });
|
|
return [
|
|
`ERR status=${res.status} state=${dockerState(containerID)}`,
|
|
`stderr=${res.stderr.trim()}`,
|
|
`diag=${diag.stdout.trim()} ${diag.stderr.trim()}`,
|
|
`logs=${logs.stdout.trim()} ${logs.stderr.trim()}`,
|
|
].join("\n");
|
|
}
|
|
return res.stdout.trim();
|
|
}
|
|
|
|
export function dockerFileContent(containerID: string, filePath: string): string {
|
|
const res = spawnSync("docker", ["exec", containerID, "cat", filePath], {
|
|
encoding: "utf8",
|
|
});
|
|
if (res.status !== 0) {
|
|
const logs = spawnSync("docker", ["logs", "--tail", "40", containerID], {
|
|
encoding: "utf8",
|
|
});
|
|
throw new Error(
|
|
[
|
|
`failed to read ${filePath} from Docker container ${containerID}`,
|
|
`stderr=${res.stderr.trim()}`,
|
|
`logs=${logs.stdout.trim()} ${logs.stderr.trim()}`,
|
|
].join("\n"),
|
|
);
|
|
}
|
|
return res.stdout;
|
|
}
|
|
|
|
export function dockerPathExists(containerID: string, filePath: string): boolean {
|
|
return (
|
|
spawnSync("docker", ["exec", containerID, "test", "-e", filePath], {
|
|
stdio: "ignore",
|
|
}).status === 0
|
|
);
|
|
}
|
|
|
|
export async function waitForDockerContainerRemoved(
|
|
containerID: string,
|
|
message: string,
|
|
): Promise<void> {
|
|
await expect
|
|
.poll(() => dockerInspectExists(containerID), {
|
|
timeout: 60_000,
|
|
message,
|
|
})
|
|
.toBe(false);
|
|
}
|
|
|
|
export type DockerExecResult = { status: number | null; stdout: string; stderr: string };
|
|
|
|
export function dockerExec(containerID: string, ...command: string[]): DockerExecResult {
|
|
const result = spawnSync("docker", ["exec", containerID, ...command], { encoding: "utf8" });
|
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
}
|
|
|
|
/**
|
|
* Returns true when the container can perform a full user-namespace map,
|
|
* which requires both the seccomp/AppArmor relaxation AND functioning
|
|
* sub-id mapping (newuidmap/newgidmap setuid binaries or /etc/subuid
|
|
* entries). Without this bwrap will fail with "setting up uid map:
|
|
* Permission denied" even when namespace creation is allowed.
|
|
*
|
|
* The probe uses `unshare --map-root-user` which goes one step beyond
|
|
* `unshare --user`: it also maps the UID inside the new namespace, the
|
|
* operation bwrap needs. On hosts where user namespaces are allowed but
|
|
* mapping is unavailable, `unshare --user` succeeds but this probe fails.
|
|
*/
|
|
export function dockerHasSubuidMapping(containerID: string): boolean {
|
|
const result = spawnSync(
|
|
"docker",
|
|
["exec", containerID, "sh", "-c", "unshare --map-root-user --user true 2>/dev/null"],
|
|
{ encoding: "utf8", stdio: "pipe" },
|
|
);
|
|
return result.status === 0;
|
|
}
|
|
|
|
export function dockerSecurityOpt(containerID: string): string[] | null {
|
|
const result = spawnSync(
|
|
"docker",
|
|
["inspect", "--format", "{{json .HostConfig.SecurityOpt}}", containerID],
|
|
{
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
if (result.status !== 0)
|
|
throw new Error(
|
|
`failed to inspect Docker SecurityOpt for ${containerID}: ${result.stderr.trim()}`,
|
|
);
|
|
return JSON.parse(result.stdout) as string[] | null;
|
|
}
|