| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- #!/usr/bin/env bash
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- # shellcheck source=wp13-common.sh
- source "${SCRIPT_DIR}/wp13-common.sh"
- usage() {
- cat <<'EOF'
- Usage: backup.sh --output-dir PATH --confirm-maintenance-window [options]
- Create a logical PostgreSQL export and a consistent cold snapshot of every
- Compose-managed volume. The project is restarted even if archiving fails.
- Options:
- --output-dir PATH New backup directory.
- --compose-file PATH Compose file.
- --env-file PATH Optional Compose environment file.
- --project-name NAME Source project (default: dataops-test).
- --confirm-maintenance-window Required acknowledgement of bounded downtime.
- --allow-degraded Permit backup when a container is not healthy.
- --timeout SECONDS Restart health timeout (default: 600).
- --help Show this help.
- EOF
- }
- ROOT="$(wp13_default_root)"
- OUTPUT_DIR=""
- COMPOSE_FILE=""
- ENV_FILE=""
- PROJECT_NAME="dataops-test"
- CONFIRM_MAINTENANCE=0
- ALLOW_DEGRADED=0
- TIMEOUT_SECONDS=600
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
- --compose-file) COMPOSE_FILE="$2"; shift 2 ;;
- --env-file) ENV_FILE="$2"; shift 2 ;;
- --project-name) PROJECT_NAME="$2"; shift 2 ;;
- --confirm-maintenance-window) CONFIRM_MAINTENANCE=1; shift ;;
- --allow-degraded) ALLOW_DEGRADED=1; shift ;;
- --timeout) TIMEOUT_SECONDS="$2"; shift 2 ;;
- --help|-h) usage; exit 0 ;;
- *) wp13_die "unknown argument: $1" ;;
- esac
- done
- [[ -n "${OUTPUT_DIR}" ]] || wp13_die "--output-dir is required"
- [[ "${CONFIRM_MAINTENANCE}" -eq 1 ]] \
- || wp13_die "--confirm-maintenance-window is required"
- COMPOSE_FILE="${COMPOSE_FILE:-${ROOT}/deploy/docker/docker-compose.yml}"
- [[ -f "${COMPOSE_FILE}" ]] || wp13_die "compose file not found: ${COMPOSE_FILE}"
- if [[ -e "${OUTPUT_DIR}" && -n "$(find "${OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
- wp13_die "backup destination must be empty: ${OUTPUT_DIR}"
- fi
- mkdir -p "${OUTPUT_DIR}/volumes"
- OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)"
- wp13_require_command docker
- [[ -n "$(wp13_project_running_ids "${PROJECT_NAME}")" ]] \
- || wp13_die "source project is not running: ${PROJECT_NAME}"
- if [[ "${ALLOW_DEGRADED}" -eq 0 ]]; then
- wp13_wait_for_project "${PROJECT_NAME}" 5
- fi
- STARTED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
- wp13_log "creating logical PostgreSQL export"
- wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
- exec -T postgres pg_dumpall -U dataops \
- >"${OUTPUT_DIR}/postgres-all.sql"
- wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
- exec -T backend alembic -c alembic.ini current \
- >"${OUTPUT_DIR}/alembic-current.txt"
- wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
- config --images | LC_ALL=C sort -u \
- >"${OUTPUT_DIR}/images.txt"
- VOLUME_NAMES="$(
- docker volume ls \
- --filter "label=com.docker.compose.project=${PROJECT_NAME}" \
- --format '{{.Name}}' \
- | LC_ALL=C sort
- )"
- [[ -n "${VOLUME_NAMES}" ]] || wp13_die "no Compose-managed volumes found"
- PROJECT_STOPPED=0
- restart_source() {
- if [[ "${PROJECT_STOPPED}" -eq 1 ]]; then
- wp13_log "restarting source project ${PROJECT_NAME}"
- if wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" start >/dev/null; then
- wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}" || true
- fi
- fi
- }
- trap restart_source EXIT
- wp13_log "stopping source project for consistent volume snapshots"
- wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" stop >/dev/null
- PROJECT_STOPPED=1
- {
- printf 'format=wp13-backup-v1\n'
- printf 'source_project=%s\n' "${PROJECT_NAME}"
- printf 'started_at_utc=%s\n' "${STARTED_AT}"
- printf 'compose_sha256=%s\n' "$(wp13_sha256_file "${COMPOSE_FILE}")"
- printf 'alembic_current=%s\n' "$(awk 'NF {print $1; exit}' "${OUTPUT_DIR}/alembic-current.txt")"
- } >"${OUTPUT_DIR}/backup-manifest.txt"
- while IFS= read -r volume_name; do
- [[ -n "${volume_name}" ]] || continue
- logical_name="$(
- docker volume inspect \
- -f '{{index .Labels "com.docker.compose.volume"}}' \
- "${volume_name}"
- )"
- [[ -n "${logical_name}" && "${logical_name}" != "<no value>" ]] \
- || wp13_die "volume is missing Compose logical label: ${volume_name}"
- archive_name="$(wp13_safe_archive_name "${logical_name}").tar.gz"
- wp13_log "archiving volume ${logical_name}"
- docker run --rm \
- -v "${volume_name}:/volume:ro" \
- -v "${OUTPUT_DIR}/volumes:/backup" \
- postgres:16-alpine \
- sh -c "cd /volume && tar -czf '/backup/${archive_name}' ."
- printf 'volume=%s|archive=volumes/%s|source_name=%s\n' \
- "${logical_name}" "${archive_name}" "${volume_name}" \
- >>"${OUTPUT_DIR}/backup-manifest.txt"
- done <<<"${VOLUME_NAMES}"
- wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" start >/dev/null
- PROJECT_STOPPED=0
- wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}"
- printf 'completed_at_utc=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
- >>"${OUTPUT_DIR}/backup-manifest.txt"
- wp13_write_checksums "${OUTPUT_DIR}"
- wp13_log "backup completed: ${OUTPUT_DIR}"
|