408 lines
15 KiB
JavaScript
408 lines
15 KiB
JavaScript
import { describe, expect, test } from "bun:test";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { resolve } from "node:path";
|
|
|
|
const projectRoot = resolve(import.meta.dir, "..");
|
|
const installerPath = resolve(projectRoot, "apps/site/public/install.sh");
|
|
const composePath = resolve(projectRoot, "compose.yaml");
|
|
const hostedComposePath = resolve(projectRoot, "apps/site/public/compose.yaml");
|
|
|
|
function runBash(scriptPath, arguments_, environment) {
|
|
const harnessDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-installer-harness-"));
|
|
const resultPath = resolve(harnessDirectory, "result.json");
|
|
try {
|
|
const harness = spawnSync(
|
|
"node",
|
|
[
|
|
"-e",
|
|
[
|
|
'const { spawnSync } = require("node:child_process");',
|
|
'const { writeFileSync } = require("node:fs");',
|
|
"const result = spawnSync(",
|
|
' "/bin/bash",',
|
|
" [process.env.EDGE_EVER_TEST_INSTALLER, ...JSON.parse(process.env.EDGE_EVER_TEST_ARGUMENTS)],",
|
|
' { encoding: "utf8", env: process.env },',
|
|
");",
|
|
"writeFileSync(process.env.EDGE_EVER_TEST_RESULT, JSON.stringify({",
|
|
" status: result.status,",
|
|
" signal: result.signal,",
|
|
' stdout: result.stdout || "",',
|
|
' stderr: result.stderr || "",',
|
|
" error: result.error?.message,",
|
|
"}));",
|
|
].join("\n"),
|
|
],
|
|
{
|
|
env: {
|
|
...environment,
|
|
EDGE_EVER_TEST_ARGUMENTS: JSON.stringify(arguments_),
|
|
EDGE_EVER_TEST_INSTALLER: scriptPath,
|
|
EDGE_EVER_TEST_RESULT: resultPath,
|
|
},
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
if (harness.status !== 0) {
|
|
throw new Error(`installer test harness failed: ${harness.stderr}`);
|
|
}
|
|
return JSON.parse(readFileSync(resultPath, "utf8"));
|
|
} finally {
|
|
rmSync(harnessDirectory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function runInstaller(arguments_, environment) {
|
|
return runBash(installerPath, arguments_, environment);
|
|
}
|
|
|
|
describe("Docker installer", () => {
|
|
test("publishes the repository Compose file unchanged", async () => {
|
|
expect(await readFile(hostedComposePath, "utf8")).toBe(await readFile(composePath, "utf8"));
|
|
});
|
|
|
|
test("installs with TCR and preserves the generated password on rerun", async () => {
|
|
const fixture = await mkdtemp(resolve(tmpdir(), "edgeever-installer-"));
|
|
const fakeBin = resolve(fixture, "bin");
|
|
const installDirectory = resolve(fixture, "edgeever");
|
|
const dockerLog = resolve(fixture, "docker.log");
|
|
const curlLog = resolve(fixture, "curl.log");
|
|
const fakeDocker = resolve(fakeBin, "docker");
|
|
const fakeCurl = resolve(fakeBin, "curl");
|
|
const fakeUname = resolve(fakeBin, "uname");
|
|
const fakeCrontab = resolve(fakeBin, "crontab");
|
|
const crontabStore = resolve(fixture, "crontab");
|
|
|
|
try {
|
|
await mkdir(fakeBin, { recursive: true });
|
|
await writeFile(
|
|
fakeDocker,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
printf '%s\\n' "$*" >> "$EDGE_EVER_TEST_DOCKER_LOG"
|
|
if [[ "$1" == "version" ]]; then
|
|
printf '27.5.1\\n'
|
|
elif [[ "$1" == "compose" && "$2" == "version" && "\${3:-}" == "--short" ]]; then
|
|
printf '2.32.4\\n'
|
|
elif [[ "$1" == "inspect" && "$*" == *".Config.Image"* ]]; then
|
|
printf 'ccr.ccs.tencentyun.com/edgeever/edgeever:latest\\n'
|
|
elif [[ "$1" == "inspect" ]]; then
|
|
printf 'healthy\\n'
|
|
elif [[ "$1" == "compose" && "$*" == *" ps -q edgeever"* ]]; then
|
|
printf 'edgeever-test-container\\n'
|
|
fi
|
|
`,
|
|
);
|
|
await writeFile(
|
|
fakeCurl,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
printf '%s\n' "$*" >> "$EDGE_EVER_TEST_CURL_LOG"
|
|
output=''
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ "$1" == "--output" ]]; then
|
|
output="$2"
|
|
shift 2
|
|
else
|
|
shift
|
|
fi
|
|
done
|
|
cp "$EDGE_EVER_TEST_COMPOSE_SOURCE" "$output"
|
|
`,
|
|
);
|
|
await writeFile(
|
|
fakeCrontab,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
if [[ "\${1:-}" == "-l" ]]; then
|
|
if [[ -f "$EDGE_EVER_TEST_CRONTAB" ]]; then
|
|
cat "$EDGE_EVER_TEST_CRONTAB"
|
|
else
|
|
exit 1
|
|
fi
|
|
else
|
|
cp "$1" "$EDGE_EVER_TEST_CRONTAB"
|
|
fi
|
|
`,
|
|
);
|
|
await writeFile(
|
|
fakeUname,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
case "\${1:-}" in
|
|
-s) printf 'Linux\\n' ;;
|
|
-r) printf '3.10.0-1160.119.1.el7.x86_64\\n' ;;
|
|
-sr) printf 'Linux 3.10.0-1160.119.1.el7.x86_64\\n' ;;
|
|
-m) printf 'x86_64\\n' ;;
|
|
*) printf 'Linux\\n' ;;
|
|
esac
|
|
`,
|
|
);
|
|
await chmod(fakeDocker, 0o755);
|
|
await chmod(fakeCurl, 0o755);
|
|
await chmod(fakeUname, 0o755);
|
|
await chmod(fakeCrontab, 0o755);
|
|
|
|
const environment = {
|
|
...process.env,
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
EDGE_EVER_INSTALL_DIR: installDirectory,
|
|
EDGE_EVER_TEST_COMPOSE_SOURCE: composePath,
|
|
EDGE_EVER_TEST_CRONTAB: crontabStore,
|
|
EDGE_EVER_TEST_CURL_LOG: curlLog,
|
|
EDGE_EVER_TEST_DOCKER_LOG: dockerLog,
|
|
};
|
|
const first = runInstaller(["--mirror", "tcr", "--port", "18789"], environment);
|
|
if (first.status !== 0) {
|
|
const commands = await readFile(dockerLog, "utf8").catch(() => "no Docker commands");
|
|
throw new Error(
|
|
`installer failed (exit=${first.status}, signal=${first.signal}):\n${first.stdout}\n${first.stderr}\n${commands}`,
|
|
);
|
|
}
|
|
expect(first.status).toBe(0);
|
|
|
|
const firstEnvironment = await readFile(resolve(installDirectory, ".env"), "utf8");
|
|
expect(firstEnvironment).toContain(
|
|
"EDGE_EVER_IMAGE='ccr.ccs.tencentyun.com/edgeever/edgeever'",
|
|
);
|
|
expect(firstEnvironment).toContain("EDGE_EVER_VERSION='latest'");
|
|
expect(firstEnvironment).toContain("EDGE_EVER_PORT='18789'");
|
|
expect(firstEnvironment).toMatch(/EDGE_EVER_AUTH_PASSWORD='[a-f0-9]{32}'/);
|
|
const generatedPassword = firstEnvironment.match(
|
|
/EDGE_EVER_AUTH_PASSWORD='([a-f0-9]{32})'/,
|
|
)?.[1];
|
|
if (!generatedPassword) {
|
|
throw new Error("installer did not generate a password");
|
|
}
|
|
expect(first.stderr).toContain("[STEP 1/6] Validate environment");
|
|
expect(first.stderr).toContain("[STEP 6/6] Wait for health check");
|
|
expect(first.stderr).toContain("[SUCCESS] EdgeEver is ready");
|
|
expect(first.stderr).toContain("Host OS:");
|
|
expect(first.stderr).toContain("Kernel:");
|
|
expect(first.stderr).toContain("Architecture:");
|
|
expect(first.stderr).toContain("Docker Engine: 27.5.1");
|
|
expect(first.stderr).toContain("Docker Compose: 2.32.4");
|
|
expect(first.stderr).toContain(
|
|
"Linux kernel 3.10.0-1160.119.1.el7.x86_64 does not meet EdgeEver's minimum 5.6 requirement",
|
|
);
|
|
expect(first.stderr).toContain(
|
|
"This host is outside the supported Docker environment",
|
|
);
|
|
expect(first.stderr).toContain(
|
|
"upgrade the host OS/kernel if startup reports EISDIR",
|
|
);
|
|
expect(first.stderr).toContain(
|
|
"EdgeEver target: ccr.ccs.tencentyun.com/edgeever/edgeever:latest",
|
|
);
|
|
expect(first.stderr).toContain(
|
|
"Running image: ccr.ccs.tencentyun.com/edgeever/edgeever:latest",
|
|
);
|
|
expect(first.stderr).not.toContain(generatedPassword);
|
|
expect(first.stdout).toContain(`Password: ${generatedPassword}`);
|
|
expect(first.stdout).toContain("docker compose logs --tail 200 -f edgeever");
|
|
expect(first.stdout).toContain("Automatic updates: daily at 04:17 server time");
|
|
|
|
const second = runInstaller([], environment);
|
|
if (second.status !== 0) {
|
|
throw new Error(
|
|
`installer rerun failed (exit=${second.status}, signal=${second.signal}):\n${second.stdout}\n${second.stderr}`,
|
|
);
|
|
}
|
|
expect(second.status).toBe(0);
|
|
expect(await readFile(resolve(installDirectory, ".env"), "utf8")).toBe(firstEnvironment);
|
|
|
|
const updaterSource = await readFile(resolve(installDirectory, "update.sh"), "utf8");
|
|
expect(updaterSource).toContain("Checking for updates");
|
|
expect(updaterSource).toContain("Update complete. Running image:");
|
|
expect(updaterSource).toContain("Persistent storage diagnostics:");
|
|
expect(updaterSource).toContain("/data write test: failed.");
|
|
expect(updaterSource).not.toContain(generatedPassword);
|
|
const installedCrontab = await readFile(crontabStore, "utf8");
|
|
expect(installedCrontab).toContain("17 4 * * *");
|
|
expect(installedCrontab).toContain("edgeever-auto-update:edgeever");
|
|
expect(installedCrontab.match(/edgeever-auto-update:edgeever/g)).toHaveLength(1);
|
|
|
|
const update = runBash(resolve(installDirectory, "update.sh"), [], environment);
|
|
expect(update.status).toBe(0);
|
|
expect(update.stdout).toContain("Checking for updates.");
|
|
expect(update.stdout).toContain(
|
|
"Update complete. Running image: ccr.ccs.tencentyun.com/edgeever/edgeever:latest.",
|
|
);
|
|
|
|
const disabled = runInstaller(["--no-auto-update"], environment);
|
|
expect(disabled.status).toBe(0);
|
|
expect(disabled.stdout).toContain("Automatic updates: disabled");
|
|
expect(
|
|
await readFile(resolve(installDirectory, "update.sh"), "utf8").then(
|
|
() => true,
|
|
() => false,
|
|
),
|
|
).toBe(false);
|
|
expect(await readFile(crontabStore, "utf8")).not.toContain("edgeever-auto-update:edgeever");
|
|
|
|
const log = await readFile(dockerLog, "utf8");
|
|
expect(log).toContain("compose version");
|
|
expect(log.match(/ pull$/gm)).toHaveLength(4);
|
|
expect(log.match(/ up -d --remove-orphans$/gm)).toHaveLength(4);
|
|
expect(log).toContain("inspect --format");
|
|
const curlRequests = await readFile(curlLog, "utf8");
|
|
expect(
|
|
curlRequests.match(
|
|
/edgeever-installer-1256854452\.cos\.ap-guangzhou\.myqcloud\.com\/compose\.yaml/g,
|
|
),
|
|
).toHaveLength(4);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
}, 15_000);
|
|
|
|
test("prints actionable diagnostics without exposing the password", async () => {
|
|
const fixture = await mkdtemp(resolve(tmpdir(), "edgeever-installer-failure-"));
|
|
const fakeBin = resolve(fixture, "bin");
|
|
const installDirectory = resolve(fixture, "edgeever");
|
|
const fakeDocker = resolve(fakeBin, "docker");
|
|
const fakeCurl = resolve(fakeBin, "curl");
|
|
const secret = "installer-secret-must-stay-hidden";
|
|
|
|
try {
|
|
await mkdir(fakeBin, { recursive: true });
|
|
await writeFile(
|
|
fakeDocker,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
if [[ "$*" == *" pull" ]]; then
|
|
printf 'mock pull failure\n' >&2
|
|
exit 42
|
|
elif [[ "$*" == *" ps -a" ]]; then
|
|
printf 'mock container status\n'
|
|
elif [[ "$*" == *" logs --tail 80 edgeever" ]]; then
|
|
printf 'mock recent logs\n'
|
|
fi
|
|
`,
|
|
);
|
|
await writeFile(
|
|
fakeCurl,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ "$1" == "--output" ]]; then
|
|
cp "$EDGE_EVER_TEST_COMPOSE_SOURCE" "$2"
|
|
exit 0
|
|
fi
|
|
shift
|
|
done
|
|
exit 1
|
|
`,
|
|
);
|
|
await chmod(fakeDocker, 0o755);
|
|
await chmod(fakeCurl, 0o755);
|
|
|
|
const result = runInstaller(
|
|
[],
|
|
{
|
|
...process.env,
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
EDGE_EVER_AUTH_PASSWORD: secret,
|
|
EDGE_EVER_INSTALL_DIR: installDirectory,
|
|
EDGE_EVER_TEST_COMPOSE_SOURCE: composePath,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(42);
|
|
expect(result.stderr).toContain("A command failed (exit code: 42");
|
|
expect(result.stderr).toContain("Stage: Pull image and start container");
|
|
expect(result.stderr).toContain("Container status:");
|
|
expect(result.stderr).toContain("mock container status");
|
|
expect(result.stderr).toContain("Recent container logs:");
|
|
expect(result.stderr).toContain("mock recent logs");
|
|
expect(result.stderr).toContain("Troubleshooting commands:");
|
|
expect(result.stderr).toContain("docs/deploy-docker.md");
|
|
expect(result.stderr).not.toContain(secret);
|
|
expect(result.stdout).not.toContain(secret);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("identifies a NAS bind-mount permission failure and prints a targeted repair", async () => {
|
|
const fixture = await mkdtemp(resolve(tmpdir(), "edgeever-installer-permission-"));
|
|
const fakeBin = resolve(fixture, "bin");
|
|
const installDirectory = resolve(fixture, "edgeever");
|
|
const nasDataDirectory = "/volume1/docker/edgeever data";
|
|
const fakeDocker = resolve(fakeBin, "docker");
|
|
const fakeCurl = resolve(fakeBin, "curl");
|
|
|
|
try {
|
|
await mkdir(fakeBin, { recursive: true });
|
|
await writeFile(
|
|
fakeDocker,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
if [[ "$1" == "version" ]]; then
|
|
printf '27.5.1\\n'
|
|
elif [[ "$1" == "compose" && "$2" == "version" && "\${3:-}" == "--short" ]]; then
|
|
printf '2.32.4\\n'
|
|
elif [[ "$1" == "compose" && "$*" == *" ps -q edgeever"* ]]; then
|
|
printf 'edgeever-permission-container\\n'
|
|
elif [[ "$1" == "inspect" && "$*" == *".Config.User"* ]]; then
|
|
printf 'bun\\n'
|
|
elif [[ "$1" == "inspect" && "$*" == *".Mounts"* ]]; then
|
|
printf 'bind||${nasDataDirectory}\\n'
|
|
elif [[ "$1" == "inspect" ]]; then
|
|
printf 'exited\\n'
|
|
elif [[ "$1" == "compose" && "$*" == *" run --rm --no-deps --entrypoint /bin/sh edgeever"* ]]; then
|
|
printf '/bin/sh: cannot create /data/.edgeever-write-test: Permission denied\\n' >&2
|
|
printf 'EDGEEVER_DATA_WRITE_FAILED\\n' >&2
|
|
printf 'Runtime identity: uid=1000(bun) gid=1000(bun)\\n' >&2
|
|
printf 'Data directory: drwxr-xr-x 2 0 0 4096 /data\\n' >&2
|
|
exit 73
|
|
elif [[ "$1" == "compose" && "$*" == *" logs --tail 80 edgeever"* ]]; then
|
|
printf 'SQLITE_CANTOPEN: unable to open database file\\n'
|
|
fi
|
|
`,
|
|
);
|
|
await writeFile(
|
|
fakeCurl,
|
|
`#!/usr/bin/env bash
|
|
set -eu
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ "$1" == "--output" ]]; then
|
|
cp "$EDGE_EVER_TEST_COMPOSE_SOURCE" "$2"
|
|
exit 0
|
|
fi
|
|
shift
|
|
done
|
|
exit 1
|
|
`,
|
|
);
|
|
await chmod(fakeDocker, 0o755);
|
|
await chmod(fakeCurl, 0o755);
|
|
|
|
const result = runInstaller(
|
|
["--no-auto-update"],
|
|
{
|
|
...process.env,
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
EDGE_EVER_INSTALL_DIR: installDirectory,
|
|
EDGE_EVER_TEST_COMPOSE_SOURCE: composePath,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Persistent storage diagnostics:");
|
|
expect(result.stderr).toContain("Container user: bun (EdgeEver expects UID/GID 1000:1000)");
|
|
expect(result.stderr).toContain(`/data mount: type=bind, source=${nasDataDirectory}`);
|
|
expect(result.stderr).toContain("/data write test: failed");
|
|
expect(result.stderr).toContain("Permission denied");
|
|
expect(result.stderr).toContain("Runtime identity: uid=1000(bun) gid=1000(bun)");
|
|
expect(result.stderr).toContain("The NAS/host directory must be writable by UID/GID 1000:1000");
|
|
expect(result.stderr).toContain("sudo chown -R 1000:1000 -- /volume1/docker/edgeever\\ data");
|
|
expect(result.stderr).toContain("NAS uses ACL permissions");
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|