1#!/usr/bin/env bash
2set -euo pipefail
3
4instance=${1:?usage: tools/import-hedgedoc.sh evil-hedgedoc[-preview-XXXXXXXX]}
5[[ $instance == evil-hedgedoc || $instance =~ ^evil-hedgedoc-preview-[0-9a-f]{8}$ ]] || { echo 'Expected a HedgeDoc service or preview ID' >&2; exit 1; }
6if [[ $instance == evil-hedgedoc ]]; then
7 root=/srv/prod/evil-hedgedoc
8else
9 root="/srv/staging/$instance"
10fi
11
12source_host=${STUDIO_MIGRATION_SOURCE:-zenith}
13target_host=${STUDIO_DEPLOY_HOST:-root@127.0.0.1}
14target_port=${STUDIO_DEPLOY_PORT:-2222}
15remote=(ssh -p "$target_port" "$target_host")
16path="nomad/jobs/$instance/inputs/database"
17handoff=${STUDIO_LEGACY_HANDOFF:-}
18if [[ -n $handoff ]]; then
19 [[ $instance == evil-hedgedoc && -n ${STUDIO_DEPLOY_HOST:-} ]] || {
20 echo 'Offline handoff requires production HedgeDoc and a target' >&2
21 exit 1
22 }
23 sh "$(dirname "$0")/check-legacy-handoff.sh" "$handoff" "$target_host" "$target_port"
24 entry=$("${remote[@]}" "python3 - '$handoff' evil-hedgedoc" < "$(dirname "$0")/verify-legacy-dump.py")
25 source_counts=$(python3 -c 'import json,sys; print("|".join(map(str,json.load(sys.stdin)["counts"])))' <<<"$entry")
26 upload_host=$target_host
27 upload_rsh="ssh -p $target_port"
28else
29 upload_host=$source_host
30 upload_rsh=ssh
31fi
32
33if [[ $instance != evil-hedgedoc ]]; then
34 "${remote[@]}" "test -f /var/lib/studio/stages/$instance.json"
35else
36 [[ -n ${STUDIO_DEPLOY_HOST:-} ]] || { echo 'Set STUDIO_DEPLOY_HOST to the production target' >&2; exit 1; }
37 if [[ -z $handoff ]]; then
38 source_state=$(ssh "$source_host" "sudo -n docker ps -a --filter label=com.docker.compose.service=evil-hedgedoc --format '{{.State}}'")
39 [[ $source_state == exited ]] || { echo 'Stop Zenith evil-hedgedoc before importing production data' >&2; exit 1; }
40 fi
41 job_response=$("${remote[@]}" 'curl -s -w "\n%{http_code}" -H "X-Nomad-Token: $(cat /var/lib/studio/nomad.token)" http://127.0.0.1:4646/v1/job/evil-hedgedoc')
42 job_status=${job_response##*$'\n'}
43 if [[ $job_status == 200 ]]; then
44 stopped=$(python3 -c 'import json,sys; print(str(json.load(sys.stdin)["Stop"]).lower())' <<<"${job_response%$'\n'*}")
45 [[ $stopped == true ]] || { echo 'Stop production HedgeDoc before importing' >&2; exit 1; }
46 for _ in {1..30}; do
47 running=$("${remote[@]}" 'curl -s -H "X-Nomad-Token: $(cat /var/lib/studio/nomad.token)" http://127.0.0.1:4646/v1/job/evil-hedgedoc/allocations' | python3 -c 'import json,sys; print(sum(x["ClientStatus"] == "running" for x in json.load(sys.stdin)))')
48 [[ $running == 0 ]] && break
49 sleep 2
50 done
51 [[ $running == 0 ]] || { echo 'Production HedgeDoc allocation did not stop' >&2; exit 1; }
52 elif [[ $job_status != 404 ]]; then
53 echo "Could not verify production job state: $job_status" >&2
54 exit 1
55 fi
56fi
57"${remote[@]}" "test \$(findmnt -n -o FSTYPE --mountpoint $root) = zfs"
58db_json=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad var get -out json $path")
59read -r database owner < <(python3 -c 'import json,sys; d=json.load(sys.stdin)["Items"]; print(d["name"], d["username"])' <<<"$db_json")
60if [[ $instance == evil-hedgedoc ]]; then
61 [[ $database == evil_hedgedoc && $owner == svc_evil_hedgedoc ]] || { echo 'Unexpected production database' >&2; exit 1; }
62else
63 [[ $database =~ ^evil_hedgedoc_s_[0-9a-f]{8}$ && $owner =~ ^svc_evil_hedgedoc_s_[0-9a-f]{8}$ ]] || { echo 'Unexpected preview database' >&2; exit 1; }
64fi
65
66alloc_json=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json postgres')
67allocation=$(python3 -c 'import json,sys; ids=[x["ID"] for x in json.load(sys.stdin) if x["ClientStatus"] == "running" and x["DesiredStatus"] == "run"]; assert len(ids) == 1; print(ids[0])' <<<"$alloc_json")
68container="app-$allocation"
69podman='podman --url unix:///run/podman/podman.sock'
70scratch=$(mktemp -d)
71trap 'rm -rf "$scratch"' EXIT
72rsync -a -e "$upload_rsh" "$upload_host:/mnt/storage1/apps/evil-infra/hedgedoc/" "$scratch/uploads/"
73drift=$(rsync -rnc --delete --out-format='%n' -e "$upload_rsh" "$upload_host:/mnt/storage1/apps/evil-infra/hedgedoc/" "$scratch/uploads/")
74[[ -z $drift ]] || { echo 'Source uploads changed during copy' >&2; exit 1; }
75
76if [[ $instance != evil-hedgedoc ]]; then
77 "${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job stop -yes $instance"
78 for _ in {1..30}; do
79 running=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad job allocs -json $instance" | python3 -c 'import json,sys; print(sum(x["ClientStatus"] == "running" for x in json.load(sys.stdin)))')
80 [[ $running == 0 ]] && break
81 sleep 2
82 done
83 [[ $running == 0 ]] || { echo 'Preview did not stop' >&2; exit 1; }
84fi
85
86uploads="$root/hedgedoc/public/uploads"
87rsync -a --delete -e "ssh -p $target_port" "$scratch/uploads/" "$target_host:$uploads/"
88drift=$(rsync -rnc --delete --out-format='%n' -e "ssh -p $target_port" "$scratch/uploads/" "$target_host:$uploads/")
89[[ -z $drift ]] || { echo 'Target uploads differ from source copy' >&2; exit 1; }
90uid=$("${remote[@]}" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"evil-hedgedoc\"])'")
91"${remote[@]}" "chown -R $uid:$uid $uploads; chmod 750 $uploads"
92
93backup="/var/lib/studio/$instance-before-import.dump"
94"${remote[@]}" "umask 077; $podman exec $container pg_dump -U postgres -Fc $database > $backup; test -s $backup"
95"${remote[@]}" "$podman exec $container psql -U postgres -d $database -v ON_ERROR_STOP=1 -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public AUTHORIZATION $owner;' >/dev/null"
96
97if [[ -n $handoff ]]; then
98 "${remote[@]}" "set -o pipefail; cat '$handoff/evil-hedgedoc.dump' | $podman exec -i $container pg_restore -U postgres -d $database --no-owner --no-acl --role=$owner"
99else
100 ssh "$source_host" 'sudo -n docker exec postgres pg_dump -U postgres -Fc evil-hedgedoc' |
101 "${remote[@]}" "$podman exec -i $container pg_restore -U postgres -d $database --no-owner --no-acl --role=$owner"
102fi
103
104counts_sql='SELECT (SELECT count(*) FROM "Notes"), (SELECT count(*) FROM "Users"), (SELECT count(*) FROM "Revisions"), (SELECT count(*) FROM "Authors");'
105if [[ -z $handoff ]]; then
106 source_counts=$(printf '%s\n' "$counts_sql" | ssh "$source_host" sudo -n docker exec -i postgres psql -U postgres -d evil-hedgedoc -At)
107fi
108target_counts=$(printf '%s\n' "$counts_sql" | "${remote[@]}" "$podman exec -i $container psql -U postgres -d $database -At")
109[[ $source_counts == "$target_counts" ]] || { echo 'Restored HedgeDoc record counts differ from Zenith' >&2; exit 1; }
110echo "Verified HedgeDoc Notes, Users, Revisions, and Authors counts: $target_counts"
111
112if [[ $instance == evil-hedgedoc ]]; then
113 if [[ -z $handoff ]]; then
114 [[ $(ssh "$source_host" "sudo -n docker ps -a --filter label=com.docker.compose.service=evil-hedgedoc --format '{{.State}}'") == exited ]] || {
115 echo 'Zenith evil-hedgedoc restarted during import; leave the home server stopped' >&2
116 exit 1
117 }
118 fi
119 echo "Production data imported; promote a tested HedgeDoc preview. Previous database: $backup"
120else
121 python3 "$(dirname "$0")/deploy.py" stage evil-hedgedoc
122 echo "Preview database imported; previous preview dump: $backup"
123fi