feat(restore): mealie restore test + freshness check negativ-test fix
Mealie-Restore-Test: Borg-Extract der App-Daten + pg_restore in isoliertes Test-Postgres + Mealie-Boot + HTTP /api/app/about Smoke. Machbarkeit vorab verifiziert (kein shfs-chmod-Problem, Mealie laeuft als root und switcht intern auf PUID 99). Freshness-Check: pg_header_ok() Docker-Fallback lieferte bei korruptem Dump return 2 (unchecked) statt return 1 (invalid). Negativ-Test am 2026-06-03 bewiesen: korrupter mealie.dump wird jetzt als DUMP_HEADER_INVALID erkannt (Critical, Exit 1). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
services:
|
||||
restoretest-mealie-postgres:
|
||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||
container_name: restoretest-mealie-postgres
|
||||
restart: "no"
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_DB: mealie
|
||||
POSTGRES_PASSWORD: restoretest-mealie-db
|
||||
PGDATA: /var/lib/postgresql/18/docker
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/mealie/postgres:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mealie -d mealie"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.19.2@sha256:f68e959bf66f4f458893ea58facac71690fe6f2ac7a31466b5cecb41b4e99c02
|
||||
container_name: restoretest-mealie
|
||||
restart: "no"
|
||||
depends_on:
|
||||
restoretest-mealie-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
ALLOW_SIGNUP: "false"
|
||||
PUID: "99"
|
||||
PGID: "100"
|
||||
DB_ENGINE: postgres
|
||||
POSTGRES_SERVER: restoretest-mealie-postgres
|
||||
POSTGRES_DB: mealie
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_PASSWORD: restoretest-mealie-db
|
||||
BASE_URL: http://127.0.0.1:19925
|
||||
ports:
|
||||
- "127.0.0.1:19925:9000"
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/mealie/data:/app/data
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Mealie Restore Smoke Test
|
||||
#
|
||||
# Borg-Extract der App-Daten + pg_restore des mealie.dump in isoliertes
|
||||
# Test-Postgres + Mealie-Boot + HTTP-Smoke.
|
||||
|
||||
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/mealie"
|
||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mealie-extract"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/mealie-compose.test.yml"
|
||||
REPORT_FILE="$REPORT_ROOT/mealie-$(date +%F).md"
|
||||
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/mealie.dump"
|
||||
|
||||
if [ "$WHATIF" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Mealie restore test
|
||||
Mode: WhatIf
|
||||
RestoreRoot: $RESTORE_ROOT
|
||||
Borg source: local/appdata/mealie/data
|
||||
Host dump: $DUMP_HOST_PATH
|
||||
Test endpoint: 127.0.0.1:19925
|
||||
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 "mealie" "$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" "$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: App-Daten aus Borg
|
||||
borg_extract "/restore/mealie-extract" "local/appdata/mealie/data"
|
||||
if [ ! -d "$EXTRACT_DIR/local/appdata/mealie/data" ]; then
|
||||
echo "Mealie data path missing in Borg archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp -a "$EXTRACT_DIR/local/appdata/mealie/data/." "$RESTORE_ROOT/data/"
|
||||
chmod -R a+rwX "$RESTORE_ROOT/data"
|
||||
|
||||
# Stufe 2: Test-Postgres hochfahren + Dump einspielen
|
||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie-postgres >/dev/null
|
||||
until docker exec restoretest-mealie-postgres pg_isready -U mealie -d mealie >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
restore_ok=0
|
||||
for attempt in $(seq 1 12); do
|
||||
if docker exec -i restoretest-mealie-postgres \
|
||||
pg_restore -U mealie -d mealie --clean --if-exists --no-owner --no-privileges \
|
||||
< "$DUMP_HOST_PATH" 2>/tmp/mealie-pg-restore.err; then
|
||||
restore_ok=1
|
||||
break
|
||||
fi
|
||||
if grep -qiE "starting up|shutting down|connection refused" /tmp/mealie-pg-restore.err; then
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
if grep -qiE "FATAL|PANIC" /tmp/mealie-pg-restore.err; then
|
||||
cat /tmp/mealie-pg-restore.err >&2
|
||||
exit 1
|
||||
fi
|
||||
restore_ok=1
|
||||
break
|
||||
done
|
||||
if [ "$restore_ok" -ne 1 ]; then
|
||||
cat /tmp/mealie-pg-restore.err >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stufe 3: Mealie starten
|
||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie >/dev/null
|
||||
|
||||
http_status=""
|
||||
for _ in $(seq 1 60); do
|
||||
http_status="$(curl -s -o /tmp/mealie-body.html -w '%{http_code}' \
|
||||
-L http://127.0.0.1:19925/api/app/about || true)"
|
||||
if [ "$http_status" = "200" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$http_status" != "200" ]; then
|
||||
echo "Mealie HTTP smoke failed: status=$http_status" >&2
|
||||
docker logs --tail 80 restoretest-mealie >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rezept-Count als Sanity-Check
|
||||
recipe_count="$(docker exec restoretest-mealie-postgres \
|
||||
psql -U mealie -d mealie -tAc \
|
||||
"SELECT count(*) FROM recipes;" 2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||
|
||||
write_report "$REPORT_FILE" <<EOF
|
||||
# Mealie Restore Test Report - $(date +%F)
|
||||
|
||||
- Service: \`mealie\`
|
||||
- Source repo: \`$repo\`
|
||||
- Archive: \`$archive\`
|
||||
- Restore root: \`$RESTORE_ROOT\`
|
||||
- Test containers: \`restoretest-mealie\`, \`restoretest-mealie-postgres\`
|
||||
- Test endpoint: \`http://127.0.0.1:19925/api/app/about\`
|
||||
- Result: \`SUCCESS\`
|
||||
|
||||
## Checks
|
||||
|
||||
- Borg extract of data: \`ok\`
|
||||
- Host dump copy: \`ok\`
|
||||
- Dump import into isolated Postgres: \`ok\`
|
||||
- HTTP status from /api/app/about: \`$http_status\`
|
||||
- Recipe count in test DB: \`$recipe_count\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Productive Mealie secrets were NOT mounted; test uses throwaway DB password.
|
||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||
EOF
|
||||
|
||||
RESTORE_SUCCESS=1
|
||||
echo "Mealie restore test ok -> $REPORT_FILE"
|
||||
@@ -58,6 +58,12 @@ case "$MODE" in
|
||||
fi
|
||||
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
|
||||
;;
|
||||
mealie)
|
||||
if [ "$WHATIF" = "--what-if" ]; then
|
||||
exec "$SCRIPT_DIR/mealie-restore-test.sh" --what-if
|
||||
fi
|
||||
exec "$SCRIPT_DIR/mealie-restore-test.sh"
|
||||
;;
|
||||
shared-pg-cluster)
|
||||
if [ "$WHATIF" = "--what-if" ]; then
|
||||
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh" --what-if
|
||||
|
||||
Reference in New Issue
Block a user