| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | source_host=${STUDIO_MIGRATION_SOURCE:-zenith} |
| 5 | root=/mnt/storage1/apps/studio-handoff |
| 6 | id=$(date -u +%Y%m%dT%H%M%SZ)-$(python3 -c 'import secrets; print(secrets.token_hex(3))') |
| 7 | pending=$root/.pending-$id |
| 8 | final=$root/$id |
| 9 | |
| 10 | ssh "$source_host" 'test "$(findmnt -n -o SOURCE --mountpoint /mnt/storage1/apps)" = storage1/apps; test "$(findmnt -n -o FSTYPE --mountpoint /mnt/storage1/apps)" = zfs' |
| 11 | ssh "$source_host" 'sudo -n docker info >/dev/null' |
| 12 | |
| 13 | check_stopped() { |
| 14 | [[ $(ssh "$source_host" "sudo -n docker inspect -f '{{.State.Running}}' evil-forgejo") == false ]] || { |
| 15 | echo 'Stop Zenith evil-forgejo before exporting the final database' >&2 |
| 16 | exit 1 |
| 17 | } |
| 18 | [[ $(ssh "$source_host" "sudo -n docker ps -a --filter label=com.docker.compose.service=evil-hedgedoc --format '{{.State}}'") == exited ]] || { |
| 19 | echo 'Stop Zenith evil-hedgedoc before exporting the final databases' >&2 |
| 20 | exit 1 |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | check_stopped |
| 25 | ssh "$source_host" 'sudo -n docker exec postgres pg_isready -U postgres >/dev/null' |
| 26 | ssh "$source_host" 'test -r /mnt/storage1/apps/home-infra/.env' |
| 27 | ssh "$source_host" "set -e; test ! -e '$final'; mkdir -m 700 -p '$root'; mkdir -m 700 '$pending'" |
| 28 | for database in evil-forgejo evil-hedgedoc; do |
| 29 | ssh "$source_host" "set -e; umask 077; sudo -n docker exec postgres pg_dump -U postgres -Fc --no-owner --no-acl '$database' > '$pending/$database.dump'; test -s '$pending/$database.dump'; sudo -n docker exec -i postgres pg_restore -l < '$pending/$database.dump' >/dev/null" |
| 30 | done |
| 31 | check_stopped |
| 32 | ssh "$source_host" python3 - "$pending" "$id" <<'PY' |
| 33 | import hashlib |
| 34 | import json |
| 35 | from pathlib import Path |
| 36 | import subprocess |
| 37 | import sys |
| 38 | |
| 39 | folder = Path(sys.argv[1]) |
| 40 | manifest = {"id": sys.argv[2], "databases": {}} |
| 41 | queries = { |
| 42 | "evil-forgejo": 'SELECT (SELECT count(*) FROM "user"), (SELECT count(*) FROM repository)', |
| 43 | "evil-hedgedoc": 'SELECT (SELECT count(*) FROM "Notes"), (SELECT count(*) FROM "Users"), (SELECT count(*) FROM "Revisions"), (SELECT count(*) FROM "Authors")', |
| 44 | } |
| 45 | for name in ("evil-forgejo", "evil-hedgedoc"): |
| 46 | source = folder / f"{name}.dump" |
| 47 | digest = hashlib.sha256() |
| 48 | with source.open("rb") as file: |
| 49 | for chunk in iter(lambda: file.read(1024 * 1024), b""): |
| 50 | digest.update(chunk) |
| 51 | result = subprocess.run(["sudo", "-n", "docker", "exec", "postgres", "psql", "-U", "postgres", "-d", name, |
| 52 | "-At", "-c", queries[name]], check=True, capture_output=True, text=True) |
| 53 | counts = [int(value) for value in result.stdout.strip().split("|")] |
| 54 | if len(counts) != (4 if name == "evil-hedgedoc" else 2): |
| 55 | raise ValueError(f"Unexpected count result for {name}") |
| 56 | manifest["databases"][name] = {"file": source.name, "bytes": source.stat().st_size, |
| 57 | "sha256": digest.hexdigest(), "counts": counts} |
| 58 | legacy_env = (Path("/mnt/storage1/apps/home-infra/.env")).read_text().splitlines() |
| 59 | forgejo_secrets = { |
| 60 | "lfs_jwt": "EVIL_FORGEJO_SERVER_LFS_JWT_SECRET", |
| 61 | "oauth_jwt": "EVIL_FORGEJO_OAUTH2_JWT_SECRET", |
| 62 | "security_key": "EVIL_FORGEJO_SECURITY_SECRET_KEY", |
| 63 | "internal_token": "EVIL_FORGEJO_SECURITY_INTERNAL_TOKEN", |
| 64 | "anubis_key": "ANUBIS_PRIVATE_KEY", |
| 65 | "mailer_address": "MAILER_ADDRESS", |
| 66 | "mailer_username": "MAILER_USERNAME", |
| 67 | "mailer_password": "MAILER_PASSWORD", |
| 68 | } |
| 69 | fingerprints = {} |
| 70 | for target, source in forgejo_secrets.items(): |
| 71 | values = [line.split("=", 1)[1] for line in legacy_env if line.startswith(source + "=")] |
| 72 | if len(values) != 1 or not values[0]: |
| 73 | raise ValueError(f"Legacy Forgejo secret is unavailable: {source}") |
| 74 | fingerprints[target] = hashlib.sha256(values[0].encode()).hexdigest() |
| 75 | manifest["databases"]["evil-forgejo"]["secretSha256"] = fingerprints |
| 76 | (folder / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") |
| 77 | PY |
| 78 | ssh "$source_host" "set -e; chmod 600 '$pending/manifest.json'; mv '$pending' '$final'; sync" |
| 79 | check_stopped |
| 80 | echo "Legacy PostgreSQL handoff: $final" |