Files
authentik/scripts/generate_quadlet.py
T
Dominic R ca24943fb0 lifecycle/quadlet: use systemd restart policy
Agent-thread: https://sdko.org/internal/threads/019e93bf-9dbb-7ba2-9375-cb430876c101

A7k-product: product

A7k-product-repo: 1

Co-authored-by: Agent <agent@svc.sdko.net>
2026-06-04 13:59:42 -04:00

191 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Generate the Quadlet unit files under ``lifecycle/quadlet/``.
Mirrors ``scripts/generate_compose.py``: the authentik image tag is pinned to
``authentik_version()`` so ``make bump`` rewrites these unit files on every
release in lockstep with ``lifecycle/container/compose.yml``.
Quadlet does not expand ``${VAR}`` in ``Image=``, so the tag is written
literally rather than wrapped in a compose-style ``${AUTHENTIK_TAG:-...}``
fallback.
Two unit sets are emitted:
* ``lifecycle/quadlet/`` — rootful, dropped into ``/etc/containers/systemd/``.
* ``lifecycle/quadlet/rootless/`` — rootless, dropped into
``~/.config/containers/systemd/``. Uses systemd specifiers ``%h`` (home) and
``%t`` (``$XDG_RUNTIME_DIR``) so a single file works for any rootless user.
"""
from pathlib import Path
from packaging.version import parse
from authentik import authentik_version
version = authentik_version()
version_parsed = parse(version)
version_split = version_parsed.base_version.split(".")
# Mirror generate_compose.py: for an rc on a patch release (e.g. 2026.2.2-rc1)
# fall back to the previous released patch so the pinned tag is always pullable.
if version_parsed.is_prerelease and version_split[-1] != "0":
previous_patch = int(version_split[-1]) - 1
version_split[-1] = str(previous_patch)
version = ".".join(version_split)
AUTHENTIK_IMAGE = f"ghcr.io/goauthentik/server:{version}"
POSTGRES_IMAGE = "docker.io/library/postgres:16-alpine"
OUTPUT_DIR = Path("lifecycle/quadlet")
ROOTLESS_DIR = OUTPUT_DIR / "rootless"
POD = """[Unit]
Description=authentik
[Pod]
PodName=authentik
PublishPort=9000:9000
PublishPort=9443:9443
[Install]
WantedBy=default.target
"""
DATABASE_VOLUME = """[Volume]
VolumeName=authentik-database
"""
def postgresql_container(env_file: str) -> str:
return f"""[Unit]
Description=authentik PostgreSQL
[Container]
ContainerName=authentik-postgresql
Image={POSTGRES_IMAGE}
AutoUpdate=registry
Pod=authentik.pod
Volume=authentik-database.volume:/var/lib/postgresql/data
EnvironmentFile={env_file}
Environment=POSTGRES_DB=authentik
Environment=POSTGRES_USER=authentik
HealthCmd=pg_isready -d authentik -U authentik
HealthInterval=30s
HealthStartPeriod=20s
HealthTimeout=5s
HealthRetries=5
[Service]
Restart=always
[Install]
WantedBy=default.target
"""
def server_container(env_file: str, data_dir: str) -> str:
return f"""[Unit]
Description=authentik server
Requires=authentik-postgresql.container
After=authentik-postgresql.container
[Container]
ContainerName=authentik-server
Image={AUTHENTIK_IMAGE}
AutoUpdate=registry
Pod=authentik.pod
Exec=server
EnvironmentFile={env_file}
Environment=AUTHENTIK_POSTGRESQL__HOST=localhost
Environment=AUTHENTIK_POSTGRESQL__NAME=authentik
Environment=AUTHENTIK_POSTGRESQL__USER=authentik
Volume={data_dir}/data:/data:Z
Volume={data_dir}/custom-templates:/templates:Z
PodmanArgs=--shm-size=512m
[Service]
Restart=always
[Install]
WantedBy=default.target
"""
def worker_container(env_file: str, data_dir: str, podman_sock_dir: str) -> str:
return f"""[Unit]
Description=authentik worker
Requires=authentik-postgresql.container
After=authentik-postgresql.container
[Container]
ContainerName=authentik-worker
Image={AUTHENTIK_IMAGE}
AutoUpdate=registry
Pod=authentik.pod
Exec=worker
User=0:0
EnvironmentFile={env_file}
Environment=AUTHENTIK_POSTGRESQL__HOST=localhost
Environment=AUTHENTIK_POSTGRESQL__NAME=authentik
Environment=AUTHENTIK_POSTGRESQL__USER=authentik
Environment=AUTHENTIK_LISTEN__HTTP=0.0.0.0:9001
Environment=AUTHENTIK_LISTEN__METRICS=0.0.0.0:9301
Volume={podman_sock_dir}/podman/podman.sock:/run/podman/podman.sock:Z
Volume={data_dir}/data:/data:Z
Volume={data_dir}/certs:/certs:Z
Volume={data_dir}/custom-templates:/templates:Z
PodmanArgs=--shm-size=512m
[Service]
Restart=always
[Install]
WantedBy=default.target
"""
ROOTFUL = {
"authentik.pod": POD,
"authentik-database.volume": DATABASE_VOLUME,
"authentik-postgresql.container": postgresql_container(
env_file="/etc/authentik/authentik.env",
),
"authentik-server.container": server_container(
env_file="/etc/authentik/authentik.env",
data_dir="/var/lib/authentik",
),
"authentik-worker.container": worker_container(
env_file="/etc/authentik/authentik.env",
data_dir="/var/lib/authentik",
podman_sock_dir="/run",
),
}
ROOTLESS = {
"authentik.pod": POD,
"authentik-database.volume": DATABASE_VOLUME,
"authentik-postgresql.container": postgresql_container(
env_file="%h/.config/authentik/authentik.env",
),
"authentik-server.container": server_container(
env_file="%h/.config/authentik/authentik.env",
data_dir="%h/.local/share/authentik",
),
"authentik-worker.container": worker_container(
env_file="%h/.config/authentik/authentik.env",
data_dir="%h/.local/share/authentik",
podman_sock_dir="%t",
),
}
def write(target: Path, units: dict[str, str]) -> None:
target.mkdir(parents=True, exist_ok=True)
for name, body in units.items():
(target / name).write_text(body)
write(OUTPUT_DIR, ROOTFUL)
write(ROOTLESS_DIR, ROOTLESS)