Files
main/scripts/backup-yc-playground.sh

246 lines
10 KiB
Bash
Executable File

#!/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 <name> [--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 <name> [--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 <<SUMMARY
${GREEN}========================================${NC}
${GREEN} Backup Complete: ${NS}${NC}
${GREEN}========================================${NC}
Directory: ${BACKUP_DIR}
Size: $(du -sh "${BACKUP_DIR}" | cut -f1)
Downtime: ${DOWNTIME}
SUMMARY