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:
parent
5b68942b23
commit
bd56922a2f
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
4629
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -155,6 +155,7 @@ webauthn = "*"
|
||||||
wsproto = "*"
|
wsproto = "*"
|
||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
zxcvbn = "*"
|
zxcvbn = "*"
|
||||||
|
watchdog = "*"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Reference in a new issue