backup.sh 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. # shellcheck source=wp13-common.sh
  5. source "${SCRIPT_DIR}/wp13-common.sh"
  6. usage() {
  7. cat <<'EOF'
  8. Usage: backup.sh --output-dir PATH --confirm-maintenance-window [options]
  9. Create a logical PostgreSQL export and a consistent cold snapshot of every
  10. Compose-managed volume. The project is restarted even if archiving fails.
  11. Options:
  12. --output-dir PATH New backup directory.
  13. --compose-file PATH Compose file.
  14. --env-file PATH Optional Compose environment file.
  15. --project-name NAME Source project (default: dataops-test).
  16. --confirm-maintenance-window Required acknowledgement of bounded downtime.
  17. --allow-degraded Permit backup when a container is not healthy.
  18. --timeout SECONDS Restart health timeout (default: 600).
  19. --help Show this help.
  20. EOF
  21. }
  22. ROOT="$(wp13_default_root)"
  23. OUTPUT_DIR=""
  24. COMPOSE_FILE=""
  25. ENV_FILE=""
  26. PROJECT_NAME="dataops-test"
  27. CONFIRM_MAINTENANCE=0
  28. ALLOW_DEGRADED=0
  29. TIMEOUT_SECONDS=600
  30. while [[ $# -gt 0 ]]; do
  31. case "$1" in
  32. --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
  33. --compose-file) COMPOSE_FILE="$2"; shift 2 ;;
  34. --env-file) ENV_FILE="$2"; shift 2 ;;
  35. --project-name) PROJECT_NAME="$2"; shift 2 ;;
  36. --confirm-maintenance-window) CONFIRM_MAINTENANCE=1; shift ;;
  37. --allow-degraded) ALLOW_DEGRADED=1; shift ;;
  38. --timeout) TIMEOUT_SECONDS="$2"; shift 2 ;;
  39. --help|-h) usage; exit 0 ;;
  40. *) wp13_die "unknown argument: $1" ;;
  41. esac
  42. done
  43. [[ -n "${OUTPUT_DIR}" ]] || wp13_die "--output-dir is required"
  44. [[ "${CONFIRM_MAINTENANCE}" -eq 1 ]] \
  45. || wp13_die "--confirm-maintenance-window is required"
  46. COMPOSE_FILE="${COMPOSE_FILE:-${ROOT}/deploy/docker/docker-compose.yml}"
  47. [[ -f "${COMPOSE_FILE}" ]] || wp13_die "compose file not found: ${COMPOSE_FILE}"
  48. if [[ -e "${OUTPUT_DIR}" && -n "$(find "${OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
  49. wp13_die "backup destination must be empty: ${OUTPUT_DIR}"
  50. fi
  51. mkdir -p "${OUTPUT_DIR}/volumes"
  52. OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)"
  53. wp13_require_command docker
  54. [[ -n "$(wp13_project_running_ids "${PROJECT_NAME}")" ]] \
  55. || wp13_die "source project is not running: ${PROJECT_NAME}"
  56. if [[ "${ALLOW_DEGRADED}" -eq 0 ]]; then
  57. wp13_wait_for_project "${PROJECT_NAME}" 5
  58. fi
  59. STARTED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
  60. wp13_log "creating logical PostgreSQL export"
  61. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  62. exec -T postgres pg_dumpall -U dataops \
  63. >"${OUTPUT_DIR}/postgres-all.sql"
  64. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  65. exec -T backend alembic -c alembic.ini current \
  66. >"${OUTPUT_DIR}/alembic-current.txt"
  67. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  68. config --images | LC_ALL=C sort -u \
  69. >"${OUTPUT_DIR}/images.txt"
  70. VOLUME_NAMES="$(
  71. docker volume ls \
  72. --filter "label=com.docker.compose.project=${PROJECT_NAME}" \
  73. --format '{{.Name}}' \
  74. | LC_ALL=C sort
  75. )"
  76. [[ -n "${VOLUME_NAMES}" ]] || wp13_die "no Compose-managed volumes found"
  77. PROJECT_STOPPED=0
  78. restart_source() {
  79. if [[ "${PROJECT_STOPPED}" -eq 1 ]]; then
  80. wp13_log "restarting source project ${PROJECT_NAME}"
  81. if wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" start >/dev/null; then
  82. wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}" || true
  83. fi
  84. fi
  85. }
  86. trap restart_source EXIT
  87. wp13_log "stopping source project for consistent volume snapshots"
  88. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" stop >/dev/null
  89. PROJECT_STOPPED=1
  90. {
  91. printf 'format=wp13-backup-v1\n'
  92. printf 'source_project=%s\n' "${PROJECT_NAME}"
  93. printf 'started_at_utc=%s\n' "${STARTED_AT}"
  94. printf 'compose_sha256=%s\n' "$(wp13_sha256_file "${COMPOSE_FILE}")"
  95. printf 'alembic_current=%s\n' "$(awk 'NF {print $1; exit}' "${OUTPUT_DIR}/alembic-current.txt")"
  96. } >"${OUTPUT_DIR}/backup-manifest.txt"
  97. while IFS= read -r volume_name; do
  98. [[ -n "${volume_name}" ]] || continue
  99. logical_name="$(
  100. docker volume inspect \
  101. -f '{{index .Labels "com.docker.compose.volume"}}' \
  102. "${volume_name}"
  103. )"
  104. [[ -n "${logical_name}" && "${logical_name}" != "<no value>" ]] \
  105. || wp13_die "volume is missing Compose logical label: ${volume_name}"
  106. archive_name="$(wp13_safe_archive_name "${logical_name}").tar.gz"
  107. wp13_log "archiving volume ${logical_name}"
  108. docker run --rm \
  109. -v "${volume_name}:/volume:ro" \
  110. -v "${OUTPUT_DIR}/volumes:/backup" \
  111. postgres:16-alpine \
  112. sh -c "cd /volume && tar -czf '/backup/${archive_name}' ."
  113. printf 'volume=%s|archive=volumes/%s|source_name=%s\n' \
  114. "${logical_name}" "${archive_name}" "${volume_name}" \
  115. >>"${OUTPUT_DIR}/backup-manifest.txt"
  116. done <<<"${VOLUME_NAMES}"
  117. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" start >/dev/null
  118. PROJECT_STOPPED=0
  119. wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}"
  120. printf 'completed_at_utc=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
  121. >>"${OUTPUT_DIR}/backup-manifest.txt"
  122. wp13_write_checksums "${OUTPUT_DIR}"
  123. wp13_log "backup completed: ${OUTPUT_DIR}"