1#!/usr/bin/env bash
2set -euo pipefail
3
4: "${STUDIO_DEPLOY_HOST:?Set STUDIO_DEPLOY_HOST to the production target}"
5: "${STUDIO_LEGACY_HANDOFF:?Set STUDIO_LEGACY_HANDOFF to the offline PostgreSQL handoff}"
6
7target_port=${STUDIO_DEPLOY_PORT:-22}
8remote=(ssh -p "$target_port" "$STUDIO_DEPLOY_HOST")
9sh "$(dirname "$0")/check-legacy-handoff.sh" "$STUDIO_LEGACY_HANDOFF" "$STUDIO_DEPLOY_HOST" "$target_port"
10root=/srv/prod/evil-forgejo
11source_dir=/mnt/storage1/apps/evil-infra/forgejo
12database=evil_forgejo
13owner=svc_evil_forgejo
14podman='podman --url unix:///run/podman/podman.sock'
15
16entry=$("${remote[@]}" "python3 - '$STUDIO_LEGACY_HANDOFF' evil-forgejo" < "$(dirname "$0")/verify-legacy-dump.py")
17source_counts=$(python3 -c 'import json,sys; print("|".join(map(str,json.load(sys.stdin)["counts"])))' <<<"$entry")
18"${remote[@]}" "test \$(findmnt -n -o FSTYPE --mountpoint $root) = zfs; test -d $source_dir"
19
20response=$("${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-forgejo')
21[[ ${response##*$'\n'} == 200 ]] || { echo 'evil.inc Forgejo is unavailable on the home server' >&2; exit 1; }
22stopped=$(python3 -c 'import json,sys; print(str(json.load(sys.stdin)["Stop"]).lower())' <<<"${response%$'\n'*}")
23[[ $stopped == true ]] || { echo 'Stop evil.inc Forgejo on the home server before importing' >&2; exit 1; }
24for _ in {1..30}; do
25 running=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json evil-forgejo' |
26 python3 -c 'import json,sys; print(sum(a["ClientStatus"] == "running" for a in json.load(sys.stdin)))')
27 [[ $running == 0 ]] && break
28 sleep 2
29done
30[[ $running == 0 ]] || { echo 'evil.inc Forgejo did not stop on the home server' >&2; exit 1; }
31
32db_json=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad var get -out json nomad/jobs/evil-forgejo/inputs/database')
33read -r actual_database actual_owner < <(python3 -c 'import json,sys; d=json.load(sys.stdin)["Items"]; print(d["name"], d["username"])' <<<"$db_json")
34[[ $actual_database == "$database" && $actual_owner == "$owner" ]] || { echo 'Unexpected evil.inc Forgejo database on the home server' >&2; exit 1; }
35
36"${remote[@]}" "python3 - '$STUDIO_LEGACY_HANDOFF'" <<'PY'
37import hashlib
38import json
39import os
40from pathlib import Path
41import subprocess
42import sys
43
44legacy = json.loads((Path(sys.argv[1]) / "manifest.json").read_text())["databases"]["evil-forgejo"]["secretSha256"]
45environment = {**os.environ, "NOMAD_TOKEN": Path("/var/lib/studio/nomad.token").read_text().strip()}
46result = subprocess.run(["nomad", "var", "get", "-out", "json", "nomad/jobs/evil-forgejo"],
47 check=True, capture_output=True, text=True, env=environment)
48actual = json.loads(result.stdout)["Items"]
49for name, fingerprint in legacy.items():
50 if name not in actual or hashlib.sha256(actual[name].encode()).hexdigest() != fingerprint:
51 raise ValueError(f"Import the original evil.inc Forgejo secret before restoring data: {name}")
52PY
53
54allocation=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad job allocs -json postgres' |
55 python3 -c 'import json,sys; ids=[a["ID"] for a in json.load(sys.stdin) if a["ClientStatus"] == "running" and a["DesiredStatus"] == "run"]; assert len(ids) == 1; print(ids[0])')
56container=app-$allocation
57source_manifest=$("${remote[@]}" "python3 - '$source_dir'" < "$(dirname "$0")/tree-hash.py")
58source_bytes=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["bytes"])' <<<"$source_manifest")
59available=$("${remote[@]}" "df -B1 --output=avail $root | tail -n 1")
60(( available > source_bytes )) || { echo 'Too little free space for the evil.inc Forgejo app directory' >&2; exit 1; }
61
62dataset=$("${remote[@]}" "findmnt -n -o SOURCE --mountpoint $root")
63[[ $dataset == */prod/evil-forgejo ]] || { echo 'Unexpected production dataset' >&2; exit 1; }
64snapshot=$dataset@before-evil-forgejo-import-$(date +%s)-$$
65"${remote[@]}" "set -e; zfs snapshot '$snapshot'; mkdir -p '$root/data'; rsync -aH --numeric-ids --one-file-system --delete '$source_dir/' '$root/data/'"
66target_manifest=$("${remote[@]}" "python3 - '$root/data'" < "$(dirname "$0")/tree-hash.py")
67[[ $source_manifest == "$target_manifest" ]] || { echo "Forgejo app-directory copy differs; restore $snapshot" >&2; exit 1; }
68uid=$("${remote[@]}" "python3 -c 'import json; print(json.load(open(\"/var/lib/studio/identities.json\"))[\"evil-forgejo\"])'")
69"${remote[@]}" "chown -R $uid:$uid '$root/data'"
70
71backup=$root/before-import-$(date +%s)-$$.dump
72"${remote[@]}" "umask 077; $podman exec $container pg_dump -U postgres -Fc $database > '$backup'; test -s '$backup'"
73"${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"
74"${remote[@]}" "set -o pipefail; cat '$STUDIO_LEGACY_HANDOFF/evil-forgejo.dump' | $podman exec -i $container pg_restore -U postgres -d $database --no-owner --no-acl --role=$owner"
75counts=$("${remote[@]}" "$podman exec $container psql -U postgres -d $database -At -c 'SELECT (SELECT count(*) FROM \"user\"), (SELECT count(*) FROM repository)'")
76[[ $counts == "$source_counts" ]] || { echo "Forgejo database counts differ; restore $backup" >&2; exit 1; }
77echo "Imported evil.inc Forgejo app files and database; verified user/repository counts: $counts. Previous dataset: $snapshot. Previous database: $backup"