#!/bin/bash set -euo pipefail # ================================================================ # Backup script for Matrix instances on yc-playground cluster # ================================================================ # Backs up the old chart (matrix-2.9.17) prod instances: # t0rt1k.tech, roglog.space, uretra.space # # Usage: ./backup-yc-playground.sh [--no-downtime] # Example: ./backup-yc-playground.sh t0rt1k # ./backup-yc-playground.sh t0rt1k --no-downtime (test, no scaling) # ================================================================ readonly YC_CONTEXT="yc-playground" readonly K="${KUBECTL:-kubectl} --context ${YC_CONTEXT}" readonly BACKUP_BASE="$(dirname "$(realpath "$0")")/../backups" readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[$(date +%H:%M:%S)]${NC} $*"; } warn() { echo -e "${YELLOW}[$(date +%H:%M:%S)] WARN:${NC} $*"; } err() { echo -e "${RED}[$(date +%H:%M:%S)] ERROR:${NC} $*"; } # ------------------------------------------------------------------- # Parse args # ------------------------------------------------------------------- DOWNTIME=true NAME="" for arg in "$@"; do case "$arg" in --no-downtime) DOWNTIME=false ;; *) NAME="$arg" ;; esac done if [ -z "${NAME}" ]; then echo "Usage: $0 [--no-downtime]" echo " name: t0rt1k | roglog | uretra" echo " --no-downtime: skip scale up/down (test run)" exit 1 fi readonly NS="matrix-${NAME}" readonly BACKUP_DIR="${BACKUP_BASE}/matrix-${NAME}-${TIMESTAMP}" # Synapse labels readonly SYNAPSE_LABEL="app.kubernetes.io/instance=chat,app.kubernetes.io/name=matrix" # MAS labels readonly MAS_LABEL="app=mas" readonly MAS_PG_LABEL="app=mas-postgresql" # ------------------------------------------------------------------- # Prerequisites # ------------------------------------------------------------------- log "=== Backing up ${NS} (downtime=${DOWNTIME}) ===" if ! ${K} get ns "${NS}" >/dev/null 2>&1; then err "Namespace ${NS} not found on yc-playground." exit 1 fi mkdir -p "${BACKUP_DIR}" log "Backup directory: ${BACKUP_DIR}" # ------------------------------------------------------------------- # Step 1: Backup Synapse media # ------------------------------------------------------------------- log "=== Step 1: Backing up Synapse media ===" SYNAPSE_POD=$(${K} -n "${NS}" get pods -l "${SYNAPSE_LABEL}" -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' 2>/dev/null) if [ -n "${SYNAPSE_POD}" ]; then log "Synapse pod: ${SYNAPSE_POD}" # Check if media directory exists if ${K} exec -n "${NS}" "${SYNAPSE_POD}" -- test -d /data/media_store 2>/dev/null; then ${K} exec -n "${NS}" "${SYNAPSE_POD}" -- tar czf /tmp/synapse-media.tar.gz -C /data media_store/ ${K} cp "${NS}/${SYNAPSE_POD}:/tmp/synapse-media.tar.gz" "${BACKUP_DIR}/synapse-media.tar.gz" 2>/dev/null || \ ${K} cp "${NS}/${SYNAPSE_POD}:tmp/synapse-media.tar.gz" "${BACKUP_DIR}/synapse-media.tar.gz" ${K} exec -n "${NS}" "${SYNAPSE_POD}" -- rm -f /tmp/synapse-media.tar.gz log "Media saved: $(du -h "${BACKUP_DIR}/synapse-media.tar.gz" | cut -f1)" else warn "/data/media_store not found in Synapse pod." fi else warn "No running Synapse pod found — skipping media backup." fi # ------------------------------------------------------------------- # Step 2: Stop Synapse + MAS # ------------------------------------------------------------------- if ${DOWNTIME}; then log "=== Step 2: Stopping Synapse + MAS ===" SYNAPSE_READY=$(${K} -n "${NS}" get deploy chat-matrix -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") MAS_READY=$(${K} -n "${NS}" get deploy mas -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") if [ "${SYNAPSE_READY}" != "0" ]; then ${K} -n "${NS}" scale deploy chat-matrix --replicas=0 log "Synapse scaled to 0." else log "Synapse already scaled to 0." fi if [ "${MAS_READY}" != "0" ]; then ${K} -n "${NS}" scale deploy mas --replicas=0 log "MAS scaled to 0." else log "MAS already scaled to 0." fi ${K} -n "${NS}" wait --for=delete pod -l "${SYNAPSE_LABEL}" --timeout=120s 2>/dev/null || warn "Synapse may still be terminating." ${K} -n "${NS}" wait --for=delete pod -l "${MAS_LABEL}" --timeout=120s 2>/dev/null || warn "MAS may still be terminating." log "Synapse + MAS stopped." else log "=== Step 2: Skipping downtime (--no-downtime) ===" fi # ------------------------------------------------------------------- # Step 3: Dump Synapse PostgreSQL # ------------------------------------------------------------------- log "=== Step 3: Dumping Synapse PostgreSQL ===" # Get the postgres password from the secret SYNAPSE_PG_PW=$(${K} -n "${NS}" get secret chat-postgresql -o jsonpath='{.data.postgres-password}' 2>/dev/null | base64 -d) if [ -z "${SYNAPSE_PG_PW}" ]; then SYNAPSE_PG_PW=$(${K} -n "${NS}" get secret chat-postgresql -o jsonpath='{.data.password}' 2>/dev/null | base64 -d) fi if [ -z "${SYNAPSE_PG_PW}" ]; then warn "Could not read chat-postgresql secret — trying env var from pod." SYNAPSE_PG_PW=$(${K} exec -n "${NS}" chat-postgresql-0 -c postgresql -- bash -c 'echo $POSTGRES_POSTGRES_PASSWORD' 2>/dev/null) fi ${K} exec -n "${NS}" chat-postgresql-0 -c postgresql -- bash -c "env PGPASSWORD='${SYNAPSE_PG_PW}' pg_dump -U postgres -d matrix -f /bitnami/postgresql/data/dump.sql" 2>&1 ${K} cp "${NS}/chat-postgresql-0:/bitnami/postgresql/data/dump.sql" "${BACKUP_DIR}/dump-synapse.sql" 2>/dev/null || true ${K} exec -n "${NS}" chat-postgresql-0 -c postgresql -- rm -f /bitnami/postgresql/data/dump.sql log "Synapse dump saved: ${BACKUP_DIR}/dump-synapse.sql ($(wc -c < "${BACKUP_DIR}/dump-synapse.sql") bytes)" # ------------------------------------------------------------------- # Step 4: Dump MAS PostgreSQL # ------------------------------------------------------------------- log "=== Step 4: Dumping MAS PostgreSQL ===" MAS_PG_POD=$(${K} -n "${NS}" get pods -l "${MAS_PG_LABEL}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) if [ -n "${MAS_PG_POD}" ]; then MAS_PG_PW=$(${K} exec -n "${NS}" "${MAS_PG_POD}" -c postgresql -- bash -c 'echo $POSTGRESQL_PASSWORD' 2>/dev/null) MAS_DB=$(${K} exec -n "${NS}" "${MAS_PG_POD}" -c postgresql -- bash -c 'echo $POSTGRESQL_DATABASE' 2>/dev/null) if [ -n "${MAS_PG_PW}" ]; then ${K} exec -n "${NS}" "${MAS_PG_POD}" -c postgresql -- bash -c "env PGPASSWORD='${MAS_PG_PW}' pg_dump -U '${MAS_DB:-mas}' -d '${MAS_DB:-mas}' -f /bitnami/postgresql/data/dump.sql" 2>&1 ${K} cp "${NS}/${MAS_PG_POD}:/bitnami/postgresql/data/dump.sql" "${BACKUP_DIR}/dump-mas.sql" 2>/dev/null || true ${K} exec -n "${NS}" "${MAS_PG_POD}" -c postgresql -- rm -f /bitnami/postgresql/data/dump.sql log "MAS dump saved: ${BACKUP_DIR}/dump-mas.sql ($(wc -c < "${BACKUP_DIR}/dump-mas.sql") bytes)" else warn "Could not read MAS PG password — skipping MAS dump." fi else warn "No MAS PG pod found — skipping MAS dump." fi # ------------------------------------------------------------------- # Step 5: Export secrets # ------------------------------------------------------------------- log "=== Step 5: Exporting secrets ===" for secret in chat-matrix chat-postgresql mas matrixrtc-livekit; do if ${K} -n "${NS}" get secret "${secret}" >/dev/null 2>&1; then ${K} -n "${NS}" get secret "${secret}" -o yaml > "${BACKUP_DIR}/secret-${secret}.yaml" log "Secret saved: secret-${secret}.yaml" else warn "Secret '${secret}' not found." fi done # ------------------------------------------------------------------- # Step 6: Restart Synapse + MAS # ------------------------------------------------------------------- if ${DOWNTIME}; then log "=== Step 6: Restarting Synapse + MAS ===" ${K} -n "${NS}" scale deploy chat-matrix --replicas=1 2>/dev/null || warn "Could not scale Synapse." ${K} -n "${NS}" scale deploy mas --replicas=1 2>/dev/null || warn "Could not scale MAS." log "Synapse + MAS restarted." else log "=== Step 6: Skipped (--no-downtime) ===" fi # ------------------------------------------------------------------- # Step 7: Write README # ------------------------------------------------------------------- log "=== Step 7: Writing README ===" cat > "${BACKUP_DIR}/README.txt" << READEOF Backup for ${NS} — ${TIMESTAMP} ================================== Instance: ${NAME} (namespace: ${NS}) Chart: matrix-2.9.17 (old chart, NOT ESS) Dump PG: Synapse (chat-postgresql) + MAS (mas-postgresql) Files: dump-synapse.sql Synapse PostgreSQL dump (pg_dump -U postgres -d matrix) dump-mas.sql MAS PostgreSQL dump (pg_dump -U mas -d mas) synapse-media.tar.gz Synapse media files (/data/media_store/) secret-chat-matrix.yaml Synapse secrets (signing.key, macaroon, etc.) secret-chat-postgresql.yaml PG passwords secret-mas.yaml MAS secrets (encryption-key, signing-key, shared-secret) secret-matrixrtc-livekit.yaml LiveKit secrets CRITICAL for restore: - secret-chat-matrix.yaml (SYNAPSE_SIGNING_KEY for federation identity) - secret-mas.yaml (MAS encryption-key for user sessions) - dump-synapse.sql (all user data, rooms, messages) - dump-mas.sql (MAS user auth data) Migration to ESS chart notes: - The old chart uses separate Helm releases per component (chat, element-call, livekit). - ESS bundles everything into the matrix-stack chart. - PostgreSQL is external (CNPG) on the new cluster. - MAS keys must be restored EXACTLY as-is for user auth to work. - The Synapse signing.key MUST match the federation identity. READEOF log "README saved." # ------------------------------------------------------------------- # Summary # ------------------------------------------------------------------- cat <