feat(erp): manifeste d'exercice, rejeu à blanc, gardes manquantes, trunk réconcilié #92
@@ -37,6 +37,29 @@ TMP_S3_SECRET="dolibarr-backup-s3-temp"
|
|||||||
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||||
die() { printf '\033[1;31mABORT:\033[0m %s\n' "$*" >&2; exit 1; }
|
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
|
CMD="${1:-}"; shift || true
|
||||||
ENV="prod"; KEY=""; KIND=""; YES=0
|
ENV="prod"; KEY=""; KIND=""; YES=0
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -72,11 +95,11 @@ SH
|
|||||||
|
|
||||||
copy_s3_secret() {
|
copy_s3_secret() {
|
||||||
command -v python3 >/dev/null || die "python3 required to copy the S3 secret without exposing it"
|
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))" \
|
| 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-encode an in-container script (host vars already substituted by the caller)
|
||||||
b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
|
b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
|
||||||
@@ -87,8 +110,8 @@ run_backup() {
|
|||||||
copy_s3_secret
|
copy_s3_secret
|
||||||
log "Backup ${ENV}: DB=$DB PVC=$PVC -> s3://$BUCKET/$PREFIX/{db,docs}/"
|
log "Backup ${ENV}: DB=$DB PVC=$PVC -> s3://$BUCKET/$PREFIX/{db,docs}/"
|
||||||
local B64; B64="$(b64 "$(cat "${SCRIPT_DIR}/../../chart/files/backup-job.sh")")"
|
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
|
K delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
kubectl apply -f - >/dev/null <<EOF
|
K apply -f - >/dev/null <<EOF
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata: { name: dolibarr-backup, namespace: $NS }
|
metadata: { name: dolibarr-backup, namespace: $NS }
|
||||||
@@ -118,10 +141,10 @@ spec:
|
|||||||
command: ["/bin/sh","-c"]
|
command: ["/bin/sh","-c"]
|
||||||
args: ["echo $B64 | base64 -d | sh"]
|
args: ["echo $B64 | base64 -d | sh"]
|
||||||
EOF
|
EOF
|
||||||
kubectl wait --for=condition=complete job/dolibarr-backup -n "$NS" --timeout=300s >/dev/null 2>&1 \
|
K wait --for=condition=complete job/dolibarr-backup -n "$NS" --timeout=300s >/dev/null 2>&1 \
|
||||||
|| die "backup Job did not complete — kubectl logs -n $NS job/dolibarr-backup"
|
|| die "backup Job did not complete — K logs -n $NS job/dolibarr-backup"
|
||||||
kubectl logs -n "$NS" job/dolibarr-backup | sed 's/^/ /'
|
K logs -n "$NS" job/dolibarr-backup | sed 's/^/ /'
|
||||||
kubectl delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
K delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
cleanup_secret; trap - EXIT
|
cleanup_secret; trap - EXIT
|
||||||
log "Backup complete."
|
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)"
|
echo "docs/:"; S3 ls "s3://$BUCKET/$PREFIX/docs/" || echo " (empty)"
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
kubectl delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
K delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
kubectl apply -f - >/dev/null <<EOF
|
K apply -f - >/dev/null <<EOF
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata: { name: dolibarr-backup-list, namespace: $NS }
|
metadata: { name: dolibarr-backup-list, namespace: $NS }
|
||||||
@@ -153,9 +176,9 @@ spec:
|
|||||||
command: ["/bin/sh","-c"]
|
command: ["/bin/sh","-c"]
|
||||||
args: ["echo $(b64 "$SCRIPT") | base64 -d | sh"]
|
args: ["echo $(b64 "$SCRIPT") | base64 -d | sh"]
|
||||||
EOF
|
EOF
|
||||||
kubectl wait --for=condition=complete job/dolibarr-backup-list -n "$NS" --timeout=180s >/dev/null 2>&1 || true
|
K wait --for=condition=complete job/dolibarr-backup-list -n "$NS" --timeout=180s >/dev/null 2>&1 || true
|
||||||
kubectl logs -n "$NS" job/dolibarr-backup-list 2>/dev/null | sed 's/^/ /'
|
K 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 delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
cleanup_secret; trap - EXIT
|
cleanup_secret; trap - EXIT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +190,8 @@ run_restore() {
|
|||||||
copy_s3_secret
|
copy_s3_secret
|
||||||
local DEPLOY="$NS" # release/instance name == namespace (erp / erp-sandbox)
|
local DEPLOY="$NS" # release/instance name == namespace (erp / erp-sandbox)
|
||||||
log "Restore ${KIND} on '${ENV}' from ${KEY} (scaling ${DEPLOY} to 0)"
|
log "Restore ${KIND} on '${ENV}' from ${KEY} (scaling ${DEPLOY} to 0)"
|
||||||
kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=0 >/dev/null 2>&1 || true
|
K 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 wait --for=delete pod -l app.kubernetes.io/instance="$NS" -n "$NS" --timeout=120s >/dev/null 2>&1 || true
|
||||||
|
|
||||||
local SCRIPT VOLS="[]" MOUNTS="[]"
|
local SCRIPT VOLS="[]" MOUNTS="[]"
|
||||||
if [[ "$KIND" == "db" ]]; then
|
if [[ "$KIND" == "db" ]]; then
|
||||||
@@ -197,8 +220,8 @@ EOF
|
|||||||
)"
|
)"
|
||||||
fi
|
fi
|
||||||
local B64; B64="$(b64 "$SCRIPT")"
|
local B64; B64="$(b64 "$SCRIPT")"
|
||||||
kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
K delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
kubectl apply -f - >/dev/null <<EOF
|
K apply -f - >/dev/null <<EOF
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata: { name: dolibarr-restore, namespace: $NS }
|
metadata: { name: dolibarr-restore, namespace: $NS }
|
||||||
@@ -221,19 +244,21 @@ spec:
|
|||||||
command: ["/bin/sh","-c"]
|
command: ["/bin/sh","-c"]
|
||||||
args: ["echo $B64 | base64 -d | sh"]
|
args: ["echo $B64 | base64 -d | sh"]
|
||||||
EOF
|
EOF
|
||||||
if ! kubectl wait --for=condition=complete job/dolibarr-restore -n "$NS" --timeout=300s >/dev/null 2>&1; then
|
if ! K 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/^/ /'
|
K logs -n "$NS" job/dolibarr-restore 2>&1 | sed 's/^/ /'
|
||||||
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
|
||||||
die "restore Job did not complete"
|
die "restore Job did not complete"
|
||||||
fi
|
fi
|
||||||
kubectl logs -n "$NS" job/dolibarr-restore | sed 's/^/ /'
|
K logs -n "$NS" job/dolibarr-restore | sed 's/^/ /'
|
||||||
kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
K delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
log "Scaling ${DEPLOY} back to 1"
|
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
|
cleanup_secret; trap - EXIT
|
||||||
log "Restore complete."
|
log "Restore complete."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_arcodange_cluster
|
||||||
|
|
||||||
case "$CMD" in
|
case "$CMD" in
|
||||||
backup) run_backup ;;
|
backup) run_backup ;;
|
||||||
list) run_list ;;
|
list) run_list ;;
|
||||||
|
|||||||
@@ -173,9 +173,117 @@ sync_documents() {
|
|||||||
log "Documents synced. (For a one-shot logo only, scope the tar to mycompany/logos.)"
|
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 <<EOF
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata: { name: sandbox-blank, namespace: $SB_NS }
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
ttlSecondsAfterFinished: 900
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: blank
|
||||||
|
image: $PG_IMAGE
|
||||||
|
env:
|
||||||
|
- { name: SB_PGUSER, valueFrom: { secretKeyRef: { name: vso-db-credentials, key: username } } }
|
||||||
|
- { name: SB_PGPASSWORD, valueFrom: { secretKeyRef: { name: vso-db-credentials, key: password } } }
|
||||||
|
- { name: PGHOST, value: "$PGHOST" }
|
||||||
|
- { name: PGSSLMODE, value: "disable" }
|
||||||
|
command: ["/bin/sh","-c"]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
Q() { PGPASSWORD=\$SB_PGPASSWORD psql -h "\$PGHOST" -U "\$SB_PGUSER" -d $SB_DB -tAc "\$1"; }
|
||||||
|
# Ceinture ET bretelles : on refuse d'agir si la base courante n'est
|
||||||
|
# pas celle du bac à sable. Une purge sur la mauvaise base ne se
|
||||||
|
# rattrape pas par un refresh.
|
||||||
|
CUR=\$(Q 'select current_database()')
|
||||||
|
[ "\$CUR" = "$SB_DB" ] || { echo "ABORT: base courante '\$CUR' au lieu de $SB_DB"; exit 1; }
|
||||||
|
echo "cible=\$CUR role=$SB_ROLE"
|
||||||
|
AVANT=\$(Q "select count(*) from pg_tables where schemaname='public' and tablename like 'llx_%'")
|
||||||
|
echo "tables llx_ avant purge : \$AVANT"
|
||||||
|
PGPASSWORD=\$SB_PGPASSWORD psql -h "\$PGHOST" -U "\$SB_PGUSER" -d $SB_DB -v ON_ERROR_STOP=1 \\
|
||||||
|
-c "DROP OWNED BY $SB_ROLE CASCADE;"
|
||||||
|
APRES=\$(Q "select count(*) from pg_tables where schemaname='public' and tablename like 'llx_%'")
|
||||||
|
echo "tables llx_ après purge : \$APRES"
|
||||||
|
[ "\$APRES" -eq 0 ] || { echo "ABORT: \$APRES tables subsistent — purge incomplète"; exit 1; }
|
||||||
|
echo "DONE."
|
||||||
|
EOF
|
||||||
|
K wait --for=condition=complete job/sandbox-blank -n "$SB_NS" --timeout=180s >/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
|
case "${1:-}" in
|
||||||
refresh-from-prod) assert_arcodange_cluster; refresh_from_prod ;;
|
refresh-from-prod) assert_arcodange_cluster; refresh_from_prod ;;
|
||||||
|
blank) assert_arcodange_cluster; shift; blank_sandbox "${1:-}" ;;
|
||||||
sync-documents) assert_arcodange_cluster; sync_documents ;;
|
sync-documents) assert_arcodange_cluster; sync_documents ;;
|
||||||
refresh) assert_arcodange_cluster; refresh_from_prod; 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
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user