fix(docker): improve storage permission diagnostics
This commit is contained in:
1 parent
faa98131f6
commit
0532f38008
4 files changed
+215
No files matched your search
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ Compose 会创建一个命名卷。所有需要在容器替换后保留的数据
|
||||
|
||||
镜像以非 root 的 `bun` 用户运行(UID/GID 均为 `1000`)。如果 NAS 必须使用主机
|
||||
目录绑定而不是命名卷,请先创建目录,并为 UID/GID `1000` 授予读写权限。
|
||||
安装或自动更新失败时,脚本会对 `/data` 执行实际写入测试,输出挂载类型、来源、
|
||||
容器用户和目录状态。确认是权限问题后,还会根据 Docker 命名卷或 NAS 目录绑定
|
||||
给出对应的修复命令;脚本不会擅自修改现有数据的权限。
|
||||
|
||||
## 配置
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user