feat: aligner le dashboard production sur recette

This commit is contained in:
Santinova Deploy
2026-09-05 02:23:30 +00:00
parent 79240dd914
commit e352c46fd2
101 changed files with 11870 additions and 196 deletions

150
ops/gitea-token-rotation.sh Normal file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly COMMAND="${1:-status}"
readonly FORCE="${2:-}"
readonly GITEA_CONTAINER="${GITEA_CONTAINER:-gitea}"
readonly GITEA_URL="${GITEA_URL:-https://git.recette.santinova-soft.org}"
readonly GITEA_USER="${GITEA_USERNAME:-manus-admin}"
readonly DASHBOARD_APP_DIR="${DASHBOARD_APP_DIR:-/opt/manus-deploy/apps/manus-dashboard}"
readonly DASHBOARD_ENV_FILE="${DASHBOARD_ENV_FILE:-${DASHBOARD_APP_DIR}/.env}"
readonly STATE_DIR="${GITEA_ROTATION_STATE_DIR:-/opt/manus-deploy/secrets/gitea-token-rotation}"
readonly STATUS_FILE="${GITEA_ROTATION_STATUS_FILE:-/var/log/manus-deploy/gitea-token-rotation.json}"
readonly PENDING_TOKEN_FILE="${STATE_DIR}/pending.token"
readonly PENDING_NAME_FILE="${STATE_DIR}/pending.name"
readonly CURRENT_NAME_FILE="${STATE_DIR}/current.name"
readonly LAST_ROTATION_FILE="${STATE_DIR}/last-rotation"
readonly LOCK_FILE="${STATE_DIR}/rotation.lock"
readonly APPROVAL_FILE="${STATE_DIR}/approval.request"
readonly TOKEN_PREFIX="santinova-recette"
readonly MAX_AGE_DAYS="90"
install -d -m 0700 "${STATE_DIR}"
install -d -m 0750 "$(dirname "${STATUS_FILE}")"
exec 9>"${LOCK_FILE}"
flock -n 9 || { echo "Une opération de rotation Gitea est déjà en cours." >&2; exit 1; }
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/[[:cntrl:]]/ /g'
}
write_status() {
local state="$1"
local message="$2"
local token_name="${3:-}"
local temporary
temporary="$(mktemp "${STATUS_FILE}.XXXXXX")"
chmod 0640 "${temporary}"
printf '{"state":"%s","message":"%s","tokenName":"%s","updatedAt":"%s"}\n' \
"$(json_escape "${state}")" \
"$(json_escape "${message}")" \
"$(json_escape "${token_name}")" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "${temporary}"
mv "${temporary}" "${STATUS_FILE}"
}
read_env_token() {
sed -n -E 's/^GITEA_TOKEN=(.*)$/\1/p' "${DASHBOARD_ENV_FILE}" | tail -n 1
}
validate_token() {
local token="$1"
local response status result=1
response="$(mktemp /tmp/gitea-token-validation.XXXXXX)"
if status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/user")" \
&& [[ "${status}" == "200" ]] \
&& grep -q '"login":"manus-admin"' "${response}" \
&& status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/repos/${GITEA_USER}/manus-dashboard")" \
&& [[ "${status}" == "200" ]] \
&& status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/repos/${GITEA_USER}/referentiel-ia")" \
&& [[ "${status}" == "200" ]]; then
result=0
fi
rm -f "${response}"
return "${result}"
}
rotation_due() {
[[ "${FORCE}" == "--force" ]] && return 0
[[ -f "${LAST_ROTATION_FILE}" ]] || return 0
local last_epoch now_epoch max_age_seconds
last_epoch="$(date -d "$(<"${LAST_ROTATION_FILE}")" +%s 2>/dev/null || echo 0)"
now_epoch="$(date +%s)"
max_age_seconds="$((MAX_AGE_DAYS * 86400))"
(( now_epoch - last_epoch >= max_age_seconds ))
}
prepare_rotation() {
if [[ -s "${PENDING_TOKEN_FILE}" && -s "${PENDING_NAME_FILE}" ]]; then
local existing_pending
existing_pending="$(<"${PENDING_TOKEN_FILE}")"
validate_token "${existing_pending}"
write_status "pending_approval" "Un jeton préparé et validé attend déjà une approbation manuelle." "$(<"${PENDING_NAME_FILE}")"
echo "GITEA_ROTATION_PENDING"
return 0
fi
if ! rotation_due; then
write_status "current" "La rotation trimestrielle n’est pas encore requise." "$(cat "${CURRENT_NAME_FILE}" 2>/dev/null || true)"
echo "GITEA_ROTATION_NOT_DUE"
return 0
fi
local current_token token_name pending_token
current_token="$(read_env_token)"
[[ -n "${current_token}" ]] || { write_status "failed" "Jeton actif absent du fichier .env serveur."; exit 1; }
validate_token "${current_token}" || { write_status "failed" "Le jeton actif ne passe pas les contrôles Gitea."; exit 1; }
token_name="${TOKEN_PREFIX}-$(date -u +%Y%m%d-%H%M%S)"
pending_token="$(docker exec --user git "${GITEA_CONTAINER}" gitea admin user generate-access-token \
--username "${GITEA_USER}" --token-name "${token_name}" --scopes all --raw)"
[[ -n "${pending_token}" ]] || { write_status "failed" "Gitea n’a pas généré de nouveau jeton." "${token_name}"; exit 1; }
printf '%s' "${pending_token}" > "${PENDING_TOKEN_FILE}"
printf '%s' "${token_name}" > "${PENDING_NAME_FILE}"
chmod 0600 "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
if ! validate_token "${pending_token}"; then
rm -f "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
write_status "failed" "Le nouveau jeton a été créé mais n’a pas passé les contrôles." "${token_name}"
exit 1
fi
write_status "pending_approval" "Nouveau jeton créé et validé. Approbation manuelle requise avant basculement." "${token_name}"
echo "GITEA_ROTATION_PREPARED"
}
apply_rotation() {
trap 'rm -f "${APPROVAL_FILE}"' EXIT
[[ -s "${PENDING_TOKEN_FILE}" && -s "${PENDING_NAME_FILE}" ]] || { write_status "failed" "Aucun jeton préparé à appliquer."; exit 1; }
local pending_token pending_name current_token backup
pending_token="$(<"${PENDING_TOKEN_FILE}")"
pending_name="$(<"${PENDING_NAME_FILE}")"
current_token="$(read_env_token)"
validate_token "${pending_token}" || { write_status "failed" "Le jeton en attente n’est plus valide." "${pending_name}"; exit 1; }
validate_token "${current_token}" || { write_status "failed" "Le jeton actif ne passe plus les contrôles préalables." "${pending_name}"; exit 1; }
backup="$(mktemp "${STATE_DIR}/dashboard.env.backup.XXXXXX")"
chmod 0600 "${backup}"
cp "${DASHBOARD_ENV_FILE}" "${backup}"
sed -i -E "s/^GITEA_TOKEN=.*/GITEA_TOKEN=${pending_token}/" "${DASHBOARD_ENV_FILE}"
if ! grep -q '^GITEA_TOKEN=' "${DASHBOARD_ENV_FILE}"; then
printf '\nGITEA_TOKEN=%s\n' "${pending_token}" >> "${DASHBOARD_ENV_FILE}"
fi
write_status "applying" "Basculement du dashboard vers le jeton validé." "${pending_name}"
if ! (cd "${DASHBOARD_APP_DIR}" && docker compose up -d --force-recreate dashboard >/dev/null && sleep 8 && validate_token "${pending_token}"); then
cp "${backup}" "${DASHBOARD_ENV_FILE}"
(cd "${DASHBOARD_APP_DIR}" && docker compose up -d --force-recreate dashboard >/dev/null) || true
rm -f "${backup}"
write_status "rollback" "Le basculement a échoué ; l’ancien jeton a été restauré." "${pending_name}"
exit 1
fi
rm -f "${backup}" "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
printf '%s' "${pending_name}" > "${CURRENT_NAME_FILE}"
date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_ROTATION_FILE}"
chmod 0600 "${CURRENT_NAME_FILE}" "${LAST_ROTATION_FILE}"
write_status "active_revoke_previous" "Basculement validé. Révoquer manuellement l’ancien jeton dans Gitea après le contrôle final." "${pending_name}"
echo "GITEA_ROTATION_APPLIED"
}
case "${COMMAND}" in
prepare) prepare_rotation ;;
apply) apply_rotation ;;
status)
if [[ -f "${STATUS_FILE}" ]]; then cat "${STATUS_FILE}"; else write_status "not_configured" "Aucune rotation n’a encore été préparée."; cat "${STATUS_FILE}"; fi
;;
*) echo "Usage: $0 {prepare [--force]|apply|status}" >&2; exit 2 ;;
esac