DC-050 harden dataDir + add image-layer migration
Three-part fix for the silent data-loss failure mode that survives DC-039:
If SERVICES_FILE env was unset, platformPaths.dataDir resolved to /etc/dashcaddy
(image-layer path), and audit/license/error logs would silently land there and
vanish on every container recreate.
1. platform-paths.assertSafe({mode:'production'}) — throws FATAL on forbidden
zones (/app/src,routes,scripts,utils,managers,security + /etc/* + /usr + /var).
Bypassed with SKIP_DATA_DIR_GUARD=1.
2. server.js calls assertSafe() before any runtime work.
3. start.sh one-time migration: scans 6 known image-layer zombie paths,
copies non-empty content to bind mount with 'migrated-' prefix,
gated by sentinel file. Survives set -e per-file failures.
19/19 platform-paths tests + 5/5 shell migration tests.
Suite: 1066/1067 (1 pre-existing public-routes-drift failure from in-flight
auth refactor, untouched by this commit).
Verified live on DNS2: live audit log at /app/data/audit-log.json (315KB,
active) is unaffected; vestigial 2-byte /app/src/security/audit-log.json +
140KB /app/src/utils/error.log (pre-DC-039 era) will be recovered on next
container recreate.
This commit is contained in:
@@ -14,6 +14,60 @@ HOST_IP="172.17.0.1"
|
||||
DNS_PRIMARY="100.121.150.22" # Technitium (Tailscale IP) — resolves *.sami
|
||||
DNS_FALLBACK="8.8.8.8"
|
||||
|
||||
# --- One-time migration from Docker image layer to bind mount --------------
|
||||
# DC-039 follow-up. Before v1.14.10, certain modules (audit-logger, license-
|
||||
# keygen, credential-manager) defaulted their files to /app/src/* via
|
||||
# path.join(__dirname, 'foo.json'). Those writes landed in the Docker image
|
||||
# layer and VANISHED on every container recreate. This step scans for any
|
||||
# non-empty zombie files left over from a previous image (where /opt/dashcaddy/
|
||||
# previously used /opt/dashcaddy/dashcaddy-api/src/... as the path root) and
|
||||
# copies their contents into the bind-mounted data dir ONCE.
|
||||
#
|
||||
# Idempotent: bails out if the migration sentinel file already exists.
|
||||
# Designed to be a no-op on every fresh install.
|
||||
MIGRATION_SENTINEL="${DATA_DIR}/.migrated-from-image-layer"
|
||||
IMAGE_LAYER_ZOMBIES=(
|
||||
"/opt/dashcaddy/dashcaddy-api/src/security/audit-log.json"
|
||||
"/opt/dashcaddy/dashcaddy-api/src/security/.encryption-key"
|
||||
"/opt/dashcaddy/dashcaddy-api/src/security/.encryption-key.bak"
|
||||
"/opt/dashcaddy/dashcaddy-api/src/utils/error.log"
|
||||
"/opt/dashcaddy/dashcaddy-api/src/managers/.license-secret"
|
||||
"/opt/dashcaddy/dashcaddy-api/src/managers/.license-counter"
|
||||
)
|
||||
# Note: set -e is active at top of script. Each per-file step uses an
|
||||
# explicit `|| true` (or guarded `if`) so a single unreadable zombie file
|
||||
# can't take down the whole container. The sentinel write at the end is
|
||||
# outside any conditional so it always runs once.
|
||||
run_image_layer_migration() {
|
||||
if [ -f "${MIGRATION_SENTINEL}" ]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${DATA_DIR}" || { echo "[start.sh] [migration] mkdir failed: ${DATA_DIR}"; return 0; }
|
||||
local migrated=0
|
||||
for src in "${IMAGE_LAYER_ZOMBIES[@]}"; do
|
||||
if [ -f "${src}" ] && [ -s "${src}" ]; then
|
||||
local dest_name dest
|
||||
dest_name="$(basename "${src}")"
|
||||
dest="${DATA_DIR}/migrated-${dest_name}"
|
||||
if [ ! -f "${dest}" ]; then
|
||||
echo "[start.sh] [migration] Recovering image-layer file: ${src} -> ${dest}"
|
||||
if cp -a "${src}" "${dest}" 2>/dev/null; then
|
||||
migrated=$((migrated + 1))
|
||||
else
|
||||
echo "[start.sh] [migration] WARN: failed to copy ${src} (continuing)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$migrated" -gt 0 ]; then
|
||||
echo "[start.sh] [migration] Recovered ${migrated} file(s) from image layer."
|
||||
echo "[start.sh] [migration] Review files prefixed 'migrated-' in ${DATA_DIR} and merge or delete."
|
||||
fi
|
||||
# Sentinel write MUST run regardless of any per-file failure above.
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "${MIGRATION_SENTINEL}" 2>/dev/null || echo "1" > "${MIGRATION_SENTINEL}"
|
||||
}
|
||||
run_image_layer_migration
|
||||
|
||||
# --- /etc/hosts overrides for the container ---------------------------------
|
||||
# The base image (node:20-alpine) has no entries for *.sami. We must inject
|
||||
# them via --add-host so health checks inside the container can resolve LAN
|
||||
|
||||
Reference in New Issue
Block a user