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>
This commit is contained in:
Jens L
2024-04-02 14:19:32 +02:00
committed by GitHub
parent 4d8ee983ef
commit 7ea721c487
21 changed files with 290 additions and 82 deletions

View File

@ -7,7 +7,6 @@ function log {
function wait_for_db {
python -m lifecycle.wait_for_db
python -m lifecycle.migrate
log "Bootstrap completed"
}
@ -65,7 +64,6 @@ if [[ "${AUTHENTIK_REMOTE_DEBUG}" == "true" ]]; then
fi
if [[ "$1" == "server" ]]; then
wait_for_db
set_mode "server"
# If we have bootstrap credentials set, run bootstrap tasks outside of main server
# sync, so that we can sure the first start actually has working bootstrap
@ -75,7 +73,6 @@ if [[ "$1" == "server" ]]; then
fi
run_authentik
elif [[ "$1" == "worker" ]]; then
wait_for_db
set_mode "worker"
check_if_root "python -m manage worker"
elif [[ "$1" == "worker-status" ]]; then

View File

@ -2,13 +2,11 @@
import os
from hashlib import sha512
from multiprocessing import cpu_count
from os import makedirs
from pathlib import Path
from tempfile import gettempdir
from typing import TYPE_CHECKING
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from prometheus_client.values import MultiProcessValue
from authentik import get_full_version
@ -17,11 +15,18 @@ from authentik.lib.logging import get_logger_config
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.reflection import get_env
from authentik.root.install_id import get_install_id_raw
from lifecycle.migrate import run_migrations
from lifecycle.wait_for_db import wait_for_db
from lifecycle.worker import DjangoUvicornWorker
if TYPE_CHECKING:
from gunicorn.app.wsgiapp import WSGIApplication
from gunicorn.arbiter import Arbiter
from authentik.root.asgi import AuthentikAsgi
wait_for_db()
_tmp = Path(gettempdir())
worker_class = "lifecycle.worker.DjangoUvicornWorker"
worker_tmp_dir = str(_tmp.joinpath("authentik_worker_tmp"))
@ -35,17 +40,14 @@ bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
preload = True
max_requests = 1000
max_requests_jitter = 50
logconfig_dict = get_logger_config()
# if we're running in kubernetes, use fixed workers because we can scale with more pods
# otherwise (assume docker-compose), use as much as we can
if SERVICE_HOST_ENV_NAME in os.environ:
default_workers = 2
else:
default_workers = max(cpu_count() * 0.25, 1) + 1 # Minimum of 2 workers
default_workers = 2
workers = CONFIG.get_int("web.workers", default_workers)
threads = CONFIG.get_int("web.threads", 4)
@ -100,6 +102,18 @@ def pre_fork(server: "Arbiter", worker: DjangoUvicornWorker):
worker._worker_id = _next_worker_id(server)
def post_worker_init(worker: DjangoUvicornWorker):
"""Notify ASGI app that its started up"""
# Only trigger startup DB logic on first worker
# Startup code that imports code or is otherwise needed in every worker
# does not use this signal, so we can skip this safely
if worker._worker_id != 1:
return
app: "WSGIApplication" = worker.app
root_app: "AuthentikAsgi" = app.callable
root_app.call_startup()
if not CONFIG.get_bool("disable_startup_analytics", False):
env = get_env()
should_send = env not in ["dev", "ci"]
@ -129,3 +143,5 @@ if CONFIG.get_bool("remote_debug"):
import debugpy
debugpy.listen(("0.0.0.0", 6800)) # nosec
run_migrations()

View File

@ -68,7 +68,7 @@ def release_lock(cursor: Cursor):
cursor.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,))
if __name__ == "__main__":
def run_migrations():
conn = connect(
dbname=CONFIG.get("postgresql.name"),
user=CONFIG.get("postgresql.user"),
@ -117,3 +117,7 @@ if __name__ == "__main__":
)
finally:
release_lock(curr)
if __name__ == "__main__":
run_migrations()

View File

@ -11,52 +11,61 @@ from redis.exceptions import RedisError
from authentik.lib.config import CONFIG
CONFIG.log("info", "Starting authentik bootstrap")
# Sanity check, ensure SECRET_KEY is set before we even check for database connectivity
if CONFIG.get("secret_key") is None or len(CONFIG.get("secret_key")) == 0:
CONFIG.log("info", "----------------------------------------------------------------------")
CONFIG.log("info", "Secret key missing, check https://goauthentik.io/docs/installation/.")
CONFIG.log("info", "----------------------------------------------------------------------")
sysexit(1)
def check_postgres():
while True:
try:
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"),
)
conn.cursor()
break
except OperationalError as exc:
sleep(1)
CONFIG.log("info", f"PostgreSQL connection failed, retrying... ({exc})")
CONFIG.log("info", "PostgreSQL connection successful")
while True:
try:
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"),
)
conn.cursor()
break
except OperationalError as exc:
sleep(1)
CONFIG.log("info", f"PostgreSQL connection failed, retrying... ({exc})")
CONFIG.log("info", "PostgreSQL connection successful")
def check_redis():
REDIS_PROTOCOL_PREFIX = "redis://"
if CONFIG.get_bool("redis.tls", False):
REDIS_PROTOCOL_PREFIX = "rediss://"
REDIS_URL = (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{quote_plus(CONFIG.get('redis.password'))}@{quote_plus(CONFIG.get('redis.host'))}:"
f"{CONFIG.get_int('redis.port')}/{CONFIG.get('redis.db')}"
)
while True:
try:
redis = Redis.from_url(REDIS_URL)
redis.ping()
break
except RedisError as exc:
sleep(1)
CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
CONFIG.log("info", "Redis Connection successful")
REDIS_PROTOCOL_PREFIX = "redis://"
if CONFIG.get_bool("redis.tls", False):
REDIS_PROTOCOL_PREFIX = "rediss://"
REDIS_URL = (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{quote_plus(CONFIG.get('redis.password'))}@{quote_plus(CONFIG.get('redis.host'))}:"
f"{CONFIG.get_int('redis.port')}/{CONFIG.get('redis.db')}"
)
while True:
try:
redis = Redis.from_url(REDIS_URL)
redis.ping()
break
except RedisError as exc:
sleep(1)
CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
CONFIG.log("info", "Redis Connection successful")
CONFIG.log("info", "Finished authentik bootstrap")
def wait_for_db():
CONFIG.log("info", "Starting authentik bootstrap")
# Sanity check, ensure SECRET_KEY is set before we even check for database connectivity
if CONFIG.get("secret_key") is None or len(CONFIG.get("secret_key")) == 0:
CONFIG.log("info", "----------------------------------------------------------------------")
CONFIG.log("info", "Secret key missing, check https://goauthentik.io/docs/installation/.")
CONFIG.log("info", "----------------------------------------------------------------------")
sysexit(1)
check_postgres()
check_redis()
CONFIG.log("info", "Finished authentik bootstrap")
if __name__ == "__main__":
wait_for_db()

View File

@ -12,3 +12,5 @@ class DjangoUvicornWorker(UvicornWorker):
"lifespan": "off",
"ws": "wsproto",
}
_worker_id: int