| 1 | #!/usr/bin/env bash |
| 2 | set -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 | |
| 7 | target_port=${STUDIO_DEPLOY_PORT:-22} |
| 8 | remote=(ssh -p "$target_port" "$STUDIO_DEPLOY_HOST") |
| 9 | sh "$(dirname "$0")/check-legacy-handoff.sh" "$STUDIO_LEGACY_HANDOFF" "$STUDIO_DEPLOY_HOST" "$target_port" |
| 10 | root=/srv/prod/evil-forgejo |
| 11 | source_dir=/mnt/storage1/apps/evil-infra/forgejo |
| 12 | database=evil_forgejo |
| 13 | owner=svc_evil_forgejo |
| 14 | podman='podman --url unix:///run/podman/podman.sock' |
| 15 | |
| 16 | entry=$("${remote[@]}" "python3 - '$STUDIO_LEGACY_HANDOFF' evil-forgejo" < "$(dirname "$0")/verify-legacy-dump.py") |
| 17 | source_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 | |
| 20 | 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-forgejo') |
| 21 | [[ ${response##*$'\n'} == 200 ]] || { echo 'evil.inc Forgejo is unavailable on the home server' >&2; exit 1; } |
| 22 | stopped=$(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; } |
| 24 | for _ 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 |
| 29 | done |
| 30 | [[ $running == 0 ]] || { echo 'evil.inc Forgejo did not stop on the home server' >&2; exit 1; } |
| 31 | |
| 32 | db_json=$("${remote[@]}" 'NOMAD_TOKEN=$(cat /var/lib/studio/nomad.token) nomad var get -out json nomad/jobs/evil-forgejo/inputs/database') |
| 33 | read -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' |
| 37 | import hashlib |
| 38 | import json |
| 39 | import os |
| 40 | from pathlib import Path |
| 41 | import subprocess |
| 42 | import sys |
| 43 | |
| 44 | legacy = json.loads((Path(sys.argv[1]) / "manifest.json").read_text())["databases"]["evil-forgejo"]["secretSha256"] |
| 45 | environment = {**os.environ, "NOMAD_TOKEN": Path("/var/lib/studio/nomad.token").read_text().strip()} |
| 46 | result = subprocess.run(["nomad", "var", "get", "-out", "json", "nomad/jobs/evil-forgejo"], |
| 47 | check=True, capture_output=True, text=True, env=environment) |
| 48 | actual = json.loads(result.stdout)["Items"] |
| 49 | for 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}") |
| 52 | PY |
| 53 | |
| 54 | allocation=$("${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])') |
| 56 | container=app-$allocation |
| 57 | source_manifest=$("${remote[@]}" "python3 - '$source_dir'" < "$(dirname "$0")/tree-hash.py") |
| 58 | source_bytes=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["bytes"])' <<<"$source_manifest") |
| 59 | available=$("${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 | |
| 62 | dataset=$("${remote[@]}" "findmnt -n -o SOURCE --mountpoint $root") |
| 63 | [[ $dataset == */prod/evil-forgejo ]] || { echo 'Unexpected production dataset' >&2; exit 1; } |
| 64 | snapshot=$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/'" |
| 66 | target_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; } |
| 68 | uid=$("${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 | |
| 71 | backup=$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" |
| 75 | counts=$("${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; } |
| 77 | echo "Imported evil.inc Forgejo app files and database; verified user/repository counts: $counts. Previous dataset: $snapshot. Previous database: $backup" |