backup: add a mongo driver for live, consistent dumps
Rocket.Chat and Stoat are both MongoDB-backed, and the backup engine only understood postgres, mysql/mariadb and sqlite — so a live snapshot of either would have captured a torn data directory that may not even mount. Adds mongo as a fourth kind: mongodump --archive on the backup side, mongorestore --archive --drop on the restore side (idempotent, so the caller's retry loop works the same as it does for pg_dump --clean), and a ping-based readiness probe that also waits out a replica set electing its primary. Credentials are optional. The shared sh preamble sets them from MONGO_INITDB_ROOT_USERNAME/PASSWORD when present and passes nothing when not, built with 'set --' so a password containing spaces survives word splitting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1e9e042d41
commit
9084280ea8
@ -17,7 +17,7 @@
|
|||||||
# labels:
|
# labels:
|
||||||
# libreportal.backup.db: "<kind>:<container>:<datadir>:<path>"
|
# libreportal.backup.db: "<kind>:<container>:<datadir>:<path>"
|
||||||
#
|
#
|
||||||
# kind mysql | mariadb | postgres | sqlite
|
# kind mysql | mariadb | postgres | mongo | sqlite
|
||||||
# container service container_name to `docker exec` into (server engines)
|
# container service container_name to `docker exec` into (server engines)
|
||||||
# datadir app-dir-relative folder holding raw DB files, excluded on live
|
# datadir app-dir-relative folder holding raw DB files, excluded on live
|
||||||
# path app-dir-relative path to the sqlite file (sqlite only)
|
# path app-dir-relative path to the sqlite file (sqlite only)
|
||||||
@ -25,6 +25,7 @@
|
|||||||
# Examples:
|
# Examples:
|
||||||
# "mysql:nextcloud-db:db_data:" MariaDB/MySQL in nextcloud-db, raw db_data/ excluded
|
# "mysql:nextcloud-db:db_data:" MariaDB/MySQL in nextcloud-db, raw db_data/ excluded
|
||||||
# "postgres:mastodon-db:postgres_data:" Postgres in mastodon-db
|
# "postgres:mastodon-db:postgres_data:" Postgres in mastodon-db
|
||||||
|
# "mongo:rocketchat-db:mongo_data:" MongoDB in rocketchat-db
|
||||||
# "sqlite:::data/gitea.db" sqlite file at data/gitea.db
|
# "sqlite:::data/gitea.db" sqlite file at data/gitea.db
|
||||||
#
|
#
|
||||||
# An app with no database can still opt into live snapshots (its files are
|
# An app with no database can still opt into live snapshots (its files are
|
||||||
@ -130,10 +131,19 @@ _backupDbDumpName()
|
|||||||
local kind="$1" container="$2" path="$3"
|
local kind="$1" container="$2" path="$3"
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
sqlite) echo "sqlite-$(echo "$path" | tr '/' '_').sqlite.gz" ;;
|
sqlite) echo "sqlite-$(echo "$path" | tr '/' '_').sqlite.gz" ;;
|
||||||
|
# mongodump emits a binary archive, not SQL text — name it honestly so a
|
||||||
|
# human poking at .lp-backup/db doesn't try to `zcat | psql` it.
|
||||||
|
mongo) echo "db-${container}.archive.gz" ;;
|
||||||
*) echo "db-${container}.sql.gz" ;;
|
*) echo "db-${container}.sql.gz" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Positional-arg preamble for the mongo tools: sets "$@" to the credential flags
|
||||||
|
# when the container was started with root auth, and to nothing when it wasn't.
|
||||||
|
# Built with `set --` rather than a flat string so a password containing spaces
|
||||||
|
# or globbing characters survives word splitting intact.
|
||||||
|
_backup_mongo_auth_sh='if [ -n "${MONGO_INITDB_ROOT_USERNAME:-}" ]; then set -- -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin; else set --; fi;'
|
||||||
|
|
||||||
# Wait until a server database is genuinely ready for a load. On a fresh init
|
# Wait until a server database is genuinely ready for a load. On a fresh init
|
||||||
# (the restore case) the engine starts a throwaway temp server, runs its setup,
|
# (the restore case) the engine starts a throwaway temp server, runs its setup,
|
||||||
# then stops it and starts the real one — a simple ping passes against the temp
|
# then stops it and starts the real one — a simple ping passes against the temp
|
||||||
@ -149,6 +159,11 @@ _backupDbWaitReady()
|
|||||||
case "$kind" in
|
case "$kind" in
|
||||||
postgres)
|
postgres)
|
||||||
runFileOp docker exec "$container" sh -c 'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}" -tAc "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
|
runFileOp docker exec "$container" sh -c 'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}" -tAc "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
|
||||||
|
mongo)
|
||||||
|
# mongosh on 6.0+, the legacy mongo shell on older images. A
|
||||||
|
# ping that returns ok:1 means the node is past init AND (for a
|
||||||
|
# replica set, which Rocket.Chat requires) has a primary.
|
||||||
|
runFileOp docker exec "$container" sh -c "$_backup_mongo_auth_sh"' (mongosh "$@" --quiet --eval "db.adminCommand({ping:1}).ok" 2>/dev/null || mongo "$@" --quiet --eval "db.adminCommand({ping:1}).ok" 2>/dev/null) | grep -q 1' >/dev/null 2>&1 && good=1 ;;
|
||||||
*)
|
*)
|
||||||
runFileOp docker exec "$container" sh -c 'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; mariadb -uroot -p"$RP" -N -e "SELECT 1" 2>/dev/null || mysql -uroot -p"$RP" -N -e "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
|
runFileOp docker exec "$container" sh -c 'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; mariadb -uroot -p"$RP" -N -e "SELECT 1" 2>/dev/null || mysql -uroot -p"$RP" -N -e "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
|
||||||
esac
|
esac
|
||||||
@ -172,6 +187,12 @@ _backupDbImport()
|
|||||||
postgres)
|
postgres)
|
||||||
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
|
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
|
||||||
'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}"' >/dev/null 2>&1 ;;
|
'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}"' >/dev/null 2>&1 ;;
|
||||||
|
mongo)
|
||||||
|
# --drop replaces each collection as it is restored, which is what
|
||||||
|
# makes a re-run idempotent (the retry loop in the caller depends on
|
||||||
|
# that, exactly like pg_dump --clean --if-exists).
|
||||||
|
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
|
||||||
|
"$_backup_mongo_auth_sh"' mongorestore "$@" --archive --drop --quiet' >/dev/null 2>&1 ;;
|
||||||
*)
|
*)
|
||||||
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
|
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
|
||||||
'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; (mariadb -uroot -p"$RP" 2>/dev/null || mysql -uroot -p"$RP")' >/dev/null 2>&1 ;;
|
'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; (mariadb -uroot -p"$RP" 2>/dev/null || mysql -uroot -p"$RP")' >/dev/null 2>&1 ;;
|
||||||
@ -217,6 +238,18 @@ backupDbDump()
|
|||||||
isError "$kind dump failed ($container)"; rc=1
|
isError "$kind dump failed ($container)"; rc=1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
mongo)
|
||||||
|
isNotice "Dumping mongo ($container) — live, consistent"
|
||||||
|
# No --gzip on mongodump: the pipeline below already gzips, and
|
||||||
|
# compressing twice just burns CPU for nothing.
|
||||||
|
if runFileOp docker exec "$container" sh -c \
|
||||||
|
"$_backup_mongo_auth_sh"' mongodump "$@" --archive --quiet' \
|
||||||
|
2>/dev/null | gzip | runFileWrite "$dump"; then
|
||||||
|
isSuccessful "mongo dump written ($container)"
|
||||||
|
else
|
||||||
|
isError "mongo dump failed ($container)"; rc=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
sqlite)
|
sqlite)
|
||||||
isNotice "Dumping sqlite ($path) — live, consistent"
|
isNotice "Dumping sqlite ($path) — live, consistent"
|
||||||
local src="$app_dir/$path"
|
local src="$app_dir/$path"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user