129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
#!/usr/bin/env python
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src"
|
|
|
|
DEFAULT_MODULE_SETS: tuple[tuple[str, str], ...] = (
|
|
("core-only", "tenancy,access,admin"),
|
|
("files-only", "tenancy,access,admin,files"),
|
|
("mail-only", "tenancy,access,admin,mail"),
|
|
("campaign-only", "tenancy,access,admin,campaigns"),
|
|
("campaign-with-files", "tenancy,access,admin,campaigns,files"),
|
|
("campaign-with-mail", "tenancy,access,admin,campaigns,mail"),
|
|
("full-product", "tenancy,access,admin,policy,audit,campaigns,files,mail,calendar,docs,ops"),
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run GovOPlaN migrations and startup smoke checks against a disposable PostgreSQL database.",
|
|
)
|
|
parser.add_argument(
|
|
"--database-url",
|
|
default=os.environ.get("GOVOPLAN_POSTGRES_DATABASE_URL") or os.environ.get("DATABASE_URL"),
|
|
help="SQLAlchemy PostgreSQL URL, for example postgresql+psycopg://user:pass@127.0.0.1:55432/govoplan.",
|
|
)
|
|
parser.add_argument(
|
|
"--module-set",
|
|
action="append",
|
|
default=[],
|
|
metavar="NAME=MODULES",
|
|
help="Module permutation to check. May be passed multiple times. Defaults to the standard product permutations.",
|
|
)
|
|
parser.add_argument(
|
|
"--reset-schema",
|
|
action="store_true",
|
|
help="DROP and recreate the public schema before every module set. Use only with a disposable database.",
|
|
)
|
|
parser.add_argument("--skip-migrate", action="store_true", help="Skip govoplan_core.commands.init_db.")
|
|
parser.add_argument("--skip-smoke", action="store_true", help="Skip govoplan_core.devserver --smoke.")
|
|
parser.add_argument("--python", default=sys.executable, help="Python executable to use. Defaults to the current interpreter.")
|
|
parser.add_argument("--app-env", default="staging", help="APP_ENV value to pass to subprocesses. Defaults to staging.")
|
|
args = parser.parse_args()
|
|
|
|
database_url = (args.database_url or "").strip()
|
|
if not database_url:
|
|
parser.error("--database-url or GOVOPLAN_POSTGRES_DATABASE_URL is required")
|
|
if not database_url.startswith("postgresql"):
|
|
parser.error("This check is PostgreSQL-only. Use a postgresql+psycopg:// or postgresql:// DATABASE_URL.")
|
|
|
|
module_sets = _parse_module_sets(args.module_set)
|
|
ok = True
|
|
for name, modules in module_sets:
|
|
print(f"\n== {name}: {modules} ==")
|
|
if args.reset_schema:
|
|
_reset_public_schema(database_url)
|
|
env = _check_env(database_url=database_url, modules=modules, app_env=args.app_env)
|
|
if not args.skip_migrate:
|
|
ok = _run([args.python, "-m", "govoplan_core.commands.init_db", "--database-url", database_url], env=env) and ok
|
|
if not args.skip_smoke:
|
|
ok = _run([args.python, "-m", "govoplan_core.devserver", "--smoke", "--no-reload"], env=env) and ok
|
|
return 0 if ok else 1
|
|
|
|
|
|
def _parse_module_sets(raw: list[str]) -> tuple[tuple[str, str], ...]:
|
|
if not raw:
|
|
return DEFAULT_MODULE_SETS
|
|
parsed: list[tuple[str, str]] = []
|
|
for item in raw:
|
|
name, separator, modules = item.partition("=")
|
|
if not separator or not name.strip() or not modules.strip():
|
|
raise SystemExit(f"Invalid --module-set value {item!r}. Use NAME=module_a,module_b.")
|
|
parsed.append((name.strip(), modules.strip()))
|
|
return tuple(parsed)
|
|
|
|
|
|
def _check_env(*, database_url: str, modules: str, app_env: str) -> dict[str, str]:
|
|
env = os.environ.copy()
|
|
env["APP_ENV"] = app_env
|
|
env["DATABASE_URL"] = database_url
|
|
env["ENABLED_MODULES"] = modules
|
|
env["DEV_AUTO_MIGRATE_ENABLED"] = "false"
|
|
env["DEV_BOOTSTRAP_ENABLED"] = "false"
|
|
env["CELERY_ENABLED"] = "false"
|
|
env.setdefault("MASTER_KEY_B64", base64.urlsafe_b64encode(os.urandom(32)).decode("ascii"))
|
|
env.setdefault("FILE_STORAGE_BACKEND", "local")
|
|
env.setdefault("FILE_STORAGE_LOCAL_ROOT", str(ROOT / "runtime" / "postgres-check-files"))
|
|
env.setdefault("MOCK_MAILBOX_DIR", str(ROOT / "runtime" / "postgres-check-mock-mailbox"))
|
|
env["PYTHONPATH"] = os.pathsep.join(part for part in (str(SRC), env.get("PYTHONPATH", "")) if part)
|
|
return env
|
|
|
|
|
|
def _run(command: list[str], *, env: dict[str, str]) -> bool:
|
|
print("+ " + " ".join(command))
|
|
result = subprocess.run(command, cwd=ROOT, env=env, check=False)
|
|
if result.returncode != 0:
|
|
print(f"Command failed with exit code {result.returncode}: {' '.join(command)}", file=sys.stderr)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _reset_public_schema(database_url: str) -> None:
|
|
try:
|
|
from sqlalchemy import create_engine
|
|
except ModuleNotFoundError as exc:
|
|
raise SystemExit("SQLAlchemy is required for --reset-schema. Install govoplan-core requirements first.") from exc
|
|
|
|
engine = create_engine(database_url, isolation_level="AUTOCOMMIT")
|
|
try:
|
|
with engine.connect() as connection:
|
|
connection.exec_driver_sql("DROP SCHEMA IF EXISTS public CASCADE")
|
|
connection.exec_driver_sql("CREATE SCHEMA public")
|
|
connection.exec_driver_sql("GRANT ALL ON SCHEMA public TO public")
|
|
finally:
|
|
engine.dispose()
|
|
print("Reset PostgreSQL schema: public")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|