Files
authentik/lifecycle/migrate.py
Jens L 7ea721c487 root: move database calls from ready() to dedicated startup signal (#9081)
* root: move database calls from ready() to dedicated startup signal

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* optimise gunicorn startup to only do DB code in one worker

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* always use 2 workers in compose

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* send startup signals for test runner

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove k8s import that isn't really needed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ci: bump nested actions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix @reconcile_app not triggering reconcile due to changed functions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* connect startup with uid

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* adjust some log levels

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove internal healthcheck

we didn't really use it to do anything, and we shouldn't have to since the live/ready probes are handled by django anyways and so the container runtime will restart the server if needed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add setproctitle for gunicorn and celery process titles

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* configure structlog early to use it

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Revert "configure structlog early to use it"

This reverts commit 16778fdbbca0f5c474d376c2f85c6f8032c06044.

* Revert "adjust some log levels"

This reverts commit a129f7ab6aecf27f1206aea1ad8384ce897b74ad.

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	authentik/root/settings.py

* optimize startup to not spawn a bunch of one-off processes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* idk why this shows up

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-02 14:19:32 +02:00

124 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python
"""System Migration handler"""
from importlib.util import module_from_spec, spec_from_file_location
from inspect import getmembers, isclass
from os import environ, system
from pathlib import Path
from typing import Any
from psycopg import Connection, Cursor, connect
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
LOGGER = get_logger()
ADV_LOCK_UID = 1000
LOCKED = False
class CommandError(Exception):
"""Error raised when a system_crit command fails"""
class BaseMigration:
"""Base System Migration"""
cur: Cursor
con: Connection
def __init__(self, cur: Any, con: Any):
self.cur = cur
self.con = con
def system_crit(self, command: str):
"""Run system command"""
LOGGER.debug("Running system_crit command", command=command)
retval = system(command) # nosec
if retval != 0:
raise CommandError("Migration error")
def fake_migration(self, *app_migration: tuple[str, str]):
"""Fake apply a list of migrations, arguments are
expected to be tuples of (app_label, migration_name)"""
for app, _migration in app_migration:
self.system_crit(f"./manage.py migrate {app} {_migration} --fake")
def needs_migration(self) -> bool:
"""Return true if Migration needs to be run"""
return False
def run(self):
"""Run the actual migration"""
def wait_for_lock(cursor: Cursor):
"""lock an advisory lock to prevent multiple instances from migrating at once"""
LOGGER.info("waiting to acquire database lock")
cursor.execute("SELECT pg_advisory_lock(%s)", (ADV_LOCK_UID,))
global LOCKED # noqa: PLW0603
LOCKED = True
def release_lock(cursor: Cursor):
"""Release database lock"""
if not LOCKED:
return
LOGGER.info("releasing database lock")
cursor.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,))
def run_migrations():
conn = connect(
dbname=CONFIG.get("postgresql.name"),
user=CONFIG.get("postgresql.user"),
password=CONFIG.get("postgresql.password"),
host=CONFIG.get("postgresql.host"),
port=CONFIG.get_int("postgresql.port"),
sslmode=CONFIG.get("postgresql.sslmode"),
sslrootcert=CONFIG.get("postgresql.sslrootcert"),
sslcert=CONFIG.get("postgresql.sslcert"),
sslkey=CONFIG.get("postgresql.sslkey"),
)
curr = conn.cursor()
try:
for migration_path in Path(__file__).parent.absolute().glob("system_migrations/*.py"):
spec = spec_from_file_location("lifecycle.system_migrations", migration_path)
if not spec:
continue
mod = module_from_spec(spec)
spec.loader.exec_module(mod)
for name, sub in getmembers(mod, isclass):
if name != "Migration":
continue
migration = sub(curr, conn)
if migration.needs_migration():
wait_for_lock(curr)
LOGGER.info("Migration needs to be applied", migration=migration_path.name)
migration.run()
LOGGER.info("Migration finished applying", migration=migration_path.name)
release_lock(curr)
LOGGER.info("applying django migrations")
environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
wait_for_lock(curr)
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(["", "migrate_schemas"])
execute_from_command_line(["", "migrate_schemas", "--schema", "template", "--tenant"])
execute_from_command_line(
["", "check"] + ([] if CONFIG.get_bool("debug") else ["--deploy"])
)
finally:
release_lock(curr)
if __name__ == "__main__":
run_migrations()