77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply checked migrations atomically from the trusted deployment runner."""
|
|
import argparse
|
|
import hashlib
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--container", default="endoleap-dev-postgres-1")
|
|
parser.add_argument("--database", default="foundry_auth")
|
|
parser.add_argument("--role", default="foundry")
|
|
args = parser.parse_args()
|
|
for value in (args.database, args.role):
|
|
if not re.fullmatch(r"[a-z_][a-z0-9_]{0,62}", value):
|
|
raise SystemExit("Invalid deployment database or role")
|
|
migrations = sorted((Path(__file__).resolve().parent.parent / "db").glob("[0-9]*.sql"))
|
|
if not migrations:
|
|
raise SystemExit("No migrations found")
|
|
|
|
script = [
|
|
"\\set ON_ERROR_STOP on",
|
|
"BEGIN;",
|
|
"SET LOCAL lock_timeout='30s';",
|
|
"SELECT pg_advisory_xact_lock(731920164);",
|
|
"CREATE TABLE IF NOT EXISTS foundry_schema_migrations "
|
|
"(name text PRIMARY KEY, checksum text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now());",
|
|
]
|
|
tables = set()
|
|
for index, path in enumerate(migrations):
|
|
source = path.read_text()
|
|
name = path.name
|
|
if not re.fullmatch(r"[0-9]+-[a-z0-9-]+\.sql", name):
|
|
raise SystemExit("Invalid migration name")
|
|
checksum = hashlib.sha256(source.encode()).hexdigest()
|
|
tables.update(re.findall(r"CREATE TABLE ([a-z_]+)", source))
|
|
# The first deployed version predates migration tracking.
|
|
if index == 0:
|
|
script.extend([
|
|
"SELECT bool_and(to_regclass('public.' || name) IS NOT NULL) AS baseline FROM "
|
|
"unnest(ARRAY['project_accounts','oauth_attempts','login_grants','project_sessions']) name \\gset",
|
|
"\\if :baseline",
|
|
f"INSERT INTO foundry_schema_migrations(name,checksum) VALUES('{name}','{checksum}') ON CONFLICT DO NOTHING;",
|
|
"\\endif",
|
|
])
|
|
script.extend([
|
|
f"SELECT EXISTS(SELECT 1 FROM foundry_schema_migrations WHERE name='{name}') AS applied, "
|
|
f"EXISTS(SELECT 1 FROM foundry_schema_migrations WHERE name='{name}' AND checksum='{checksum}') AS valid \\gset",
|
|
"\\if :applied",
|
|
"\\if :valid",
|
|
"\\else",
|
|
"\\echo Migration checksum mismatch",
|
|
"\\quit 1",
|
|
"\\endif",
|
|
"\\else",
|
|
])
|
|
source = source.strip()
|
|
if not source.startswith("BEGIN;") or not source.endswith("COMMIT;"):
|
|
raise SystemExit("Migration must have explicit transaction boundaries")
|
|
script.append(source[len("BEGIN;"):-len("COMMIT;")])
|
|
script.extend([
|
|
f"INSERT INTO foundry_schema_migrations(name,checksum) VALUES('{name}','{checksum}');",
|
|
"\\endif",
|
|
])
|
|
for table in sorted(tables):
|
|
script.append(f'GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE "{table}" TO "{args.role}";')
|
|
script.extend([
|
|
f'GRANT USAGE,SELECT ON SEQUENCE link_clicks_id_seq TO "{args.role}";',
|
|
"COMMIT;",
|
|
])
|
|
subprocess.run(
|
|
["docker", "exec", "-i", args.container, "sh", "-c",
|
|
'exec psql -X -U "${POSTGRES_USER:-postgres}" -d "$1" -v ON_ERROR_STOP=1', "sh", args.database],
|
|
input=("\n".join(script) + "\n").encode(), check=True,
|
|
)
|
|
print("Schema migrations and application grants verified.")
|