cisd/scripts/resource-restore/apply-resource-backup.sh
2026-04-29 16:45:52 +08:00

182 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
WORK_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--work-dir)
WORK_DIR="${2:-}"
shift 2
;;
*)
echo "[ERROR] unsupported argument: $1" >&2
exit 1
;;
esac
done
if [[ -z "${WORK_DIR}" ]]; then
echo "[ERROR] --work-dir is required" >&2
exit 1
fi
PAYLOAD_DIR="${WORK_DIR}/payload"
STATUS_FILE="${WORK_DIR}/status.json"
LOG_FILE="${WORK_DIR}/restore.log"
MANIFEST="${PAYLOAD_DIR}/payload-manifest.json"
FILES_PLAN="${WORK_DIR}/restore-files.tsv"
DATABASES_PLAN="${WORK_DIR}/restore-databases.tsv"
MQ_PLAN="${WORK_DIR}/restore-mq.tsv"
ENV_FILE="${WORK_DIR}/restore-env.sh"
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/ }"
printf '%s' "${value}"
}
write_status() {
local status="$1"
local phase="$2"
local message="$3"
printf '{"status":"%s","phase":"%s","message":"%s","updatedAt":"%s"}\n' \
"$(json_escape "${status}")" \
"$(json_escape "${phase}")" \
"$(json_escape "${message}")" \
"$(date '+%Y-%m-%dT%H:%M:%S%z')" > "${STATUS_FILE}"
}
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}"
}
fail() {
local message="$1"
local phase="${2:-FAILED}"
log "[ERROR] ${message}"
write_status "FAILED" "${phase}" "${message}"
exit 1
}
require_env() {
local name="$1"
local value="${!name:-}"
if [[ -z "${value}" ]]; then
fail "required environment variable ${name} is empty" "${2:-PRECHECK}"
fi
}
restore_files() {
[[ -f "${FILES_PLAN}" ]] || return 0
while IFS=$'\t' read -r entry_path restore_path || [[ -n "${entry_path:-}" ]]; do
[[ -n "${entry_path:-}" ]] || continue
[[ -n "${restore_path:-}" ]] || fail "restore file target is empty for ${entry_path}" "FILE"
local source_file="${PAYLOAD_DIR}/${entry_path}"
[[ -f "${source_file}" ]] || fail "restore source file not found: ${entry_path}" "FILE"
mkdir -p "$(dirname "${restore_path}")"
cp -f "${source_file}" "${restore_path}"
log "restored file: ${restore_path}"
done < "${FILES_PLAN}"
}
restore_databases() {
[[ -f "${DATABASES_PLAN}" ]] || return 0
[[ -s "${DATABASES_PLAN}" ]] || return 0
local script="${DB_RESTORE_SCRIPT_PATH:-}"
[[ -n "${script}" && -x "${script}" ]] || fail "DB_RESTORE_SCRIPT_PATH is not executable" "DATABASE"
require_env "DB_HOST" "DATABASE"
require_env "DB_PORT" "DATABASE"
require_env "DB_USER" "DATABASE"
local mysql_path="${MYSQL_PATH:-mysql}"
local db_password="${DB_PASSWORD:-}"
while IFS=$'\t' read -r entry_path db_name || [[ -n "${entry_path:-}" ]]; do
[[ -n "${entry_path:-}" ]] || continue
[[ -n "${db_name:-}" ]] || fail "database name is empty for ${entry_path}" "DATABASE"
local sql_file="${PAYLOAD_DIR}/${entry_path}"
[[ -f "${sql_file}" ]] || fail "sql file not found: ${entry_path}" "DATABASE"
"${script}" \
--mysql-path "${mysql_path}" \
--host "${DB_HOST}" \
--port "${DB_PORT}" \
--username "${DB_USER}" \
--password "${db_password}" \
--db-name "${db_name}" \
--sql-file "${sql_file}" >> "${LOG_FILE}" 2>&1
log "restored database: ${db_name}"
done < "${DATABASES_PLAN}"
}
replay_mq() {
[[ -f "${MQ_PLAN}" ]] || return 0
[[ -s "${MQ_PLAN}" ]] || return 0
local script="${MQ_REPLAY_SCRIPT_PATH:-}"
[[ -n "${script}" && -x "${script}" ]] || fail "MQ_REPLAY_SCRIPT_PATH is not executable" "MQ_REPLAY"
while IFS= read -r entry_path || [[ -n "${entry_path:-}" ]]; do
[[ -n "${entry_path:-}" ]] || continue
local context_file="${PAYLOAD_DIR}/${entry_path}"
[[ -f "${context_file}" ]] || fail "mq replay context not found: ${entry_path}" "MQ_REPLAY"
"${script}" --context-file "${context_file}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1
log "mq replay applied"
done < "${MQ_PLAN}"
}
health_check() {
local url="${HEALTH_CHECK_URL:-}"
[[ -n "${url}" ]] || {
log "health check skipped: HEALTH_CHECK_URL is empty"
return 0
}
command -v curl >/dev/null 2>&1 || fail "curl command not found for health check" "HEALTH_CHECK"
local timeout="${HEALTH_CHECK_TIMEOUT_SECONDS:-5}"
local response
response="$(curl -fsS --max-time "${timeout}" "${url}")" || fail "health check request failed" "HEALTH_CHECK"
if [[ "${response}" != *"UP"* ]]; then
fail "health check did not return UP" "HEALTH_CHECK"
fi
log "health check passed"
}
mkdir -p "${WORK_DIR}"
touch "${LOG_FILE}"
chmod 700 "${WORK_DIR}" || true
if [[ -f "${ENV_FILE}" ]]; then
# shellcheck disable=SC1090
source "${ENV_FILE}"
fi
[[ -f "${MANIFEST}" ]] || fail "payload manifest not found" "PRECHECK"
write_status "RUNNING" "SERVICE_STOP" "stopping services"
log "SERVICE_STOP"
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
"${TMS_SCRIPT_PATH}" stop >> "${LOG_FILE}" 2>&1 || true
fi
write_status "RUNNING" "FILE" "restoring files"
log "FILE"
restore_files
write_status "RUNNING" "DATABASE" "restoring databases"
log "DATABASE"
restore_databases
write_status "RUNNING" "MQ_REPLAY" "replaying mq configuration"
log "MQ_REPLAY"
replay_mq
write_status "RUNNING" "SERVICE_START" "starting services"
log "SERVICE_START"
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
"${TMS_SCRIPT_PATH}" start >> "${LOG_FILE}" 2>&1 || true
fi
write_status "RUNNING" "HEALTH_CHECK" "checking health"
log "HEALTH_CHECK"
health_check
write_status "SUCCESS" "DONE" "restore applied"
log "DONE"