Files
kandev/apps/web/e2e/helpers/docker.ts
T

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;
}