blueprints: watch blueprints directory and trigger tasks (#4309)

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-12-30 11:30:18 +01:00 committed by GitHub
parent 5b68942b23
commit bd56922a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 2387 additions and 2296 deletions

View File

@ -10,6 +10,13 @@ from django.utils.text import slugify
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from watchdog.events import (
FileCreatedEvent,
FileModifiedEvent,
FileSystemEvent,
FileSystemEventHandler,
)
from watchdog.observers import Observer
from yaml import load from yaml import load
from yaml.error import YAMLError from yaml.error import YAMLError
@ -32,6 +39,7 @@ from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
_file_watcher_started = False
@dataclass @dataclass
@ -45,6 +53,39 @@ class BlueprintFile:
meta: Optional[BlueprintMetadata] = field(default=None) meta: Optional[BlueprintMetadata] = field(default=None)
def start_blueprint_watcher():
"""Start blueprint watcher, if it's not running already."""
# This function might be called twice since it's called on celery startup
# pylint: disable=global-statement
global _file_watcher_started
if _file_watcher_started:
return
observer = Observer()
observer.schedule(BlueprintEventHandler(), CONFIG.y("blueprints_dir"), recursive=True)
observer.start()
_file_watcher_started = True
class BlueprintEventHandler(FileSystemEventHandler):
"""Event handler for blueprint events"""
def on_any_event(self, event: FileSystemEvent):
if not isinstance(event, (FileCreatedEvent, FileModifiedEvent)):
return
if event.is_directory:
return
if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery")
blueprints_discover.delay()
if isinstance(event, FileModifiedEvent):
path = Path(event.src_path)
root = Path(CONFIG.y("blueprints_dir")).absolute()
rel_path = str(path.relative_to(root))
for instance in BlueprintInstance.objects.filter(path=rel_path):
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
apply_blueprint.delay(instance.pk.hex)
@CELERY_APP.task( @CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError), throws=(DatabaseError, ProgrammingError, InternalError),
) )
@ -60,8 +101,7 @@ def blueprints_find():
"""Find blueprints and return valid ones""" """Find blueprints and return valid ones"""
blueprints = [] blueprints = []
root = Path(CONFIG.y("blueprints_dir")) root = Path(CONFIG.y("blueprints_dir"))
for file in root.glob("**/*.yaml"): for path in root.glob("**/*.yaml"):
path = Path(file)
LOGGER.debug("found blueprint", path=str(path)) LOGGER.debug("found blueprint", path=str(path))
with open(path, "r", encoding="utf-8") as blueprint_file: with open(path, "r", encoding="utf-8") as blueprint_file:
try: try:

View File

@ -99,6 +99,9 @@ def worker_ready_hook(*args, **kwargs):
task.delay() task.delay()
except ProgrammingError as exc: except ProgrammingError as exc:
LOGGER.warning("Startup task failed", task=task, exc=exc) LOGGER.warning("Startup task failed", task=task, exc=exc)
from authentik.blueprints.v1.tasks import start_blueprint_watcher
start_blueprint_watcher()
# Using a string here means the worker doesn't have to serialize # Using a string here means the worker doesn't have to serialize

View File

@ -436,6 +436,7 @@ _LOGGING_HANDLER_MAP = {
"asyncio": "WARNING", "asyncio": "WARNING",
"redis": "WARNING", "redis": "WARNING",
"silk": "INFO", "silk": "INFO",
"fsevents": "WARNING",
} }
for handler_name, level in _LOGGING_HANDLER_MAP.items(): for handler_name, level in _LOGGING_HANDLER_MAP.items():
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false

4629
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -155,6 +155,7 @@ webauthn = "*"
wsproto = "*" wsproto = "*"
xmlsec = "*" xmlsec = "*"
zxcvbn = "*" zxcvbn = "*"
watchdog = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
bandit = "*" bandit = "*"

View File

@ -13,6 +13,11 @@ Blueprints offer a new way to template, automate and distribute authentik config
Blueprints are yaml files, whose format is described further in [File structure](./v1/structure). Blueprints can be applied in one of two ways: Blueprints are yaml files, whose format is described further in [File structure](./v1/structure). Blueprints can be applied in one of two ways:
- As a Blueprint instance, which is a YAML file mounted into the authentik (worker) container. This file is read and applied regularly (every 60 minutes). Multiple instances can be created for a single blueprint file, and instances can be given context key:value attributes to configure the blueprint. - As a Blueprint instance, which is a YAML file mounted into the authentik (worker) container. This file is read and applied regularly (every 60 minutes). Multiple instances can be created for a single blueprint file, and instances can be given context key:value attributes to configure the blueprint.
:::info
Starting with authentik 2022.12.1, authentik listens for file modification/creation events in the blueprint directory and will automatically trigger a discovery when a new blueprint file is created, and trigger a blueprint apply when a file is modified.
:::
- As a Flow import, which is a YAML file uploaded via the Browser/API. This file is validated and applied directly after being uploaded, but is not further monitored/applied. - As a Flow import, which is a YAML file uploaded via the Browser/API. This file is validated and applied directly after being uploaded, but is not further monitored/applied.
Starting with authentik 2022.8, blueprints are used to manage authentik default flows and other system objects. These blueprints can be disabled/replaced with custom blueprints in certain circumstances. Starting with authentik 2022.8, blueprints are used to manage authentik default flows and other system objects. These blueprints can be disabled/replaced with custom blueprints in certain circumstances.