From a8b80f17e448d4f38e6e8ca1549de6db0c9688d9 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 30 Jun 2026 18:05:19 +0200 Subject: [PATCH] feat(backup): restore subcommand (db + documents), proven on the sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dolibarr-backup.sh restore --db|--docs --env --yes — the recovery half of the dedicated backup. DESTRUCTIVE (gated by --yes + explicit --env): scales the app to 0, then - --db: DROP OWNED BY CASCADE + pg_restore --no-owner --role (same mechanics as sandbox-lifecycle.sh), from s3://.../erp//db/.dump; - --docs: clears /var/www/documents and untars s3://.../erp//docs/.tar.gz; then scales the app back to 1. OWNER_ROLE per env (erp_role / erp_sandbox_role). The key is the bare filename from `list`; --db/--docs selects the subpath. Proven on the sandbox: backup → mutate MAIN_INFO_SOCIETE_NOM to a sentinel → restore --db → the value reverted to the backup's ('Arcodange'). (First run caught a path bug — the fetch missed the db/ subdir — now fixed.) Co-Authored-By: Claude Opus 4.7 (1M context) --- ops/backup/README.md | 18 +++++--- ops/backup/dolibarr-backup.sh | 81 +++++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/ops/backup/README.md b/ops/backup/README.md index ffc0931..cec5595 100644 --- a/ops/backup/README.md +++ b/ops/backup/README.md @@ -60,14 +60,22 @@ orchestrator and the scheduled CronJob (see "Automation" below). (`erp/prod/db/…` 1.2 MB, `erp/prod/docs/…` 12.5 MB). Proven end-to-end live on the sandbox (dump + tar + GCS upload + retention prune). -## Restore (manual, for now) +## Restore ```sh -# DB: aws s3 cp s3://arcodange-backup/erp//db/.dump - | pg_restore -h -U -d --clean -# docs: aws s3 cp s3://arcodange-backup/erp//docs/.tar.gz - | tar -C /var/www/documents -xzf - +ops/backup/dolibarr-backup.sh list --env # find the key +ops/backup/dolibarr-backup.sh restore --db .dump --env --yes +ops/backup/dolibarr-backup.sh restore --docs .tar.gz --env --yes ``` -The sandbox iso-prod refresh (`ops/sandbox/sandbox-lifecycle.sh`) is the natural -restore-drill bench. A `restore` subcommand is wired next. + +**DESTRUCTIVE** (requires `--yes` + an explicit `--env`): scales the app to 0, then +- **`--db`**: `DROP OWNED BY CASCADE` + `pg_restore --no-owner --role` + (same reset mechanics as `ops/sandbox/sandbox-lifecycle.sh`), then scales back; +- **`--docs`**: clears `/var/www/documents` and untars the archive, then scales back. + +The key is the bare `` filename from `list`; `--db`/`--docs` selects the +`db/` or `docs/` subpath. Proven on the sandbox: a mutated `MAIN_INFO_SOCIETE_NOM` +was reverted to the backup's value by `restore --db`. ## Automation — the CronJob (gated on creds) diff --git a/ops/backup/dolibarr-backup.sh b/ops/backup/dolibarr-backup.sh index 2fa33b9..5ebfbf0 100755 --- a/ops/backup/dolibarr-backup.sh +++ b/ops/backup/dolibarr-backup.sh @@ -50,8 +50,8 @@ while [[ $# -gt 0 ]]; do done case "$ENV" in - prod) NS="erp"; DB="erp" ;; - sandbox) NS="erp-sandbox"; DB="erp-sandbox" ;; + prod) NS="erp"; DB="erp"; OWNER_ROLE="erp_role" ;; + sandbox) NS="erp-sandbox"; DB="erp-sandbox"; OWNER_ROLE="erp_sandbox_role" ;; *) die "--env must be prod|sandbox" ;; esac PVC="$NS" @@ -159,13 +159,88 @@ EOF cleanup_secret; trap - EXIT } +# restore a half from a stored key. DESTRUCTIVE: scales the app to 0, replaces the +# DB (DROP OWNED BY CASCADE + pg_restore) or the documents (clear + untar), +# then scales back. Mirrors ops/sandbox/sandbox-lifecycle.sh's reset mechanics. +run_restore() { + trap cleanup_secret EXIT + copy_s3_secret + local DEPLOY="$NS" # release/instance name == namespace (erp / erp-sandbox) + log "Restore ${KIND} on '${ENV}' from ${KEY} (scaling ${DEPLOY} to 0)" + kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=0 >/dev/null 2>&1 || true + kubectl wait --for=delete pod -l app.kubernetes.io/instance="$NS" -n "$NS" --timeout=120s >/dev/null 2>&1 || true + + local SCRIPT VOLS="[]" MOUNTS="[]" + if [[ "$KIND" == "db" ]]; then + SCRIPT="$(cat </dev/null || true +tar -C /docs -xzf /tmp/r.tgz && echo "RESTORED docs" +EOF +)" + fi + local B64; B64="$(b64 "$SCRIPT")" + kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + kubectl apply -f - >/dev/null </dev/null 2>&1; then + kubectl logs -n "$NS" job/dolibarr-restore 2>&1 | sed 's/^/ /' + kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true + die "restore Job did not complete" + fi + kubectl logs -n "$NS" job/dolibarr-restore | sed 's/^/ /' + kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + log "Scaling ${DEPLOY} back to 1" + kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true + cleanup_secret; trap - EXIT + log "Restore complete." +} + case "$CMD" in backup) run_backup ;; list) run_list ;; restore) [[ -n "$KEY" && -n "$KIND" ]] || die "restore needs --db or --docs " [[ "$YES" == "1" ]] || die "restore is DESTRUCTIVE on '$ENV' — re-run with --yes" - die "restore: wired in the chart Job (next iteration) — key=$KEY kind=$KIND env=$ENV" + run_restore ;; *) echo "usage: $0 {backup|list|restore} [--env prod|sandbox] [--db|--docs ] [--yes]" >&2; exit 2 ;; esac -- 2.54.0