blueprints: add !Context to lookup things from instance context

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-08-06 20:54:00 +02:00
parent 0227ba90bb
commit dcbf106daa
6 changed files with 53 additions and 3 deletions

View file

@ -30,6 +30,11 @@
} }
} }
}, },
"context": {
"$id": "#/properties/context",
"type": "object",
"additionalProperties": true
},
"entries": { "entries": {
"type": "array", "type": "array",
"items": { "items": {

View file

@ -100,6 +100,7 @@ class Blueprint:
entries: list[BlueprintEntry] = field(default_factory=list) entries: list[BlueprintEntry] = field(default_factory=list)
metadata: Optional[BlueprintMetadata] = field(default=None) metadata: Optional[BlueprintMetadata] = field(default=None)
context: Optional[dict] = field(default_factory=dict)
class YAMLTag: class YAMLTag:
@ -136,6 +137,29 @@ class KeyOf(YAMLTag):
) )
class Context(YAMLTag):
"""Lookup key from instance context"""
key: str
default: Optional[Any]
# pylint: disable=unused-argument
def __init__(self, loader: "BlueprintLoader", node: ScalarNode | SequenceNode) -> None:
super().__init__()
self.default = None
if isinstance(node, ScalarNode):
self.key = node.value
if isinstance(node, SequenceNode):
self.key = node.value[0].value
self.default = node.value[1].value
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
value = self.default
if self.key in blueprint.context:
value = blueprint.context[self.key]
return value
class Find(YAMLTag): class Find(YAMLTag):
"""Find any object""" """Find any object"""
@ -189,6 +213,7 @@ class BlueprintLoader(SafeLoader):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.add_constructor("!KeyOf", KeyOf) self.add_constructor("!KeyOf", KeyOf)
self.add_constructor("!Find", Find) self.add_constructor("!Find", Find)
self.add_constructor("!Context", Context)
class EntryInvalidError(SentryIgnoredException): class EntryInvalidError(SentryIgnoredException):

View file

@ -1,10 +1,11 @@
"""Blueprint importer""" """Blueprint importer"""
from contextlib import contextmanager from contextlib import contextmanager
from copy import deepcopy from copy import deepcopy
from typing import Any from typing import Any, Optional
from dacite import from_dict from dacite import from_dict
from dacite.exceptions import DaciteError from dacite.exceptions import DaciteError
from deepmerge import always_merger
from django.apps import apps from django.apps import apps
from django.db import transaction from django.db import transaction
from django.db.models import Model from django.db.models import Model
@ -75,7 +76,7 @@ class Importer:
logger: BoundLogger logger: BoundLogger
def __init__(self, yaml_input: str): def __init__(self, yaml_input: str, context: Optional[dict] = None):
self.__pk_map: dict[Any, Model] = {} self.__pk_map: dict[Any, Model] = {}
self.logger = get_logger() self.logger = get_logger()
import_dict = load(yaml_input, BlueprintLoader) import_dict = load(yaml_input, BlueprintLoader)
@ -83,6 +84,11 @@ class Importer:
self.__import = from_dict(Blueprint, import_dict) self.__import = from_dict(Blueprint, import_dict)
except DaciteError as exc: except DaciteError as exc:
raise EntryInvalidError from exc raise EntryInvalidError from exc
context = {}
always_merger.merge(context, self.__import.context)
if context:
always_merger.merge(context, context)
self.__import.context = context
@property @property
def blueprint(self) -> Blueprint: def blueprint(self) -> Blueprint:

View file

@ -121,7 +121,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(instance.path)) full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(instance.path))
file_hash = sha512(full_path.read_bytes()).hexdigest() file_hash = sha512(full_path.read_bytes()).hexdigest()
with open(full_path, "r", encoding="utf-8") as blueprint_file: with open(full_path, "r", encoding="utf-8") as blueprint_file:
importer = Importer(blueprint_file.read()) importer = Importer(blueprint_file.read(), instance.context)
valid, logs = importer.validate() valid, logs = importer.validate()
if not valid: if not valid:
instance.status = BlueprintInstanceStatus.ERROR instance.status = BlueprintInstanceStatus.ERROR

View file

@ -30,6 +30,11 @@
} }
} }
}, },
"context": {
"$id": "#/properties/context",
"type": "object",
"additionalProperties": true
},
"entries": { "entries": {
"type": "array", "type": "array",
"items": { "items": {

View file

@ -21,6 +21,12 @@ Example: `configure_flow: !Find [authentik_flows.flow, [slug, default-password-c
Looks up any model and resolves to the the matches' primary key. Looks up any model and resolves to the the matches' primary key.
First argument is the model to be queried, remaining arguments are expected to be pairs of key=value pairs to query for. First argument is the model to be queried, remaining arguments are expected to be pairs of key=value pairs to query for.
#### `!Context`
Example: `configure_flow: !Context foo`
Find values from the context. Can optionally be called with a default like `!Context [foo, default-value]`.
## Structure ## Structure
```yaml ```yaml
@ -32,6 +38,9 @@ metadata:
labels: labels:
foo: bar foo: bar
name: example-blueprint name: example-blueprint
# Optional default context, instance context is merged over this.
context:
foo: bar
# List of entries (required) # List of entries (required)
entries: entries:
- # Model in app.model notation, possibilities are listed in the schema (required) - # Model in app.model notation, possibilities are listed in the schema (required)