* 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>
		
			
				
	
	
		
			124 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			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()
 |