fix(docker): improve storage permission diagnostics

This commit is contained in:
tianma-if committed 2026-08-19 16:31:18 +08:00
1 parent faa98131f6
commit 0532f38008
4 files changed
+215

No files matched your search

+126
View File
@@ -39,6 +39,69 @@ step() {
log "STEP $1/6" "$2"
}
print_storage_diagnostics() {
local container_id container_user mount_info mount_type mount_name mount_source probe_output
container_id="$("${compose[@]}" ps -q edgeever 2>/dev/null || true)"
container_user=""
mount_info=""
if [[ -n "$container_id" ]]; then
container_user="$(docker inspect --format '{{.Config.User}}' "$container_id" 2>/dev/null || true)"
mount_info="$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Type}}|{{.Name}}|{{.Source}}{{end}}{{end}}' "$container_id" 2>/dev/null || true)"
fi
log INFO "Persistent storage diagnostics:"
log INFO "Container user: ${container_user:-unknown} (EdgeEver expects UID/GID 1000:1000)"
if [[ -n "$mount_info" ]]; then
IFS='|' read -r mount_type mount_name mount_source <<< "$mount_info"
log INFO "/data mount: type=${mount_type:-unknown}, source=${mount_source:-unknown}${mount_name:+, volume=$mount_name}"
else
mount_type=""
mount_name=""
mount_source=""
log WARN "Could not identify the /data mount; inspect it with: docker inspect $container_id"
fi
if probe_output="$("${compose[@]}" run --rm --no-deps --entrypoint /bin/sh edgeever -c '
probe="/data/.edgeever-write-test-$$"
if (umask 077 && : > "$probe" && rm -f "$probe"); then
exit 0
fi
printf "EDGEEVER_DATA_WRITE_FAILED\n" >&2
printf "Runtime identity: " >&2
id >&2 || true
printf "Data directory: " >&2
ls -ldn /data >&2 || true
exit 73
' 2>&1)"; then
log INFO "/data write test: passed"
return 0
fi
if [[ "$probe_output" != *"EDGEEVER_DATA_WRITE_FAILED"* ]]; then
log WARN "/data write test: unavailable (the diagnostic container could not run)"
[[ -z "$probe_output" ]] || printf '%s\n' "$probe_output" | sed 's/^/ /' >&2
return 0
fi
log ERROR "/data write test: failed"
printf '%s\n' "$probe_output" | sed '/^EDGEEVER_DATA_WRITE_FAILED$/d; s/^/ /' >&2
if [[ "$mount_type" == "bind" && -n "$mount_source" ]]; then
log INFO "The NAS/host directory must be writable by UID/GID 1000:1000. Suggested repair:"
printf ' sudo chown -R 1000:1000 -- %q\n' "$mount_source" >&2
printf ' sudo chmod -R u+rwX -- %q\n' "$mount_source" >&2
log INFO "If the NAS uses ACL permissions, grant UID 1000 read/write access in its control panel too."
elif [[ "$mount_type" == "volume" ]]; then
log INFO "The Docker volume must be writable by UID/GID 1000:1000. Suggested repair:"
printf ' cd %q\n' "$install_dir" >&2
printf " docker compose --project-name %q run --rm --no-deps --user 0 --entrypoint /bin/sh edgeever -c 'chown -R 1000:1000 /data'\n" "$project_name" >&2
else
log INFO "Check the Compose /data mount and grant UID/GID 1000:1000 read/write access."
printf ' cd %q\n' "$install_dir" >&2
printf ' docker compose --project-name %q config\n' "$project_name" >&2
fi
}
usage() {
cat <<'EOF'
Install or upgrade EdgeEver with Docker Compose.
@@ -75,6 +138,7 @@ print_diagnostics() {
"${compose[@]}" ps -a >&2 || true
log INFO "Recent container logs:"
"${compose[@]}" logs --tail 80 edgeever >&2 || true
print_storage_diagnostics || true
log INFO "Troubleshooting commands:"
printf ' cd %q\n' "$install_dir" >&2
printf ' docker compose --project-name %q --env-file .env --file compose.yaml ps -a\n' "$project_name" >&2
@@ -342,6 +406,67 @@ log() {
printf '[%s] [EdgeEver update] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
print_storage_diagnostics() {
local container_id container_user mount_info mount_type mount_name mount_source probe_output
container_id="$("${compose[@]}" ps -q edgeever 2>/dev/null || true)"
container_user=""
mount_info=""
if [[ -n "$container_id" ]]; then
container_user="$("$docker_command" inspect --format '{{.Config.User}}' "$container_id" 2>/dev/null || true)"
mount_info="$("$docker_command" inspect --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Type}}|{{.Name}}|{{.Source}}{{end}}{{end}}' "$container_id" 2>/dev/null || true)"
fi
log "Persistent storage diagnostics:"
log "Container user: ${container_user:-unknown} (EdgeEver expects UID/GID 1000:1000)."
if [[ -n "$mount_info" ]]; then
IFS='|' read -r mount_type mount_name mount_source <<< "$mount_info"
log "/data mount: type=${mount_type:-unknown}, source=${mount_source:-unknown}${mount_name:+, volume=$mount_name}."
else
mount_type=""
mount_name=""
mount_source=""
log "Could not identify the /data mount."
fi
if probe_output="$("${compose[@]}" run --rm --no-deps --entrypoint /bin/sh edgeever -c '
probe="/data/.edgeever-write-test-$$"
if (umask 077 && : > "$probe" && rm -f "$probe"); then
exit 0
fi
printf "EDGEEVER_DATA_WRITE_FAILED\n" >&2
printf "Runtime identity: " >&2
id >&2 || true
printf "Data directory: " >&2
ls -ldn /data >&2 || true
exit 73
' 2>&1)"; then
log "/data write test: passed."
return 0
fi
if [[ "$probe_output" != *"EDGEEVER_DATA_WRITE_FAILED"* ]]; then
log "/data write test: unavailable (the diagnostic container could not run)."
[[ -z "$probe_output" ]] || printf '%s\n' "$probe_output" | sed 's/^/ /'
return 0
fi
log "/data write test: failed."
printf '%s\n' "$probe_output" | sed '/^EDGEEVER_DATA_WRITE_FAILED$/d; s/^/ /'
if [[ "$mount_type" == "bind" && -n "$mount_source" ]]; then
log "Grant the NAS/host directory to UID/GID 1000:1000:"
printf ' sudo chown -R 1000:1000 -- %q\n' "$mount_source"
printf ' sudo chmod -R u+rwX -- %q\n' "$mount_source"
log "For NAS ACLs, also grant UID 1000 read/write access in the control panel."
elif [[ "$mount_type" == "volume" ]]; then
log "Repair the Docker volume ownership:"
printf ' cd %q\n' "$install_dir"
printf " %q compose --project-name %q run --rm --no-deps --user 0 --entrypoint /bin/sh edgeever -c 'chown -R 1000:1000 /data'\n" "$docker_command" "$project_name"
else
log "Inspect the Compose /data mount and grant UID/GID 1000:1000 read/write access."
fi
}
on_error() {
local exit_code=$?
local line_number="${BASH_LINENO[0]}"
@@ -350,6 +475,7 @@ on_error() {
if declare -p compose >/dev/null 2>&1; then
"${compose[@]}" ps -a || true
"${compose[@]}" logs --tail 80 edgeever || true
print_storage_diagnostics || true
fi
exit "$exit_code"
}
+5
View File
@@ -88,6 +88,11 @@ replacement is under `/data`:
The image runs as the non-root `bun` user (UID/GID `1000`). If a NAS requires
a host bind mount instead of the named volume, create the directory first and
grant UID/GID `1000` read/write access.
When an installation or automatic update fails, the script performs a real
write test against `/data` and reports the mount type, source, container user,
and directory state. If permissions are the cause, it prints a repair command
tailored to a Docker named volume or NAS bind mount; it never changes existing
data permissions automatically.
## Configuration
+3
View File
@@ -75,6 +75,9 @@ Compose 会创建一个命名卷。所有需要在容器替换后保留的数据
镜像以非 root 的 `bun` 用户运行(UID/GID 均为 `1000`)。如果 NAS 必须使用主机
目录绑定而不是命名卷,请先创建目录,并为 UID/GID `1000` 授予读写权限。
安装或自动更新失败时,脚本会对 `/data` 执行实际写入测试,输出挂载类型、来源、
容器用户和目录状态。确认是权限问题后,还会根据 Docker 命名卷或 NAS 目录绑定
给出对应的修复命令;脚本不会擅自修改现有数据的权限。
## 配置
+81
View File
@@ -192,6 +192,8 @@ fi
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 * * *");
@@ -299,4 +301,83 @@ exit 1
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 });
}
});
});