| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | instance=${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; } |
| 6 | if [[ $instance == evil-hedgedoc ]]; then |
| 7 | root=/srv/prod/evil-hedgedoc |
| 8 | else |
| 9 | root="/srv/staging/$instance" |
| 10 | fi |
| 11 | |
| 12 | source_host=${STUDIO_MIGRATION_SOURCE:-zenith} |
| 13 | target_host=${STUDIO_DEPLOY_HOST:-root@127.0.0.1} |
| 14 | target_port=${STUDIO_DEPLOY_PORT:-2222} |
| 15 | remote=(ssh -p "$target_port" "$target_host") |
| 16 | path="nomad/jobs/$instance/inputs/database" |
| 17 | handoff=${STUDIO_LEGACY_HANDOFF:-} |
| 18 | if [[ -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" |
| 28 | else |
| 29 | upload_host=$source_host |
| 30 | upload_rsh=ssh |
| 31 | fi |
| 32 | |
| 33 | if [[ $instance != evil-hedgedoc ]]; then |
| 34 | "${remote[@]}" "test -f /var/lib/studio/stages/$instance.json" |
| 35 | else |
| 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 |
| 56 | fi |
| 57 | "${remote[@]}" "test \$(findmnt -n -o FSTYPE --mountpoint $root) = zfs" |
| 58 | db_json=$("${remote[@]}" "NOMAD_TOKEN=\$(cat /var/lib/studio/nomad.token) nomad var get -out json $path") |
| 59 | read -r database owner < <(python3 -c 'import json,sys; d=json.load(sys.stdin)["Items"]; print(d["name"], d["username"])' <<<"$db_json") |
| 60 | if [[ $instance == evil-hedgedoc ]]; then |
| 61 | [[ $database == evil_hedgedoc && $owner == svc_evil_hedgedoc ]] || { echo 'Unexpected production database' >&2; exit 1; } |
| 62 | else |
| 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; } |
| 64 | fi |
| 65 | |
| 66 | alloc_json=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json postgres') |
| 67 | allocation=$(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") |
| 68 | container="app-$allocation" |
| 69 | podman='podman --url unix:///run/podman/podman.sock' |
| 70 | scratch=$(mktemp -d) |
| 71 | trap 'rm -rf "$scratch"' EXIT |
| 72 | rsync -a -e "$upload_rsh" "$upload_host:/mnt/storage1/apps/evil-infra/hedgedoc/" "$scratch/uploads/" |
| 73 | drift=$(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 | |
| 76 | if [[ $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; } |
| 84 | fi |
| 85 | |
| 86 | uploads="$root/hedgedoc/public/uploads" |
| 87 | rsync -a --delete -e "ssh -p $target_port" "$scratch/uploads/" "$target_host:$uploads/" |
| 88 | drift=$(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; } |
| 90 | uid=$("${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 | |
| 93 | backup="/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 | |
| 97 | if [[ -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" |
| 99 | else |
| 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" |
| 102 | fi |
| 103 | |
| 104 | counts_sql='SELECT (SELECT count(*) FROM "Notes"), (SELECT count(*) FROM "Users"), (SELECT count(*) FROM "Revisions"), (SELECT count(*) FROM "Authors");' |
| 105 | if [[ -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) |
| 107 | fi |
| 108 | target_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; } |
| 110 | echo "Verified HedgeDoc Notes, Users, Revisions, and Authors counts: $target_counts" |
| 111 | |
| 112 | if [[ $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" |
| 120 | else |
| 121 | python3 "$(dirname "$0")/deploy.py" stage evil-hedgedoc |
| 122 | echo "Preview database imported; previous preview dump: $backup" |
| 123 | fi |