163 lines
6.9 KiB
Bash
163 lines
6.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
# Проверка deploy/backup.sh без docker: синтетический инстанс во временном
|
|||
|
|
# каталоге, локальный offsite-приёмник (rsync) и поведение при недоступной
|
|||
|
|
# выгрузке. Запуск: make backup-test (AGENT.md 10.8, 11.3).
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/glchat-backup-test.XXXXXX")"
|
|||
|
|
trap 'rm -rf "$WORK"' EXIT
|
|||
|
|
|
|||
|
|
checks=0
|
|||
|
|
errors=0
|
|||
|
|
|
|||
|
|
ok() {
|
|||
|
|
checks=$((checks + 1))
|
|||
|
|
printf 'ok %s\n' "$*"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fail() {
|
|||
|
|
checks=$((checks + 1))
|
|||
|
|
errors=$((errors + 1))
|
|||
|
|
printf 'ОШИБКА %s\n' "$*" >&2
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
expect() { # expect <описание> <ожидаемое> <фактическое>
|
|||
|
|
if [ "$2" = "$3" ]; then
|
|||
|
|
ok "$1 = $3"
|
|||
|
|
else
|
|||
|
|
fail "$1: ожидалось '$2', получено '$3'"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# --- синтетический инстанс -------------------------------------------------
|
|||
|
|
|
|||
|
|
ETC="${WORK}/etc"
|
|||
|
|
DATA="${ETC}/data"
|
|||
|
|
mkdir -p "${DATA}/db" "${DATA}/files/attachments" "${ETC}/livekit"
|
|||
|
|
sqlite3 "${DATA}/db/glchat.db" <<'SQL'
|
|||
|
|
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
|
|||
|
|
CREATE TABLE messages (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
|
|||
|
|
INSERT INTO users (email) VALUES ('admin@example.test'), ('guest@example.test');
|
|||
|
|
INSERT INTO messages (body) VALUES ('привет'), ('как дела');
|
|||
|
|
SQL
|
|||
|
|
printf 'файл вложения' >"${DATA}/files/attachments/a.txt"
|
|||
|
|
printf 'service: glchat\n' >"${ETC}/compose.yaml"
|
|||
|
|
printf 'port: 7880\n' >"${ETC}/livekit/livekit.yaml"
|
|||
|
|
cat >"${ETC}/.env" <<'ENV'
|
|||
|
|
DOMAIN=gl.example.test
|
|||
|
|
APP_VERSION=v0.0.0-test
|
|||
|
|
PROFILE=standard
|
|||
|
|
BACKUP_KEEP_DAYS=30
|
|||
|
|
ENV
|
|||
|
|
|
|||
|
|
run_backup() { # run_backup <offsite-цель> [keep-days]
|
|||
|
|
BACKUP_OFFSITE_TARGET="$1" BACKUP_OFFSITE_KEEP_DAYS="${2:-}" \
|
|||
|
|
GLCHAT_ETC_DIR="$ETC" "${REPO_DIR}/deploy/backup.sh"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# --- 1. без offsite --------------------------------------------------------
|
|||
|
|
|
|||
|
|
log_out="$(run_backup "")"
|
|||
|
|
expect "локальный бэкап создан" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
case "$log_out" in
|
|||
|
|
*"offsite"*) fail "offsite упомянут, хотя цель не задана" ;;
|
|||
|
|
*) ok "без BACKUP_OFFSITE_TARGET выгрузки нет" ;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
# --- 2. offsite в локальный каталог (rsync) -------------------------------
|
|||
|
|
|
|||
|
|
OFFSITE="${WORK}/offsite"
|
|||
|
|
mkdir -p "$OFFSITE"
|
|||
|
|
run_backup "$OFFSITE" >/dev/null
|
|||
|
|
expect "offsite-копия доехала" "1" "$(find "$OFFSITE" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
local_newest="$(ls -t "${DATA}/backups"/glchat-*.tar.gz | head -1)"
|
|||
|
|
local_base="$(basename "$local_newest")"
|
|||
|
|
if [ -f "${OFFSITE}/${local_base}" ] && cmp -s "$local_newest" "${OFFSITE}/${local_base}"; then
|
|||
|
|
ok "offsite-копия ${local_base} побайтово совпадает с локальной"
|
|||
|
|
else
|
|||
|
|
fail "offsite-копия ${local_base} отсутствует или отличается"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# --- 3. недоступный приёмник: предупреждение, но не авария ----------------
|
|||
|
|
|
|||
|
|
before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
marker="${WORK}/marker"
|
|||
|
|
touch "$marker"
|
|||
|
|
if log_out="$(run_backup "no-such-host.invalid:/nonexistent" 2>&1)"; then
|
|||
|
|
ok "недоступный offsite не роняет бэкап (код 0)"
|
|||
|
|
else
|
|||
|
|
fail "недоступный offsite привёл к ошибке"
|
|||
|
|
fi
|
|||
|
|
case "$log_out" in
|
|||
|
|
*"offsite-копия не отправлена"*) ok "предупреждение о неудачной выгрузке есть" ;;
|
|||
|
|
*) fail "нет предупреждения о неудачной выгрузке" ;;
|
|||
|
|
esac
|
|||
|
|
after="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
expect "локальные бэкапы продолжают создаваться" "$((before + 1))" "$after"
|
|||
|
|
|
|||
|
|
# --- 4. нечисловой BACKUP_OFFSITE_KEEP_DAYS отключает ротацию -------------
|
|||
|
|
|
|||
|
|
if log_out="$(run_backup "$OFFSITE" "пять" 2>&1)"; then
|
|||
|
|
ok "нечисловая offsite-ротация не роняет бэкап"
|
|||
|
|
else
|
|||
|
|
fail "нечисловая offsite-ротация привела к ошибке"
|
|||
|
|
fi
|
|||
|
|
case "$log_out" in
|
|||
|
|
*"не число"*) ok "предупреждение о нечисловом значении есть" ;;
|
|||
|
|
*) fail "нет предупреждения о нечисловом значении" ;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
# --- 5. AGE_RECIPIENT: шифрование при наличии age, внятная ошибка без него --
|
|||
|
|
|
|||
|
|
# AGE_RECIPIENT дописываем во временную копию .env и возвращаем исходный файл:
|
|||
|
|
# дальше проверяется offsite через rclone, где шифрование только мешает.
|
|||
|
|
cp "${ETC}/.env" "${ETC}/env.keep"
|
|||
|
|
if command -v age >/dev/null 2>&1 && command -v age-keygen >/dev/null 2>&1; then
|
|||
|
|
recipient="$(age-keygen 2>/dev/null | awk '/public key:/ {print $4}')"
|
|||
|
|
printf 'AGE_RECIPIENT=%s\n' "$recipient" >>"${ETC}/.env"
|
|||
|
|
plain_before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
run_backup "" >/dev/null
|
|||
|
|
expect "с AGE_RECIPIENT появился .age-файл" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.age' | wc -l | tr -d ' ')"
|
|||
|
|
expect "новых незашифрованных архивов нет" "$plain_before" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
|||
|
|
else
|
|||
|
|
printf 'AGE_RECIPIENT=age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n' >>"${ETC}/.env"
|
|||
|
|
if run_backup "" >/dev/null 2>&1; then
|
|||
|
|
fail "без установленного age бэкап не должен проходить молча"
|
|||
|
|
else
|
|||
|
|
ok "без age установщик бэкапа падает с ошибкой"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
mv "${ETC}/env.keep" "${ETC}/.env"
|
|||
|
|
|
|||
|
|
# --- 6. rclone-приёмник: проверяем фактические команды rclone --------------
|
|||
|
|
|
|||
|
|
SHIM="${WORK}/bin"
|
|||
|
|
mkdir -p "$SHIM"
|
|||
|
|
cat >"${SHIM}/rclone" <<'SHIMEOF'
|
|||
|
|
#!/usr/bin/env bash
|
|||
|
|
printf 'rclone %s\n' "$*" >>"${RCLONE_LOG}"
|
|||
|
|
exit 0
|
|||
|
|
SHIMEOF
|
|||
|
|
chmod +x "${SHIM}/rclone"
|
|||
|
|
rclone_log="${WORK}/rclone.log"
|
|||
|
|
: >"$rclone_log"
|
|||
|
|
(
|
|||
|
|
export PATH="${SHIM}:${PATH}" RCLONE_LOG="$rclone_log"
|
|||
|
|
run_backup "rclone:remote:glchat" 7 >/dev/null
|
|||
|
|
)
|
|||
|
|
if grep -q "^rclone copy .*glchat-.*\.tar\.gz remote:glchat$" "$rclone_log"; then
|
|||
|
|
ok "rclone copy вызван с целью remote:glchat"
|
|||
|
|
else
|
|||
|
|
fail "rclone copy не вызван: $(cat "$rclone_log")"
|
|||
|
|
fi
|
|||
|
|
if grep -q "^rclone delete --min-age 7d remote:glchat$" "$rclone_log"; then
|
|||
|
|
ok "rclone delete --min-age 7d вызван для ротации"
|
|||
|
|
else
|
|||
|
|
fail "offsite-ротация не вызвана: $(cat "$rclone_log")"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
printf '\n=== бэкап/offsite: %d проверок, %d ошибок ===\n' "$checks" "$errors"
|
|||
|
|
[ "$errors" -eq 0 ]
|