diff --git a/ops/backup/dolibarr-backup.sh b/ops/backup/dolibarr-backup.sh index 5ebfbf0..f932cae 100755 --- a/ops/backup/dolibarr-backup.sh +++ b/ops/backup/dolibarr-backup.sh @@ -37,6 +37,29 @@ TMP_S3_SECRET="dolibarr-backup-s3-temp" log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } die() { printf '\033[1;31mABORT:\033[0m %s\n' "$*" >&2; exit 1; } +# --- garde de cluster ------------------------------------------------------- +# Ce script lit des secrets, crée des Jobs et peut RESTAURER une base. Lancé sur +# le mauvais contexte kubectl, il part sur l'infrastructure de quelqu'un d'autre. +# Le contexte courant d'une station de travail n'est pas une garantie : il suffit +# d'un `K config use-context` oublié. On l'épingle donc, et on vérifie une +# empreinte POSITIVE du homelab avant d'agir — mêmes garde-fous que +# ops/sandbox/sandbox-lifecycle.sh. +ERP_KUBE_CONTEXT="${ERP_KUBE_CONTEXT:-default}" +K() { kubectl --context "$ERP_KUBE_CONTEXT" "$@"; } + +assert_arcodange_cluster() { + kubectl config get-contexts -o name 2>/dev/null | grep -qx "$ERP_KUBE_CONTEXT" \ + || die "kube-context '$ERP_KUBE_CONTEXT' n'existe pas (définir ERP_KUBE_CONTEXT)" + for ns in erp erp-sandbox "$S3_SRC_NS"; do + K get ns "$ns" >/dev/null 2>&1 \ + || die "le contexte '$ERP_KUBE_CONTEXT' n'a pas de namespace '$ns' — refus de s'y exécuter. + Ce script lit des secrets et peut restaurer une base ; il ne doit viser que le homelab Arcodange. + Contexte courant : '$(kubectl config current-context 2>/dev/null)'. + Définir ERP_KUBE_CONTEXT sur le contexte du homelab et réessayer." + done + log "garde de cluster OK — contexte '$ERP_KUBE_CONTEXT' (namespaces erp/erp-sandbox/$S3_SRC_NS)" +} + CMD="${1:-}"; shift || true ENV="prod"; KEY=""; KIND=""; YES=0 while [[ $# -gt 0 ]]; do @@ -72,11 +95,11 @@ SH copy_s3_secret() { command -v python3 >/dev/null || die "python3 required to copy the S3 secret without exposing it" - kubectl get secret "$S3_SRC_SECRET" -n "$S3_SRC_NS" -o json \ + K get secret "$S3_SRC_SECRET" -n "$S3_SRC_NS" -o json \ | python3 -c "import json,sys; d=json.load(sys.stdin); d['metadata']={'name':'$TMP_S3_SECRET','namespace':'$NS'}; d.pop('status',None); d['data']={k:d['data'][k] for k in ('AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','AWS_ENDPOINTS')}; print(json.dumps(d))" \ - | kubectl apply -f - >/dev/null + | K apply -f - >/dev/null } -cleanup_secret() { kubectl delete secret "$TMP_S3_SECRET" -n "$NS" --ignore-not-found >/dev/null 2>&1 || true; } +cleanup_secret() { K delete secret "$TMP_S3_SECRET" -n "$NS" --ignore-not-found >/dev/null 2>&1 || true; } # b64-encode an in-container script (host vars already substituted by the caller) b64() { printf '%s' "$1" | base64 | tr -d '\n'; } @@ -87,8 +110,8 @@ run_backup() { copy_s3_secret log "Backup ${ENV}: DB=$DB PVC=$PVC -> s3://$BUCKET/$PREFIX/{db,docs}/" local B64; B64="$(b64 "$(cat "${SCRIPT_DIR}/../../chart/files/backup-job.sh")")" - kubectl delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true - kubectl apply -f - >/dev/null </dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 \ - || die "backup Job did not complete — kubectl logs -n $NS job/dolibarr-backup" - kubectl logs -n "$NS" job/dolibarr-backup | sed 's/^/ /' - kubectl delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + K wait --for=condition=complete job/dolibarr-backup -n "$NS" --timeout=300s >/dev/null 2>&1 \ + || die "backup Job did not complete — K logs -n $NS job/dolibarr-backup" + K logs -n "$NS" job/dolibarr-backup | sed 's/^/ /' + K delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true cleanup_secret; trap - EXIT log "Backup complete." } @@ -135,8 +158,8 @@ echo "db/:"; S3 ls "s3://$BUCKET/$PREFIX/db/" || echo " (empty)" echo "docs/:"; S3 ls "s3://$BUCKET/$PREFIX/docs/" || echo " (empty)" EOF )" - kubectl delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true - kubectl apply -f - >/dev/null </dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 || true - kubectl logs -n "$NS" job/dolibarr-backup-list 2>/dev/null | sed 's/^/ /' - kubectl delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + K wait --for=condition=complete job/dolibarr-backup-list -n "$NS" --timeout=180s >/dev/null 2>&1 || true + K logs -n "$NS" job/dolibarr-backup-list 2>/dev/null | sed 's/^/ /' + K delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true cleanup_secret; trap - EXIT } @@ -167,8 +190,8 @@ run_restore() { 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 + K scale deploy "$DEPLOY" -n "$NS" --replicas=0 >/dev/null 2>&1 || true + K 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 @@ -197,8 +220,8 @@ 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 || true + K 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 + if ! K wait --for=condition=complete job/dolibarr-restore -n "$NS" --timeout=300s >/dev/null 2>&1; then + K logs -n "$NS" job/dolibarr-restore 2>&1 | sed 's/^/ /' + K 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 + K logs -n "$NS" job/dolibarr-restore | sed 's/^/ /' + K 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 + K scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true cleanup_secret; trap - EXIT log "Restore complete." } +assert_arcodange_cluster + case "$CMD" in backup) run_backup ;; list) run_list ;; diff --git a/ops/sandbox/sandbox-lifecycle.sh b/ops/sandbox/sandbox-lifecycle.sh index 7761d24..a04e779 100755 --- a/ops/sandbox/sandbox-lifecycle.sh +++ b/ops/sandbox/sandbox-lifecycle.sh @@ -173,9 +173,117 @@ sync_documents() { log "Documents synced. (For a one-shot logo only, scope the tar to mycompany/logos.)" } +# --- mode vierge ------------------------------------------------------------- +# ÉTAT : INCOMPLET. La purge fonctionne, l'instance ne se reconstruit PAS. +# +# Vérifié le 2026-08-14 : après DROP OWNED et retrait de install.lock, Dolibarr +# sert une page de login sur un schéma à ZÉRO table et l'API répond « Module Api +# must be enabled ». L'entrypoint de l'image NE LANCE PAS d'installation +# automatique — retirer le verrou ne suffit pas. +# +# CE QU'IL MANQUE : un « golden empty », c'est-à-dire un pg_dump d'une instance +# Dolibarr fraîchement installée — schéma + données de référence (dictionnaires, +# pays, plan comptable) — sans aucune donnée métier. `blank` restaurerait ce dump +# au lieu de laisser la base vide, exactement comme `refresh-from-prod` restaure +# le dump de production. Seule la source change. +# +# En l'état, `blank` laisse le bac à sable INUTILISABLE : ne l'employer que suivi +# de `refresh-from-prod`, ou une fois le golden empty constitué. +# +# Vide la base du bac à sable SANS rien restaurer, pour qu'un rejeu d'exercice +# reparte de zéro. `refresh-from-prod` réimporterait précisément les défauts que +# le rejeu doit corriger : reconstruire suppose donc une base vide, pas iso-prod. +# +# CE QUI REND L'OPÉRATION SÛRE : elle est ANNULABLE. `refresh-from-prod` restaure +# un bac à sable iso-prod en trois minutes. On peut donc casser sans regret — et +# c'est la seule raison pour laquelle une commande qui détruit une base entière +# est acceptable ici. +# +# Elle ne vise QUE $SB_NS. Le namespace de production n'apparaît nulle part dans +# cette fonction, et la garde de cluster refuse tout contexte qui n'est pas le +# homelab. +blank_sandbox() { + [ "${1:-}" = "--yes" ] || die "blank détruit toutes les données du bac à sable — relancer avec --yes" + trap restore_state EXIT + + log "Pause du self-heal ArgoCD (sinon le scale-to-0 est repris en quelques secondes)" + set_selfheal false + + log "erp-sandbox à 0 réplique (accès exclusif à la base)" + K scale deploy erp-sandbox -n "$SB_NS" --replicas=0 >/dev/null + K wait --for=delete pod -l app.kubernetes.io/instance=erp-sandbox -n "$SB_NS" --timeout=120s >/dev/null 2>&1 || true + + log "Job de purge (DROP OWNED, aucune restauration)" + K delete job sandbox-blank -n "$SB_NS" --ignore-not-found >/dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 \ + || die "le Job de purge n'a pas abouti — voir : K logs -n $SB_NS job/sandbox-blank" + K logs -n "$SB_NS" job/sandbox-blank | sed 's/^/ /' + K delete job sandbox-blank -n "$SB_NS" --ignore-not-found >/dev/null 2>&1 || true + + # Le verrou d'installation vit sur le VOLUME DOCUMENTS, que la purge de la base + # ne touche pas. Tant qu'il est là, Dolibarr saute l'installeur et sert une page + # de login sur un schéma inexistant : l'instance a l'air vivante et ne l'est pas. + # Le retirer laisse l'entrypoint de l'image réinstaller un schéma neuf. + log "Retrait du verrou d'installation (sinon Dolibarr sert un login sur une base sans schéma)" + K scale deploy erp-sandbox -n "$SB_NS" --replicas=1 >/dev/null + K wait --for=condition=ready pod -l app.kubernetes.io/instance=erp-sandbox -n "$SB_NS" --timeout=180s >/dev/null 2>&1 || true + POD=$(sb_pod) + [ -n "$POD" ] && K exec -n "$SB_NS" "$POD" -- rm -f /var/www/documents/install.lock >/dev/null 2>&1 || true + + log "Redémarrage pour déclencher l'installation" + K rollout restart deploy erp-sandbox -n "$SB_NS" >/dev/null + K rollout status deploy erp-sandbox -n "$SB_NS" --timeout=300s >/dev/null 2>&1 || true + + log "Réarmement du self-heal ArgoCD" + set_selfheal true + + log "Base vidée et verrou retiré. Vérifier que le schéma se reconstruit :" + log " curl -s https://erp-sandbox.arcodange.lab/api/index.php/status" + log "Annulation à tout moment : $0 refresh-from-prod" +} + case "${1:-}" in refresh-from-prod) assert_arcodange_cluster; refresh_from_prod ;; + blank) assert_arcodange_cluster; shift; blank_sandbox "${1:-}" ;; sync-documents) assert_arcodange_cluster; sync_documents ;; refresh) assert_arcodange_cluster; refresh_from_prod; sync_documents ;; - *) echo "usage: $0 {refresh-from-prod|sync-documents|refresh}" >&2; exit 2 ;; + *) echo "usage: $0 {refresh-from-prod|sync-documents|refresh|blank --yes}" >&2; exit 2 ;; esac