diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/daemon.py | 48 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py | 24 |
2 files changed, 67 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/daemon.py b/packages/meshbay-hub/src/meshbay_hub/daemon.py index 024ca78..9609de6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/daemon.py +++ b/packages/meshbay-hub/src/meshbay_hub/daemon.py @@ -35,6 +35,12 @@ def main() -> None: prune.add_argument("--log-level", default=argparse.SUPPRESS, choices=["DEBUG", "INFO", "WARNING", "ERROR"]) + migrate = sub.add_parser( + "migrate", help="bring the database schema up to date") + migrate.add_argument("--config", type=Path, default=argparse.SUPPRESS) + migrate.add_argument("--log-level", default=argparse.SUPPRESS, + choices=["DEBUG", "INFO", "WARNING", "ERROR"]) + args = parser.parse_args() logging.basicConfig( @@ -47,6 +53,9 @@ def main() -> None: if args.command == "prune-groups": sys.exit(asyncio.run(_prune_groups(cfg, args.days, args.dry_run))) + if args.command == "migrate": + sys.exit(_migrate(cfg)) + single_worker_or_exit(cfg.server.workers) uvicorn.run( @@ -59,6 +68,45 @@ def main() -> None: ) +def migrations_dir() -> Path: + """Where this installation's migrations actually are. + + Derived from the package rather than written down, because the one place a + path like this can be correct is next to the code it describes. The RPM + installs `meshbay_hub` into a shared venv and copies `alembic.ini` to + `/opt/meshbay-hub/migrations/`, where `%(here)s/src/meshbay_hub/db/…` + resolves to a directory that does not exist — so the packaged unit's + `ExecStartPre` could never have succeeded, and a hub installed from the + package would not start at all. The same `%(here)s` trap had already been + found once on the server, with a copy of alembic.ini pointing at a + month-old snapshot of the tree. + """ + return Path(__file__).resolve().parent / "db" / "migrations" + + +def _migrate(cfg) -> int: + """`alembic upgrade head`, with the paths resolved from the installation. + + A command rather than a path in a unit file: it is correct for the RPM, + the DEB, a venv, and a checkout, and there is nothing to keep in step. + """ + from alembic import command + from alembic.config import Config + + scripts = migrations_dir() + if not (scripts / "versions").is_dir(): + print(f"meshbay-hub: no migrations at {scripts}", file=sys.stderr) + return 1 + + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", str(scripts)) + # `env.py` reads the URL from the environment the same way the server does, + # so the two cannot drift; this is set for the case where it does not. + alembic_cfg.set_main_option("sqlalchemy.url", cfg.db.url) + command.upgrade(alembic_cfg, "head") + return 0 + + def single_worker_or_exit(workers: int) -> None: """Refuse to start with more than one worker. diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py index f572716..8908deb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py @@ -19,11 +19,25 @@ if config.config_file_name is not None: target_metadata = Base.metadata -# Allow override via env var (production uses asyncpg, tests may use aiosqlite) -db_url = os.environ.get( - "MESHBAY_DATABASE_URL", - "postgresql+asyncpg://meshbay:meshbay@localhost/meshbay_hub", -) +# Three sources, in the order that keeps them from disagreeing. +# +# A caller that already resolved the URL wins: `meshbay-hub migrate` passes +# `cfg.db.url`, which is `load_config`'s answer — the very string the server +# will connect with. Reading the environment again here instead would be a +# second resolution of the same question, and the two would drift the day +# anything but the environment decides it. +# +# Then the environment, for `alembic` run by hand or from a deploy script, +# where `alembic.ini` carries only its placeholder. Then a local default, so +# a developer's checkout needs no setup. +_PLACEHOLDER = "driver://user:pass@localhost/dbname" + +db_url = config.get_main_option("sqlalchemy.url", "") +if not db_url or db_url == _PLACEHOLDER: + db_url = os.environ.get( + "MESHBAY_DATABASE_URL", + "postgresql+asyncpg://meshbay:meshbay@localhost/meshbay_hub", + ) config.set_main_option("sqlalchemy.url", db_url) |