feat(restore): mail-archiver restore smoke test

Borg-Extract der Data-Protection-Keys + pg_restore des 645M
mailarchiver-Dumps in isoliertes Test-Postgres + Container-Boot +
HTTP-Smoke. Wegwerf-DB-Connection und Auth-Password, kein produktiver
Stack-ENV, kein Authelia-ForwardAuth im Smoke.

Machbarkeit vorab verifiziert: Dump vorhanden, App-Image gepinnt,
Data-Protection-Keys im Borg, .NET-App hat kein shfs-chmod-Problem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:01:47 +02:00
parent 5ee4a158d6
commit a9b232195d
3 changed files with 219 additions and 0 deletions
@@ -0,0 +1,41 @@
services:
restoretest-mailarchiver-postgres:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-mailarchiver-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: mailarchiver
POSTGRES_DB: mailarchiver
POSTGRES_PASSWORD: restoretest-mailarchiver-db
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/mailarchiver/postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mailarchiver -d mailarchiver"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-mailarchiver:
image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
container_name: restoretest-mailarchiver
restart: "no"
depends_on:
restoretest-mailarchiver-postgres:
condition: service_healthy
environment:
TZ: Europe/Berlin
# Wegwerf-Connection-String fuer isolierten Test.
# Produktiver MAILARCHIVER_DB_CONNECTION ist Stack-ENV-only und wird
# hier bewusst NICHT verwendet.
ConnectionStrings__DefaultConnection: "Host=restoretest-mailarchiver-postgres;Database=mailarchiver;Username=mailarchiver;Password=restoretest-mailarchiver-db"
Authentication__Password: restoretest-mailarchiver-auth
ports:
- "127.0.0.1:15000:5000"
volumes:
- /mnt/user/backups/restore-lab/mailarchiver/data-protection-keys:/app/DataProtection-Keys
security_opt:
- no-new-privileges:true
@@ -0,0 +1,172 @@
#!/bin/bash
set -euo pipefail
# Mail-Archiver Restore Smoke Test
#
# Borg-Extract der Data-Protection-Keys + pg_restore des mailarchiver-Dumps
# in isoliertes Test-Postgres + Container-Boot + HTTP-Smoke.
#
# In Produktion nutzt Mail-Archiver die Shared PostgreSQL 18 — im Test
# bekommt er ein eigenes isoliertes Test-Postgres mit Wegwerf-Credentials.
# Authelia-ForwardAuth wird im Smoke nicht geprueft (kein Traefik, kein
# Auth-Middleware).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/mailarchiver"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mailarchiver-extract"
COMPOSE_FILE="$SCRIPT_DIR/mailarchiver-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/mailarchiver-$(date +%F).md"
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/postgresql17-mailarchiver.dump"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Mail-Archiver restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
Borg source: local/appdata/mailarchiver/data-protection-keys
Host dump: $DUMP_HOST_PATH (645M)
Test endpoint: 127.0.0.1:15000
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
require_path "$DUMP_HOST_PATH"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "mailarchiver" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/data-protection-keys" "$RESTORE_ROOT/postgres"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Data-Protection-Keys aus Borg
borg_extract "/restore/mailarchiver-extract" "local/appdata/mailarchiver/data-protection-keys"
if [ ! -d "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys" ]; then
echo "Mailarchiver data-protection-keys path missing in Borg archive" >&2
exit 1
fi
cp -a "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys/." "$RESTORE_ROOT/data-protection-keys/"
chmod -R a+rwX "$RESTORE_ROOT/data-protection-keys"
# Stufe 2: Test-Postgres + Dump
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver-postgres >/dev/null
until docker exec restoretest-mailarchiver-postgres pg_isready -U mailarchiver -d mailarchiver >/dev/null 2>&1; do
sleep 2
done
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-mailarchiver-postgres \
pg_restore -U mailarchiver -d mailarchiver --clean --if-exists --no-owner --no-privileges \
< "$DUMP_HOST_PATH" 2>/tmp/mailarchiver-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused" /tmp/mailarchiver-pg-restore.err; then
sleep 5
continue
fi
if grep -qiE "FATAL|PANIC" /tmp/mailarchiver-pg-restore.err; then
cat /tmp/mailarchiver-pg-restore.err >&2
exit 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/mailarchiver-pg-restore.err >&2
exit 1
fi
# Stufe 3: Container starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver >/dev/null
# Mailarchiver ist ein .NET-App, braucht ein paar Sekunden fuer DB-Migration.
# Smoke gegen den Root-Endpunkt — bei Authelia-geschuetztem Dienst liefert
# der Container selbst trotzdem einen HTTP-Response (302 oder 200).
http_status=""
for _ in $(seq 1 60); do
http_status="$(curl -s -o /tmp/mailarchiver-body.html -w '%{http_code}' \
-L http://127.0.0.1:15000/ || true)"
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "401" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ] && [ "$http_status" != "302" ] && [ "$http_status" != "401" ]; then
echo "Mailarchiver HTTP smoke failed: status=$http_status" >&2
docker logs --tail 80 restoretest-mailarchiver >&2 || true
exit 1
fi
# Tabellen-Count als Sanity
table_count="$(docker exec restoretest-mailarchiver-postgres \
psql -U mailarchiver -d mailarchiver -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Mail-Archiver Restore Test Report - $(date +%F)
- Service: \`mail-archiver\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers: \`restoretest-mailarchiver\`, \`restoretest-mailarchiver-postgres\`
- Test endpoint: \`http://127.0.0.1:15000/\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of data-protection-keys: \`ok\`
- Host dump copy (645M): \`ok\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status: \`$http_status\`
- Public table count in test DB: \`$table_count\`
## Notes
- Productive secrets (MAILARCHIVER_DB_CONNECTION, MAILARCHIVER_AUTH_PASSWORD) NOT used.
- Authelia ForwardAuth NOT tested (no Traefik in smoke).
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Mailarchiver restore test ok -> $REPORT_FILE"
+6
View File
@@ -58,6 +58,12 @@ case "$MODE" in
fi fi
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh" exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
;; ;;
mailarchiver)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh"
;;
mealie) mealie)
if [ "$WHATIF" = "--what-if" ]; then if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/mealie-restore-test.sh" --what-if exec "$SCRIPT_DIR/mealie-restore-test.sh" --what-if