fix(community): mount real host data dir into helper

Helper used DATA_DIR (in-container path) for bind-mount source.
Daemon created phantom /data on host, log + staging went there
instead of /opt/amnezia-admin-data. Resolve HOST_DATA_DIR via env
or docker inspect; place staging under DATA_DIR so single mount
exposes both log and install.sh to helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-15 02:30:02 +03:00
parent da925022cb
commit ab40786dee
3 changed files with 53 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "amnezia-admin", "name": "amnezia-admin",
"version": "1.2.9", "version": "1.2.10",
"private": true, "private": true,
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).", "description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
"license": "MIT", "license": "MIT",

View File

@@ -274,6 +274,7 @@ docker run -d --name "${CONTAINER_NAME}" --restart unless-stopped \
-p "${HOST_PORT}:3980" \ -p "${HOST_PORT}:3980" \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v "${DATA_DIR}:/data" \ -v "${DATA_DIR}:/data" \
-e "HOST_DATA_DIR=${DATA_DIR}" \
"${RUN_ENV[@]}" \ "${RUN_ENV[@]}" \
amnezia-admin:latest amnezia-admin:latest

View File

@@ -1,5 +1,5 @@
import express from "express"; import express from "express";
import { spawn } from "child_process"; import { spawn, execFileSync } from "child_process";
import crypto from "crypto"; import crypto from "crypto";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
@@ -147,6 +147,42 @@ const DATA_DIR = process.env.DATA_DIR || "/data";
const PW_FILE = path.join(DATA_DIR, "password.hash"); const PW_FILE = path.join(DATA_DIR, "password.hash");
const SECRET_FILE = path.join(DATA_DIR, "session.secret"); const SECRET_FILE = path.join(DATA_DIR, "session.secret");
/**
* Хостовый путь, смонтированный в контейнер панели как DATA_DIR.
* Нужен для одноразового docker-cli helper-а: контейнер панели монтирует
* `${HOST_DATA_DIR}:/mnt/data`, и оба контейнера видят один и тот же файл логов.
* Порядок поиска:
* 1) env HOST_DATA_DIR (передаётся install.sh при `docker run`);
* 2) docker inspect собственного контейнера (находим Source для Destination=DATA_DIR);
* 3) fallback /opt/amnezia-admin-data — типичный путь install.sh.
*/
function resolveHostDataDir() {
const explicit = process.env.HOST_DATA_DIR?.trim();
if (explicit) return explicit;
const selfId = (process.env.HOSTNAME || "").trim();
if (selfId) {
try {
const out = execFileSync(
"docker",
[
"inspect",
"--format",
`{{range .Mounts}}{{if eq .Destination "${DATA_DIR}"}}{{.Source}}{{end}}{{end}}`,
selfId,
],
{ stdio: ["ignore", "pipe", "ignore"], timeout: 5000 },
)
.toString()
.trim();
if (out) return out;
} catch {
//
}
}
return "/opt/amnezia-admin-data";
}
const HOST_DATA_DIR = resolveHostDataDir();
const SESSION_COOKIE = "amnezia_sess"; const SESSION_COOKIE = "amnezia_sess";
const SESSION_MS = 7 * 24 * 60 * 60 * 1000; const SESSION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -1495,7 +1531,13 @@ app.post("/api/community/run-private-install", requireAuth, async (req, res) =>
return res.status(400).json({ error: "Скачанный скрипт слишком большой — отказ." }); return res.status(400).json({ error: "Скачанный скрипт слишком большой — отказ." });
} }
tmpDir = fs.mkdtempSync(path.join("/tmp", "amnezia-priv-inst-")); const stageRoot = path.join(DATA_DIR, ".priv-install-tmp");
try {
fs.mkdirSync(stageRoot, { recursive: true });
} catch {
//
}
tmpDir = fs.mkdtempSync(path.join(stageRoot, "amnezia-priv-inst-"));
const scriptPath = path.join(tmpDir, "install.sh"); const scriptPath = path.join(tmpDir, "install.sh");
fs.writeFileSync(scriptPath, buf); fs.writeFileSync(scriptPath, buf);
fs.chmodSync(scriptPath, 0o700); fs.chmodSync(scriptPath, 0o700);
@@ -1563,7 +1605,9 @@ app.post("/api/community/run-private-install", requireAuth, async (req, res) =>
}; };
if (useDockerCliHelper) { if (useDockerCliHelper) {
const dataAbs = path.resolve(DATA_DIR); const dataHost = HOST_DATA_DIR;
const relStage = path.relative(DATA_DIR, tmpDir);
const stageInHelper = `/mnt/data/${relStage}`;
const prelude = COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO const prelude = COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO
? "sleep 2" ? "sleep 2"
: [ : [
@@ -1577,11 +1621,13 @@ app.post("/api/community/run-private-install", requireAuth, async (req, res) =>
exec >>"/mnt/data/community-install-last.log" 2>&1 exec >>"/mnt/data/community-install-last.log" 2>&1
set -eu set -eu
echo "→ helper (docker cli): prelude + bash/curl..." echo "→ helper (docker cli): prelude + bash/curl..."
echo " staging: ${stageInHelper} (host: ${tmpDir.replace(DATA_DIR, dataHost)})"
[ -f "${stageInHelper}/install.sh" ] || { echo "⚠ helper: нет install.sh в ${stageInHelper} (проверьте монтирование ${dataHost}:/mnt/data)"; ls -la "${stageInHelper}" 2>&1 || true; exit 125; }
${prelude} ${prelude}
${fetchTools ? `${fetchTools}\n` : ""}command -v bash >/dev/null 2>&1 || { echo "⚠ helper: нет bash после apk/apt — см. ошибки apk выше"; exit 126; } ${fetchTools ? `${fetchTools}\n` : ""}command -v bash >/dev/null 2>&1 || { echo "⚠ helper: нет bash после apk/apt — см. ошибки apk выше"; exit 126; }
command -v curl >/dev/null 2>&1 || { echo "⚠ helper: нет curl после apk/apt"; exit 126; } command -v curl >/dev/null 2>&1 || { echo "⚠ helper: нет curl после apk/apt"; exit 126; }
set +e set +e
( cd /mnt/stage && bash ./install.sh ) ( cd ${stageInHelper} && bash ./install.sh )
rv=$? rv=$?
set -e set -e
echo "" echo ""
@@ -1598,9 +1644,7 @@ exit "\${rv}"
"-v", "-v",
"/var/run/docker.sock:/var/run/docker.sock", "/var/run/docker.sock:/var/run/docker.sock",
`-v`, `-v`,
`${tmpDir}:/mnt/stage:ro`, `${dataHost}:/mnt/data`,
`-v`,
`${dataAbs}:/mnt/data`,
"-e", "-e",
"GITHUB_TOKEN", "GITHUB_TOKEN",
"-e", "-e",