Merge pull request 'feat(backup): restore subcommand (db + documents), proven on the sandbox' (#34) from claude/dolibarr-backup-restore into main
This commit was merged in pull request #34.
This commit is contained in:
+13
-5
@@ -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
|
(`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).
|
sandbox (dump + tar + GCS upload + retention prune).
|
||||||
|
|
||||||
## Restore (manual, for now)
|
## Restore
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# DB: aws s3 cp s3://arcodange-backup/erp/<env>/db/<ts>.dump - | pg_restore -h <host> -U <user> -d <db> --clean
|
ops/backup/dolibarr-backup.sh list --env <e> # find the <ts> key
|
||||||
# docs: aws s3 cp s3://arcodange-backup/erp/<env>/docs/<ts>.tar.gz - | tar -C /var/www/documents -xzf -
|
ops/backup/dolibarr-backup.sh restore --db <ts>.dump --env <e> --yes
|
||||||
|
ops/backup/dolibarr-backup.sh restore --docs <ts>.tar.gz --env <e> --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 <owner_role> 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 `<ts>` 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)
|
## Automation — the CronJob (gated on creds)
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
case "$ENV" in
|
case "$ENV" in
|
||||||
prod) NS="erp"; DB="erp" ;;
|
prod) NS="erp"; DB="erp"; OWNER_ROLE="erp_role" ;;
|
||||||
sandbox) NS="erp-sandbox"; DB="erp-sandbox" ;;
|
sandbox) NS="erp-sandbox"; DB="erp-sandbox"; OWNER_ROLE="erp_sandbox_role" ;;
|
||||||
*) die "--env must be prod|sandbox" ;;
|
*) die "--env must be prod|sandbox" ;;
|
||||||
esac
|
esac
|
||||||
PVC="$NS"
|
PVC="$NS"
|
||||||
@@ -159,13 +159,88 @@ EOF
|
|||||||
cleanup_secret; trap - EXIT
|
cleanup_secret; trap - EXIT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# restore a half from a stored key. DESTRUCTIVE: scales the app to 0, replaces the
|
||||||
|
# DB (DROP OWNED BY <owner> 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 <<EOF
|
||||||
|
$PREAMBLE
|
||||||
|
S3 cp "s3://$BUCKET/$PREFIX/db/$KEY" /tmp/r.dump
|
||||||
|
echo "fetched \$(wc -c < /tmp/r.dump) bytes"
|
||||||
|
psql -h "$PGHOST" -U "\$PGUSER" -d "$DB" -v ON_ERROR_STOP=1 -c "DROP OWNED BY $OWNER_ROLE CASCADE;"
|
||||||
|
pg_restore -h "$PGHOST" -U "\$PGUSER" -d "$DB" --no-owner --role=$OWNER_ROLE /tmp/r.dump \\
|
||||||
|
&& echo "RESTORED db" || echo "restored db (ignorable warnings)"
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
else
|
||||||
|
VOLS="
|
||||||
|
- name: docs
|
||||||
|
persistentVolumeClaim: { claimName: ${PVC} }"
|
||||||
|
MOUNTS="
|
||||||
|
- { name: docs, mountPath: /docs }"
|
||||||
|
SCRIPT="$(cat <<EOF
|
||||||
|
$PREAMBLE
|
||||||
|
S3 cp "s3://$BUCKET/$PREFIX/docs/$KEY" /tmp/r.tgz
|
||||||
|
echo "fetched \$(wc -c < /tmp/r.tgz) bytes"
|
||||||
|
rm -rf /docs/* 2>/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 <<EOF
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata: { name: dolibarr-restore, namespace: $NS }
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
ttlSecondsAfterFinished: 600
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
volumes: ${VOLS}
|
||||||
|
containers:
|
||||||
|
- name: restore
|
||||||
|
image: $PG_IMAGE
|
||||||
|
envFrom:
|
||||||
|
- secretRef: { name: $TMP_S3_SECRET }
|
||||||
|
env:
|
||||||
|
- { name: PGUSER, valueFrom: { secretKeyRef: { name: vso-db-credentials, key: username } } }
|
||||||
|
- { name: PGPASSWORD, valueFrom: { secretKeyRef: { name: vso-db-credentials, key: password } } }
|
||||||
|
volumeMounts: ${MOUNTS}
|
||||||
|
command: ["/bin/sh","-c"]
|
||||||
|
args: ["echo $B64 | base64 -d | sh"]
|
||||||
|
EOF
|
||||||
|
if ! kubectl wait --for=condition=complete job/dolibarr-restore -n "$NS" --timeout=300s >/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
|
case "$CMD" in
|
||||||
backup) run_backup ;;
|
backup) run_backup ;;
|
||||||
list) run_list ;;
|
list) run_list ;;
|
||||||
restore)
|
restore)
|
||||||
[[ -n "$KEY" && -n "$KIND" ]] || die "restore needs --db <key> or --docs <key>"
|
[[ -n "$KEY" && -n "$KIND" ]] || die "restore needs --db <key> or --docs <key>"
|
||||||
[[ "$YES" == "1" ]] || die "restore is DESTRUCTIVE on '$ENV' — re-run with --yes"
|
[[ "$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 <key>] [--yes]" >&2; exit 2 ;;
|
*) echo "usage: $0 {backup|list|restore} [--env prod|sandbox] [--db|--docs <key>] [--yes]" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user