feat(config)!: replace combine.yml with auto-applying config-sets #12 #14

Open
moritz wants to merge 1 commits from config-set-combine into main
7 changed files with 688 additions and 419 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ RUN apt-get update \
COPY --from=abra /out/abra /usr/local/bin/abra
COPY --from=deps /opt/alakazam /opt/alakazam
COPY alakazam.py combine.yml /opt/alakazam/
COPY alakazam.py /opt/alakazam/
RUN printf '#!/bin/sh\nexec /opt/alakazam/bin/python /opt/alakazam/alakazam.py "$@"\n' \
> /usr/local/bin/alakazam \
&& chmod +x /usr/local/bin/alakazam
+39 -28
View File
@@ -227,12 +227,7 @@ Configuration files support templating with Jinja2 and global variables, facilit
1. **`alaka.yml`**/**`alaka-*.yml`**: Contains global configurations for a hierarchical layer, affecting all subordinate apps.
2. **`example.com.yml`** (`<your-domain>.yml`): A minimalist configuration for each instance, specifying the apps to be installed, the instance domain and optionally instance specific configurations.
- an **instance** means a collection of apps that are integrated with each other (one server / vm can contain multiple instances)
3. **`combine.yml`**: Manages configurations required to integrate multiple apps with each other.
- This configuration should not be touched by the operator.
- At the moment it's part of the `alakazam` repository.
- In future it should be split into the recipes repositories and maintained by the recipe maintainer.
- We started to move each app entry as `alaconnect.yml` into the respective repositories.
4. **`config-sets.yml`**: Optional file placed at the `root` path. Defines named sets of per-app configurations that can be activated or deactivated per instance or group, avoiding duplication across instance files. See [examples/config-sets.yml](./examples/config-sets.yml).
3. **`config-sets.yml`**/**`config-sets-*.yml`**: Optional files at the `root` path. They define named sets of per-app configurations, either switched on per instance or group, or applying by themselves as an integration between apps. See [Config-Sets](#config-sets).
### Global Settings
@@ -259,7 +254,7 @@ uptime_kuma:
### App Configuration
`alaka.yml`/`alaka-*.yml`, `example.com.yml` and `combine.yml`/`alaconnect.yml` contain a similar configuration structure.
`alaka.yml`/`alaka-*.yml`, `example.com.yml` and the config-sets contain a similar configuration structure.
For each app/recipe the following `<app_configurations>` can be used:
- **`uncomment`/`comment`**: Manages the active status of lines in configuration files, such as uncommenting needed variables .
@@ -274,16 +269,45 @@ For each app/recipe the following `<app_configurations>` can be used:
- **`dependency`**: Names the apps that have to be set up before this one. See [Dependencies](#dependencies).
- **`secrets`**: Inserts specific values (i.E. smtp passwords) into secrets; future updates will support encrypted file usage.
- **`secret-hooks`**: Run `abra.sh` commands locally or local scripts for secrets that need to be generated.
- **`subdomain`**: Specifies the subdomain scheme for individual recipes and apps. (not available in `combine.yml`/`alaconnect.yml`)
- **`shared_secrets`**: Shares a secret between two apps, as `<source_app>: {<target_secret>: <source_secret>}`.
- **`subdomain`**: Specifies the subdomain scheme for individual recipes and apps. (not available in config-sets)
- i.e. `cloud.example.com` for nextcloud
- **`version`**: Controls the recipe version to deploy; if unspecified, the latest version is used. (not available in `combine.yml`/`alaconnect.yml`)
- **`version`**: Controls the recipe version to deploy; if unspecified, the latest version is used. (not available in config-sets)
- a commit can be pinned with `<release>@<commit>`, for example `12.0.2+2026.5.2@be9ebb3`
- only the commit is deployed; the release in front of the `@` is there so that a dependency bot can follow the recipe's tags and offer patch updates
The `combine.yml`/`alaconnect.yml` configuration additionally contains:
### Config-Sets
- **`shared_secrets`**: Specifies secret sharing between apps.
- `<source_secret_name>:<target_secret_name>`
A config-set is a named bundle of per-app configuration, defined in `config-sets.yml` or any `config-sets-*.yml` at the `root` path. The files are merged in the order `config-sets.yml`, then the `config-sets-*.yml` alphabetically, the same precedence the `alaka*.yml` of a directory have.
A set applies in one of two ways.
**Switched on per instance or group**, via `CONFIG-SETS` in any `alaka.yml` or `example.com.yml`:
```yaml
CONFIG-SETS:
bbb: true
```
Review

The second way, apply-when, describes the use for an integration with the config-sets-combine.yml example. Here, we could add a line for the other use case with the example file:

This can be used for enabling sets of per-app configurations, e.g. for enabling features, at the group or instance level. See [examples/config-sets.yml](./examples/config-sets.yml).

The second way, apply-when, describes the use for an integration with the `config-sets-combine.yml` example. Here, we could add a line for the other use case with the example file: `This can be used for enabling sets of per-app configurations, e.g. for enabling features, at the group or instance level. See [examples/config-sets.yml](./examples/config-sets.yml).`
**By itself**, when it carries `apply-when` and every app named there is part of the instance:
```yaml
integrate-authentik-nextcloud:
apply-when: [authentik, nextcloud]
authentik:
uncomment: [compose.nextcloud.yml]
nextcloud:
uncomment: [compose.authentik.yml]
shared_secrets:
authentik:
nextcloud_secret: authentik_secret
```
That is what an integration between two apps is: configuration belonging to both of them that is pointless unless both are there. `apply-when` names the condition rather than deriving it from the configured apps, so an integration can configure only one side: `[matrix-synapse, traefik]` while configuring `traefik` alone. An instance can still switch such a set off with `CONFIG-SETS: {integrate-authentik-nextcloud: false}`.
Merge priority: automatic sets < group < sets switched on explicitly < instance. An integration is the lowest layer of all, so a group configuration can adjust it — the precedence the `combine.yml` it replaces had. Its list entries land behind the ones an app configures for itself, which is what keeps an integration hook such as `app set_authentik` running after the `app core_install` that sets the app up in the first place.
[examples/config-sets-combine.yml](./examples/config-sets-combine.yml) holds the integrations alakazam used to ship as `combine.yml`. Copy it into your `root` path to keep them.
### Dependencies
@@ -346,22 +370,9 @@ These configurations are designed to modularize and simplify the management of a
<app_configurations>
```
2. **`combine.yml`**
- This file orchestrates the integration between multiple apps within the same instance (`example.com.yml`), applying `<target_app_configurations>` to ensure seamless functionality between connected applications.
- The configuration for each app combination is structured as follows, again `<target_app_configurations>` is following the guidelines set in the [App Configuration](#app-configuration) section:
```
<target_app_recipe>:
<source_app_recipe>:
<target_app_configurations>
```
- In future each `<source_app_recipe>` entry will be placed in the `alaconnect.yml` inside the `<target_app_recipe>` folder.
3. **`config-sets.yml`**
- Optional file at the `root` path that defines named sets of per-app configurations. Each top-level key is a config-set name; its value is a map of `<app_recipe>` to `<app_configurations>`.
- Activate or disable sets per instance or group via `CONFIG-SETS` in any `alaka.yml` or `example.com.yml`. Merge priority: group < config-set < instance.
- See [examples/config-sets.yml](./examples/config-sets.yml).
2. **`config-sets.yml`**/**`config-sets-*.yml`**
- Optional files at the `root` path. Each top-level key is a config-set name; its value is a map of `<app_recipe>` to `<app_configurations>`.
- See [Config-Sets](#config-sets).
### Templating Configurations
+83 -61
View File
@@ -4,7 +4,7 @@ import os
import json
import logging
from pathlib import Path
from typing import List, Dict, Any, Iterator, Optional, Union, Set, Tuple
from typing import List, Dict, Any, Iterable, Iterator, Optional, Union, Set, Tuple
import subprocess
import re
@@ -23,8 +23,9 @@ from packaging import version
from uptime_kuma_api import UptimeKumaApi, MonitorType
from time import sleep, time
COMBINE_PATH = os.path.dirname(os.path.realpath(__file__)) + "/combine.yml"
NON_APP_KEYS = {'CONFIG-SETS', 'GLOBALS'}
# key of a config-set that names the apps whose presence makes the set apply by itself
APPLY_WHEN = 'apply-when'
# INSTANCE_CONFIGS: dict: contains all app organized by recipe names and instance domains
# The structure of the dictionary is as follows:
# {
@@ -95,6 +96,8 @@ READINESS_INITIAL_DELAY = 0 # seconds to wait before the first attempt
SECRET_VARIANTS = ("conf", "secret-hooks", "exchange", "generate")
# group configuration files, inherited down the directory hierarchy
GROUP_CONFIG_RE = re.compile(r'^alaka(-.*)?\.ya?ml$')
# config-set files, read from the root path only
CONFIG_SET_RE = re.compile(r'^config-sets(-.*)?\.ya?ml$')
# instance configuration files, named '<domain>.yml' after the instance they configure
INSTANCE_CONFIG_RE = re.compile(r'^(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,6}(?:\.yaml|\.yml)$')
ROOT_PATH = None # resolved root path from alakazam.yml
1
@@ -190,7 +193,7 @@ def get_relevant_config_paths(root_path: Path, group_path: Path, exclude_paths:
Returns:
list: The configuration files to validate, without duplicates
"""
paths = [Path(COMBINE_PATH), root_path / "config-sets.yml"]
paths = [root_path / file for file in os.listdir(root_path) if CONFIG_SET_RE.match(file)]
directory = group_path if group_path.is_dir() else group_path.parent
for ancestor in reversed([directory, *directory.parents]):
if ancestor != root_path and root_path not in ancestor.parents:
@@ -404,31 +407,79 @@ def merge_all_group_configs(root_path: Path) -> Dict[str, Dict[str, Any]]:
def get_config_set_app_configs(
active_config_sets: Dict[str, Any], config_sets: Dict[str, Any]
) -> Dict[str, Any]:
def read_config_sets(root_path: Path) -> Dict[str, Any]:
"""
Merges per-app configurations from all active config-sets into a single dictionary.
Config-sets disabled with False are skipped; unknown set names log a warning.
When multiple active sets define config for the same app, later sets take precedence.
Reads and merges every 'config-sets*.yml' of the root path.
'config-sets.yml' comes first as the base, the 'config-sets-*.yml' specialisations follow in
alphabetical order, the same precedence the 'alaka*.yml' of a directory have. Config-sets live
in the root path only, so that an integration reads the same for every instance below it.
Args:
root_path (Path): The alakazam root path
Returns:
dict: The merged config-set definitions
"""
files = sorted((f for f in os.listdir(root_path) if CONFIG_SET_RE.match(f)),
key=lambda f: (not f.startswith("config-sets."), f))
merged: Dict[str, Any] = {}
defined_by: Dict[Tuple[str, ...], Tuple[str, Any]] = {}
for file in files:
config = read_config(str(root_path / file)) or {}
warn_on_key_collisions(config, defined_by, file, str(root_path))
merged = merge_dict(merged, config)
return merged
def get_config_set_app_configs(
active_config_sets: Dict[str, Any], config_sets: Dict[str, Any], instance_apps: Iterable[str]
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""
Merges per-app configurations from all applying config-sets into a single dictionary.
A set applies either because the instance enabled it under CONFIG-SETS, or by itself when it
carries an 'apply-when' and every app named there is part of the instance. That is what an
integration between two apps is: configuration that belongs to both of them and is pointless
unless both are there. Setting such a set to False for an instance still turns it off.
The two kinds are returned apart because they sit at different heights: an integration is the
lowest layer of all, below even the group configuration, so that a group can adjust it. That is
the precedence the combine.yml this replaces had. Among the explicit ones, later sets win.
Args:
active_config_sets (dict): Merged CONFIG-SETS dict (set name -> True or False).
config_sets (dict): Full config-set definitions loaded from config-sets.yml.
config_sets (dict): Full config-set definitions from the root path.
instance_apps (iterable): The app names configured for this instance.
Returns:
dict: A merged dictionary of per-app configurations from all active config-sets.
tuple: The per-app configurations of the automatic sets and of the explicitly enabled ones
"""
automatic_app_configs: Dict[str, Any] = {}
config_set_app_configs: Dict[str, Any] = {}
automatic = set()
for name, definition in config_sets.items():
if not (apply_when := (definition or {}).get(APPLY_WHEN)):
continue
automatic.add(name)
if active_config_sets.get(name) is False:
logging.debug(f"config-set '{name}' applies but is switched off for this instance")
continue
if not all(app in instance_apps for app in apply_when):
continue
logging.debug(f"config-set '{name}' applies, {', '.join(apply_when)} are all configured")
apps = {app: config for app, config in definition.items() if app != APPLY_WHEN}
automatic_app_configs = merge_dict(automatic_app_configs, apps)
for config_set_name, config_set_enabled in active_config_sets.items():
if config_set_enabled:
if config_set_name not in config_sets:
logging.warning(
f"Config-set '{config_set_name}' is enabled but not defined in config-sets.yml. Skipping."
)
else:
config_set_app_configs = merge_dict(config_set_app_configs, config_sets[config_set_name] or {})
return config_set_app_configs
if not config_set_enabled or config_set_name in automatic:
continue
if config_set_name not in config_sets:
logging.warning(
f"Config-set '{config_set_name}' is enabled but not defined in the root path. Skipping."
)
continue
config_set_app_configs = merge_dict(config_set_app_configs, config_sets[config_set_name] or {})
return automatic_app_configs, config_set_app_configs
def substitute_jinja_variable(jinja_dict, subs_dict) -> None:
@@ -474,8 +525,8 @@ def merge_instance_configs(group_config: Dict[str, Any], instance_domain: str, i
group_config.get('CONFIG-SETS') or {},
instance_config.get('CONFIG-SETS') or {},
)
config_set_app_configs = get_config_set_app_configs(active_config_sets, config_sets)
instance_apps = {k: v for k, v in instance_config.items() if k not in NON_APP_KEYS}
automatic_app_configs, config_set_app_configs = get_config_set_app_configs(active_config_sets, config_sets, instance_apps)
# Warn about config-set entries that target apps not listed in the instance config.
for config_set_app in set(config_set_app_configs) - set(instance_apps):
Review

This only checks the non-automatic config sets. E.g. an apply-when set with a typo in the app key for the applied configs is silently not applied. Update to:

for config_set_app in (set(automatic_app_configs) | set(config_set_app_configs)) - set(instance_apps):

This only checks the non-automatic config sets. E.g. an apply-when set with a typo in the app key for the applied configs is silently not applied. Update to: `for config_set_app in (set(automatic_app_configs) | set(config_set_app_configs)) - set(instance_apps):`
logging.warning(
@@ -484,11 +535,18 @@ def merge_instance_configs(group_config: Dict[str, Any], instance_domain: str, i
)
merged_config = {}
for app in instance_apps:
automatic_app_config = automatic_app_configs.get(app) or {}
group_app_config = group_config.get(app) or {}
config_set_app_config = config_set_app_configs.get(app) or {}
instance_app_config = instance_apps[app] or {}
# Priority: group < config-set < instance
merged_config[app] = merge_dict(merge_dict(group_app_config, config_set_app_config), instance_app_config)
# Priority: automatic set < group < config-set < instance. The automatic sets are merged
# last so that everything else outranks them, and with the list order reversed so that
# their entries land behind the ones the app configures for itself. An integration that
# adds 'set_authentik' must run after the 'core_install' that sets the app up at all.
merged_config[app] = merge_dict(
automatic_app_config,
merge_dict(merge_dict(group_app_config, config_set_app_config), instance_app_config),
reverse_list_order=True)
merged_config[app]['app_domain'] = map_subdomain(app, instance_domain, merged_config[app])
if not merged_config[app].get('server'):
merged_config[app]['server'] = server
@@ -609,42 +667,6 @@ def exclude_from_configs(configs: Dict[str, Dict[str, Any]], recipes: Tuple[str]
}
def merge_connection_configs(configs: Dict[str, Any]) -> Dict[str, Any]:
"""
Merge connection configurations from the 'combine.yml' to extend instance configurations with inter-app secrets and settings.
This involves integrating shared secrets and other connection-specific settings between applications within the same instance.
Args:
configs (dict): The initial instance configurations before integrating connection-specific adjustments.
Returns:
dict: The updated instance configurations after applying the connection settings.
"""
connection_config = read_config(COMBINE_PATH)
extend_shared_secrets(connection_config)
merged_configs = configs.copy()
for _, instance_config in merged_configs.items():
for target_app, source_apps in connection_config.items():
for source_app, target_conf in source_apps.items():
if target_app in instance_config and source_app in instance_config:
instance_config[target_app] = merge_dict(target_conf, instance_config[target_app], reverse_list_order=True)
return merged_configs
def extend_shared_secrets(connection_config: Dict[str, Any]) -> None:
"""
Extends connection configurations by embedding source app details into the shared secrets configuration.
This modifies the existing connection configurations in place, adding a layer of source app information to shared secrets.
Args:
connection_config (dict): Connection configurations which involve shared secrets.
"""
for _, source_apps in connection_config.items():
for source_app, target_conf in source_apps.items():
if shared_secrets:= target_conf.get('shared_secrets'):
target_conf['shared_secrets'] = {source_app: shared_secrets}
def is_secret_command(args: Tuple[str, ...]) -> bool:
"""
Checks whether an abra command carries a secret value in its arguments or in its output.
@@ -1653,15 +1675,15 @@ def cli(loglevel: str, group_path: str, exclude: Tuple[str], exclude_recipes: Tu
exclude_paths.append(str(ABRA_DIR))
preflight_configs(get_relevant_config_paths(ROOT_PATH, _group_path, exclude_paths))
all_group_configs = merge_all_group_configs(ROOT_PATH)
config_sets = read_config(str(ROOT_PATH / "config-sets.yml"))
config_sets = read_config_sets(ROOT_PATH)
instance_configs = get_merged_instance_configs(_group_path, all_group_configs, exclude_paths, config_sets)
INSTANCE_CONFIGS = merge_connection_configs(instance_configs)
INSTANCE_CONFIGS = instance_configs
# dropping the recipes here rather than at every filter is what makes the exclusion complete:
# get_apps(), create_secrets(), configure_apps() and the rest all read INSTANCE_CONFIGS.
# ALL_CONFIGS stays whole, it is the cross-instance view that backup looks up its bot in
INSTANCE_CONFIGS = exclude_from_configs(INSTANCE_CONFIGS, exclude_recipes)
all_configs = get_merged_instance_configs(ROOT_PATH, all_group_configs, exclude_paths, config_sets)
ALL_CONFIGS = merge_connection_configs(all_configs)
ALL_CONFIGS = all_configs
fetch_recipes(INSTANCE_CONFIGS)
-327
View File
@@ -1,327 +0,0 @@
authentik:
nextcloud:
uncomment:
- compose.nextcloud.yml
- NEXTCLOUD_DOMAIN
- SECRET_NEXTCLOUD_ID_VERSION
- SECRET_NEXTCLOUD_SECRET_VERSION
- nextcloud.png
wordpress:
uncomment:
- compose.wordpress.yml
- WORDPRESS_DOMAIN
- WORDPRESS_GROUP
- SECRET_WORDPRESS_ID_VERSION
- SECRET_WORDPRESS_SECRET_VERSION
- wordpress.png
matrix-synapse:
uncomment:
- compose.matrix.yml
- ELEMENT_DOMAIN
- MATRIX_DOMAIN
- SECRET_MATRIX_ID_VERSION
- SECRET_MATRIX_SECRET_VERSION
- matrix.svg
secrets:
matrix_id: matrix
wekan:
uncomment:
- compose.wekan.yml
- WEKAN_DOMAIN
- SECRET_WEKAN_ID_VERSION
- SECRET_WEKAN_SECRET_VERSION
- wekan.png
secrets:
wekan_id: wekan
vikunja:
uncomment:
- compose.vikunja.yml
- VIKUNJA_DOMAIN
- SECRET_VIKUNJA_ID_VERSION
- SECRET_VIKUNJA_SECRET_VERSION
- vikunja.svg
secrets:
vikunja_id: vikunja
kimai:
uncomment:
- compose.kimai.yml
- KIMAI_DOMAIN
- SECRET_KIMAI_ID_VERSION
- SECRET_KIMAI_SECRET_VERSION
- kimai_logo.png
- KIMAI_GROUP
zammad:
uncomment:
- compose.zammad.yml
- ZAMMAD_DOMAIN
- zammad.svg
monitoring-ng:
uncomment:
- compose.monitoring.yml
- MONITORING_DOMAIN
- SECRET_MONITORING_ID_VERSION
- SECRET_MONITORING_SECRET_VERSION
- monitoring.svg
secrets:
monitoring_id: monitoring
outline:
uncomment:
- compose.outline.yml
- OUTLINE_DOMAIN
- SECRET_OUTLINE_ID_VERSION
- SECRET_OUTLINE_SECRET_VERSION
- outline.png
secrets:
outline_id: outline
rallly:
uncomment:
- compose.rallly.yml
- RALLLY_DOMAIN
- SECRET_RALLLY_ID_VERSION
- SECRET_RALLLY_SECRET_VERSION
- rallly.png
secrets:
rallly_id: rallly
hedgedoc:
uncomment:
- compose.hedgedoc.yml
- HEDGEDOC_DOMAIN
- SECRET_HEDGEDOC_ID_VERSION
- SECRET_HEDGEDOC_SECRET_VERSION
- hedgedoc.png
secrets:
hedgedoc_id: hedgedoc
pretix:
env:
APPLICATIONS:
Pretix:
url: https://pretix.example.com/control/
group:
EXTRA_ICONS:
Pretix: ~/.abra/recipes/authentik/icons/pretix.svg
vaultwarden:
env:
APPLICATIONS:
Vaultwarden:
url: https://vaultwarden.example.com/
group:
EXTRA_ICONS:
Vaultwarden: ~/.abra/recipes/authentik/icons/vaultwarden.svg
mila:
uncomment:
- compose.mila.yml
- MILA_DOMAIN
- MILA_GROUP
- SECRET_MILA_ID_VERSION
- SECRET_MILA_SECRET_VERSION
- mila.svg
secrets:
mila_id: mila
kimai:
authentik:
uncomment:
- SSO_ENABLED
- SSO_PROVIDER_URL
- SSO_SAML_URL
- SSO_LOGOUT_URL
- SSO_ADMIN_GROUP_NAME
secret_hooks:
- insert_authentik_certificate
dependency: [authentik]
zammad:
authentik:
uncomment:
- SSO_PROVIDER_DOMAIN
- IDP_SSO_TARGET_URL
- IDP_SLO_SERVICE_URL
initial-hooks:
- local enable_authentik_sso
dependency: [authentik]
nextcloud:
authentik:
uncomment:
- compose.authentik.yml
- AUTHENTIK_USER_PREFIX
- AUTHENTIK_DOMAIN
- SECRET_AUTHENTIK_SECRET_VERSION
- SECRET_AUTHENTIK_ID_VERSION
initial-hooks:
- app set_authentik
shared_secrets:
nextcloud_secret: authentik_secret
nextcloud_id: authentik_id
onlyoffice:
uncomment:
- compose.onlyoffice.yml
- ONLYOFFICE_URL
- SECRET_ONLYOFFICE_JWT_VERSION
initial-hooks:
- app install_onlyoffice
collabora:
uncomment:
- COLLABORA_URL
initial-hooks:
- app install_collabora
onlyoffice:
nextcloud:
uncomment:
- compose.jwt.yml
- SECRET_JWT_SECRET_VERSION
shared_secrets:
onlyoffice_jwt: jwt_secret
outline:
authentik:
env:
OIDC_CLIENT_ID: outline
OIDC_AUTH_URI: https://authentik.example.com/application/o/authorize/
OIDC_TOKEN_URI: https://authentik.example.com/application/o/token/
OIDC_USERINFO_URI: https://authentik.example.com/application/o/userinfo/
OIDC_DISPLAY_NAME: "Authentik"
uncomment:
- compose.oidc.yml
- OIDC_ENABLED
- OIDC_USERNAME_CLAIM
- OIDC_SCOPES
- SECRET_OIDC_CLIENT_SECRET_VERSION
shared_secrets:
outline_secret: oidc_client_secret
wordpress:
authentik:
uncomment:
- compose.authentik.yml
- AUTHENTIK_DOMAIN
- SECRET_AUTHENTIK_SECRET_VERSION
- SECRET_AUTHENTIK_ID_VERSION
- LOGIN_TYPE
initial-hooks:
- app set_authentik
shared_secrets:
wordpress_secret: authentik_secret
wordpress_id: authentik_id
vikunja:
authentik:
env:
OAUTH_NAME: authentik
OAUTH_URL: https://authentik.example.com/application/o/vikunja/
OAUTH_LOGOUT_URL: https://authentik.example.com/application/o/vikunja/end-session/
# TODO: set CLIENT_ID as secret
OAUTH_CLIENT_ID: vikunja
uncomment:
- compose.oauth.yml
- OAUTH_ENABLED
- SECRET_OAUTH_SECRET_VERSION
shared_secrets:
#vikunja_id: oauth_id
vikunja_secret: oauth_secret
matrix-synapse:
authentik:
env:
KEYCLOAK_ID: authentik
KEYCLOAK_NAME: sso
KEYCLOAK_URL: https://authentik.example.com/application/o/matrix/
# TODO: correct client domain?
KEYCLOAK_CLIENT_DOMAIN: https://element-web.example.com
KEYCLOAK_ALLOW_EXISTING_USERS: "true"
# TODO: set CLIENT_ID as secret
KEYCLOAK_CLIENT_ID: matrix
uncomment:
- compose.keycloak.yml
- KEYCLOAK_ENABLED
- KEYCLOAK_CLIENT_ID
- SECRET_KEYCLOAK_CLIENT_SECRET_VERSION
shared_secrets:
matrix_secret: keycloak_client_secret
dependency: [authentik]
traefik:
matrix-synapse:
uncomment:
- compose.matrix.yml
- MATRIX_FEDERATION_ENABLED
rallly:
authentik:
env:
OIDC_NAME: "Authentik"
OIDC_DISCOVERY_URL: "https://authentik.example.com/application/o/rallly/.well-known/openid-configuration"
OIDC_ISSUER_URL: "https://authentik.example.com/application/o/rallly/"
OIDC_CLIENT_ID: rallly
uncomment:
- compose.oidc.yml
- SECRET_OIDC_CLIENT_SECRET_VERSION
- OIDC_ENABLED
shared_secrets:
rallly_secret: oidc_client_secret
wekan:
authentik:
env:
OAUTH2_ENABLED: "true"
OAUTH2_SERVER_URL: https://authentik.example.com
# TODO: set CLIENT_ID as secret
OAUTH2_CLIENT_ID: wekan
uncomment:
- OAUTH2_LOGIN_STYLE
- OAUTH2_AUTH_ENDPOINT
- OAUTH2_USERINFO_ENDPOINT
- OAUTH2_TOKEN_ENDPOINT
- OAUTH2_REQUEST_PERMISSIONS
- OAUTH2_ID_MAP
- OAUTH2_USERNAME_MAP
- OAUTH2_FULLNAME_MAP
- OAUTH2_EMAIL_MAP
- PROPAGATE_OIDC_DATA
- OIDC_REDIRECTION_ENABLED
shared_secrets:
wekan_secret: oauth2_secret
hedgedoc:
authentik:
env:
CMD_OAUTH2_USER_PROFILE_URL: https://authentik.example.com/application/o/userinfo/
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR: preferred_username
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR: name
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR: email
CMD_OAUTH2_TOKEN_URL: https://authentik.example.com/application/o/token/
CMD_OAUTH2_AUTHORIZATION_URL: https://authentik.example.com/application/o/authorize/
# TODO: set CLIENT_ID as secret
CMD_OAUTH2_CLIENT_ID: hedgedoc
CMD_OAUTH2_PROVIDERNAME: Authentik
uncomment:
- compose.oauth.yml
- SECRET_OAUTH_KEY_VERSION
shared_secrets:
hedgedoc_secret: oauth_key
dependency: [authentik]
mila:
authentik:
env:
OIDC_CLIENT_ID: mila
OIDC_BASE_URL: https://authentik.example.com/application/o/mila
OIDC_REDIRECT_URI: https://mila.example.com/auth/user/oidc/callback
OIDC_ADMIN_GROUP_NAME: mv_admin
OIDC_GROUPS_CLAIM: groups
OIDC_ONLY: "true"
uncomment:
- compose.oidc.yml
- SECRET_OIDC_CLIENT_SECRET_VERSION
shared_secrets:
mila_secret: oidc_client_secret
monitoring-ng:
authentik:
env:
GF_SERVER_ROOT_URL: https://monitoring-ng.example.com
OIDC_CLIENT_ID: monitoring
OIDC_AUTH_URL: https://authentik.example.com/application/o/authorize/
OIDC_API_URL: https://authentik.example.com/application/o/userinfo/
OIDC_TOKEN_URL: https://authentik.example.com/application/o/token/
uncomment:
- compose.prometheus.yml
- PROMETHEUS_RETENTION_TIME
- compose.loki.yml
- LOKI_RETENTION_PERIOD
- LOKI_STORAGE_FILESYSTEM
- compose.grafana.yml
- OIDC_ENABLED
- SECRET_GRAFANA_ADMIN_PASSWORD_VERSION
- SECRET_GRAFANA_OIDC_CLIENT_SECRET_VERSION
- SECRET_GRAFANA_SMTP_PASSWORD_VERSION
shared_secrets:
monitoring_secret: grafana_oidc_client_secret
dependency: [authentik]
+395
View File
@@ -0,0 +1,395 @@
# Integrations, converted from the combine.yml that alakazam used to ship.
# A set applies as soon as every app of its 'apply-when' is part of an instance.
# Copy this file into your alakazam root path, next to alaka-versions.yml.
integrate-authentik-nextcloud:
apply-when:
- authentik
- nextcloud
authentik:
uncomment:
- compose.nextcloud.yml
- NEXTCLOUD_DOMAIN
- SECRET_NEXTCLOUD_ID_VERSION
- SECRET_NEXTCLOUD_SECRET_VERSION
- nextcloud.png
nextcloud:
uncomment:
- compose.authentik.yml
- AUTHENTIK_USER_PREFIX
- AUTHENTIK_DOMAIN
- SECRET_AUTHENTIK_SECRET_VERSION
- SECRET_AUTHENTIK_ID_VERSION
initial-hooks:
- app set_authentik
shared_secrets:
authentik:
nextcloud_secret: authentik_secret
nextcloud_id: authentik_id
integrate-authentik-wordpress:
apply-when:
- authentik
- wordpress
authentik:
uncomment:
- compose.wordpress.yml
- WORDPRESS_DOMAIN
- WORDPRESS_GROUP
- SECRET_WORDPRESS_ID_VERSION
- SECRET_WORDPRESS_SECRET_VERSION
- wordpress.png
wordpress:
uncomment:
- compose.authentik.yml
- AUTHENTIK_DOMAIN
- SECRET_AUTHENTIK_SECRET_VERSION
- SECRET_AUTHENTIK_ID_VERSION
- LOGIN_TYPE
initial-hooks:
- app set_authentik
shared_secrets:
authentik:
wordpress_secret: authentik_secret
wordpress_id: authentik_id
integrate-authentik-matrix-synapse:
apply-when:
- authentik
- matrix-synapse
authentik:
uncomment:
- compose.matrix.yml
- ELEMENT_DOMAIN
- MATRIX_DOMAIN
- SECRET_MATRIX_ID_VERSION
- SECRET_MATRIX_SECRET_VERSION
- matrix.svg
secrets:
matrix_id: matrix
matrix-synapse:
env:
KEYCLOAK_ID: authentik
KEYCLOAK_NAME: sso
KEYCLOAK_URL: https://authentik.example.com/application/o/matrix/
# TODO: correct client domain?
KEYCLOAK_CLIENT_DOMAIN: https://element-web.example.com
KEYCLOAK_ALLOW_EXISTING_USERS: "true"
# TODO: set CLIENT_ID as secret
KEYCLOAK_CLIENT_ID: matrix
uncomment:
- compose.keycloak.yml
- KEYCLOAK_ENABLED
- KEYCLOAK_CLIENT_ID
- SECRET_KEYCLOAK_CLIENT_SECRET_VERSION
shared_secrets:
authentik:
matrix_secret: keycloak_client_secret
dependency: [authentik]
integrate-authentik-wekan:
apply-when:
- authentik
- wekan
authentik:
uncomment:
- compose.wekan.yml
- WEKAN_DOMAIN
- SECRET_WEKAN_ID_VERSION
- SECRET_WEKAN_SECRET_VERSION
- wekan.png
secrets:
wekan_id: wekan
wekan:
env:
OAUTH2_ENABLED: "true"
OAUTH2_SERVER_URL: https://authentik.example.com
# TODO: set CLIENT_ID as secret
OAUTH2_CLIENT_ID: wekan
uncomment:
- OAUTH2_LOGIN_STYLE
- OAUTH2_AUTH_ENDPOINT
- OAUTH2_USERINFO_ENDPOINT
- OAUTH2_TOKEN_ENDPOINT
- OAUTH2_REQUEST_PERMISSIONS
- OAUTH2_ID_MAP
- OAUTH2_USERNAME_MAP
- OAUTH2_FULLNAME_MAP
- OAUTH2_EMAIL_MAP
- PROPAGATE_OIDC_DATA
- OIDC_REDIRECTION_ENABLED
shared_secrets:
authentik:
wekan_secret: oauth2_secret
integrate-authentik-vikunja:
apply-when:
- authentik
- vikunja
authentik:
uncomment:
- compose.vikunja.yml
- VIKUNJA_DOMAIN
- SECRET_VIKUNJA_ID_VERSION
- SECRET_VIKUNJA_SECRET_VERSION
- vikunja.svg
secrets:
vikunja_id: vikunja
vikunja:
env:
OAUTH_NAME: authentik
OAUTH_URL: https://authentik.example.com/application/o/vikunja/
OAUTH_LOGOUT_URL: https://authentik.example.com/application/o/vikunja/end-session/
# TODO: set CLIENT_ID as secret
OAUTH_CLIENT_ID: vikunja
uncomment:
- compose.oauth.yml
- OAUTH_ENABLED
- SECRET_OAUTH_SECRET_VERSION
shared_secrets:
#vikunja_id: oauth_id
authentik:
vikunja_secret: oauth_secret
integrate-authentik-kimai:
apply-when:
- authentik
- kimai
authentik:
uncomment:
- compose.kimai.yml
- KIMAI_DOMAIN
- SECRET_KIMAI_ID_VERSION
- SECRET_KIMAI_SECRET_VERSION
- kimai_logo.png
- KIMAI_GROUP
kimai:
uncomment:
- SSO_ENABLED
- SSO_PROVIDER_URL
- SSO_SAML_URL
- SSO_LOGOUT_URL
- SSO_ADMIN_GROUP_NAME
secret_hooks:
- insert_authentik_certificate
dependency: [authentik]
integrate-authentik-zammad:
apply-when:
- authentik
- zammad
authentik:
uncomment:
- compose.zammad.yml
- ZAMMAD_DOMAIN
- zammad.svg
zammad:
uncomment:
- SSO_PROVIDER_DOMAIN
- IDP_SSO_TARGET_URL
- IDP_SLO_SERVICE_URL
initial-hooks:
- local enable_authentik_sso
dependency: [authentik]
integrate-authentik-monitoring-ng:
apply-when:
- authentik
- monitoring-ng
authentik:
uncomment:
- compose.monitoring.yml
- MONITORING_DOMAIN
- SECRET_MONITORING_ID_VERSION
- SECRET_MONITORING_SECRET_VERSION
- monitoring.svg
secrets:
monitoring_id: monitoring
monitoring-ng:
env:
GF_SERVER_ROOT_URL: https://monitoring-ng.example.com
OIDC_CLIENT_ID: monitoring
OIDC_AUTH_URL: https://authentik.example.com/application/o/authorize/
OIDC_API_URL: https://authentik.example.com/application/o/userinfo/
OIDC_TOKEN_URL: https://authentik.example.com/application/o/token/
uncomment:
- compose.prometheus.yml
- PROMETHEUS_RETENTION_TIME
- compose.loki.yml
- LOKI_RETENTION_PERIOD
- LOKI_STORAGE_FILESYSTEM
- compose.grafana.yml
- OIDC_ENABLED
- SECRET_GRAFANA_ADMIN_PASSWORD_VERSION
- SECRET_GRAFANA_OIDC_CLIENT_SECRET_VERSION
- SECRET_GRAFANA_SMTP_PASSWORD_VERSION
shared_secrets:
authentik:
monitoring_secret: grafana_oidc_client_secret
dependency: [authentik]
integrate-authentik-outline:
apply-when:
- authentik
- outline
authentik:
uncomment:
- compose.outline.yml
- OUTLINE_DOMAIN
- SECRET_OUTLINE_ID_VERSION
- SECRET_OUTLINE_SECRET_VERSION
- outline.png
secrets:
outline_id: outline
outline:
env:
OIDC_CLIENT_ID: outline
OIDC_AUTH_URI: https://authentik.example.com/application/o/authorize/
OIDC_TOKEN_URI: https://authentik.example.com/application/o/token/
OIDC_USERINFO_URI: https://authentik.example.com/application/o/userinfo/
OIDC_DISPLAY_NAME: "Authentik"
uncomment:
- compose.oidc.yml
- OIDC_ENABLED
- OIDC_USERNAME_CLAIM
- OIDC_SCOPES
- SECRET_OIDC_CLIENT_SECRET_VERSION
shared_secrets:
authentik:
outline_secret: oidc_client_secret
integrate-authentik-rallly:
apply-when:
- authentik
- rallly
authentik:
uncomment:
- compose.rallly.yml
- RALLLY_DOMAIN
- SECRET_RALLLY_ID_VERSION
- SECRET_RALLLY_SECRET_VERSION
- rallly.png
secrets:
rallly_id: rallly
rallly:
env:
OIDC_NAME: "Authentik"
OIDC_DISCOVERY_URL: "https://authentik.example.com/application/o/rallly/.well-known/openid-configuration"
OIDC_ISSUER_URL: "https://authentik.example.com/application/o/rallly/"
OIDC_CLIENT_ID: rallly
uncomment:
- compose.oidc.yml
- SECRET_OIDC_CLIENT_SECRET_VERSION
- OIDC_ENABLED
shared_secrets:
authentik:
rallly_secret: oidc_client_secret
integrate-authentik-hedgedoc:
apply-when:
- authentik
- hedgedoc
authentik:
uncomment:
- compose.hedgedoc.yml
- HEDGEDOC_DOMAIN
- SECRET_HEDGEDOC_ID_VERSION
- SECRET_HEDGEDOC_SECRET_VERSION
- hedgedoc.png
secrets:
hedgedoc_id: hedgedoc
hedgedoc:
env:
CMD_OAUTH2_USER_PROFILE_URL: https://authentik.example.com/application/o/userinfo/
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR: preferred_username
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR: name
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR: email
CMD_OAUTH2_TOKEN_URL: https://authentik.example.com/application/o/token/
CMD_OAUTH2_AUTHORIZATION_URL: https://authentik.example.com/application/o/authorize/
# TODO: set CLIENT_ID as secret
CMD_OAUTH2_CLIENT_ID: hedgedoc
CMD_OAUTH2_PROVIDERNAME: Authentik
uncomment:
- compose.oauth.yml
- SECRET_OAUTH_KEY_VERSION
shared_secrets:
authentik:
hedgedoc_secret: oauth_key
dependency: [authentik]
integrate-authentik-pretix:
apply-when:
- authentik
- pretix
authentik:
env:
APPLICATIONS:
Pretix:
url: https://pretix.example.com/control/
group:
EXTRA_ICONS:
Pretix: ~/.abra/recipes/authentik/icons/pretix.svg
integrate-authentik-vaultwarden:
apply-when:
- authentik
- vaultwarden
authentik:
env:
APPLICATIONS:
Vaultwarden:
url: https://vaultwarden.example.com/
group:
EXTRA_ICONS:
Vaultwarden: ~/.abra/recipes/authentik/icons/vaultwarden.svg
integrate-authentik-mila:
apply-when:
- authentik
- mila
authentik:
uncomment:
- compose.mila.yml
- MILA_DOMAIN
- MILA_GROUP
- SECRET_MILA_ID_VERSION
- SECRET_MILA_SECRET_VERSION
- mila.svg
secrets:
mila_id: mila
mila:
env:
OIDC_CLIENT_ID: mila
OIDC_BASE_URL: https://authentik.example.com/application/o/mila
OIDC_REDIRECT_URI: https://mila.example.com/auth/user/oidc/callback
OIDC_ADMIN_GROUP_NAME: mv_admin
OIDC_GROUPS_CLAIM: groups
OIDC_ONLY: "true"
uncomment:
- compose.oidc.yml
- SECRET_OIDC_CLIENT_SECRET_VERSION
shared_secrets:
authentik:
mila_secret: oidc_client_secret
integrate-nextcloud-onlyoffice:
apply-when:
- nextcloud
- onlyoffice
nextcloud:
uncomment:
- compose.onlyoffice.yml
- ONLYOFFICE_URL
- SECRET_ONLYOFFICE_JWT_VERSION
initial-hooks:
- app install_onlyoffice
onlyoffice:
uncomment:
- compose.jwt.yml
- SECRET_JWT_SECRET_VERSION
shared_secrets:
nextcloud:
onlyoffice_jwt: jwt_secret
integrate-collabora-nextcloud:
apply-when:
- collabora
- nextcloud
nextcloud:
uncomment:
- COLLABORA_URL
initial-hooks:
- app install_collabora
integrate-matrix-synapse-traefik:
apply-when:
- matrix-synapse
- traefik
traefik:
uncomment:
- compose.matrix.yml
- MATRIX_FEDERATION_ENABLED
+166
View File
@@ -0,0 +1,166 @@
"""Tests for config-sets, in particular the ones that apply by themselves."""
import logging
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import alakazam
from alakazam import get_config_set_app_configs, merge_instance_configs, read_config_sets
INTEGRATION = {
"integrate-authentik-nextcloud": {
"apply-when": ["authentik", "nextcloud"],
"authentik": {"uncomment": ["compose.nextcloud.yml"]},
"nextcloud": {"uncomment": ["compose.authentik.yml"],
"shared_secrets": {"authentik": {"nextcloud_secret": "authentik_secret"}}},
}
}
def apply(config_sets, instance_apps, active=None):
"""The effective configuration, with the automatic sets below the explicit ones."""
automatic, explicit = get_config_set_app_configs(active or {}, config_sets, instance_apps)
return alakazam.merge_dict(automatic, explicit)
class TestApplyWhen:
def test_it_applies_when_every_app_is_there(self):
result = apply(INTEGRATION, ["authentik", "nextcloud", "traefik"])
assert result["authentik"]["uncomment"] == ["compose.nextcloud.yml"]
assert result["nextcloud"]["shared_secrets"] == {"authentik": {"nextcloud_secret": "authentik_secret"}}
def test_it_stays_out_when_one_app_is_missing(self):
assert apply(INTEGRATION, ["authentik", "traefik"]) == {}
def test_apply_when_is_not_passed_on_as_an_app(self):
"""It is a condition, not configuration, and would end up in an .env otherwise."""
assert "apply-when" not in apply(INTEGRATION, ["authentik", "nextcloud"])
def test_an_instance_can_switch_it_off(self):
assert apply(INTEGRATION, ["authentik", "nextcloud"],
active={"integrate-authentik-nextcloud": False}) == {}
def test_a_one_sided_integration_configures_only_one_app(self):
"""4 of the 17 pairs in the shipped combine.yml were one-sided."""
sets = {"integrate-matrix-synapse-traefik": {
"apply-when": ["matrix-synapse", "traefik"],
"traefik": {"uncomment": ["compose.matrix.yml"]}}}
assert list(apply(sets, ["matrix-synapse", "traefik"])) == ["traefik"]
assert apply(sets, ["traefik"]) == {}
class TestExplicitSets:
SET = {"bbb": {"authentik": {"env": {"A": "b"}}}}
def test_an_enabled_set_applies(self):
assert apply(self.SET, ["authentik"], active={"bbb": True})["authentik"]["env"] == {"A": "b"}
def test_a_set_that_is_not_enabled_does_not_apply(self):
assert apply(self.SET, ["authentik"]) == {}
def test_an_unknown_name_is_reported(self, caplog):
with caplog.at_level(logging.WARNING):
apply(self.SET, ["authentik"], active={"typo": True})
assert "'typo' is enabled but not defined" in caplog.text
def test_an_explicit_set_refines_an_automatic_one(self):
"""Automatic integrations are the base, an explicit set may still override them."""
sets = dict(INTEGRATION)
sets["override"] = {"authentik": {"env": {"SOURCE": "explicit"}}}
result = apply(sets, ["authentik", "nextcloud"], active={"override": True})
assert result["authentik"]["env"] == {"SOURCE": "explicit"}
assert result["authentik"]["uncomment"] == ["compose.nextcloud.yml"]
class TestReadConfigSets:
def write(self, root, name, text):
(root / name).write_text(text)
def test_the_base_file_is_read(self, tmp_path):
self.write(tmp_path, "config-sets.yml", "bbb:\n authentik:\n env:\n A: b\n")
assert read_config_sets(tmp_path)["bbb"]["authentik"]["env"] == {"A": "b"}
def test_further_files_are_merged(self, tmp_path):
self.write(tmp_path, "config-sets.yml", "bbb:\n authentik: {}\n")
self.write(tmp_path, "config-sets-authentik.yml", "integrate:\n apply-when: [a, b]\n")
assert sorted(read_config_sets(tmp_path)) == ["bbb", "integrate"]
def test_the_base_file_is_merged_first(self, tmp_path):
self.write(tmp_path, "config-sets.yml", "bbb:\n authentik:\n env:\n A: base\n")
self.write(tmp_path, "config-sets-later.yml", "bbb:\n authentik:\n env:\n A: later\n")
assert read_config_sets(tmp_path)["bbb"]["authentik"]["env"]["A"] == "later"
def test_a_root_without_config_sets(self, tmp_path):
assert read_config_sets(tmp_path) == {}
def test_a_key_defined_twice_is_reported(self, tmp_path, caplog):
self.write(tmp_path, "config-sets.yml", "bbb:\n authentik:\n env:\n A: base\n")
self.write(tmp_path, "config-sets-later.yml", "bbb:\n authentik:\n env:\n A: later\n")
with caplog.at_level(logging.WARNING):
read_config_sets(tmp_path)
assert "bbb.authentik.env.A" in caplog.text
class TestIntegrationReachesTheMergedConfig:
"""The instance config is what every command reads, the integration has to arrive there."""
def merge(self, apps):
return merge_instance_configs({}, "example.com", {app: None for app in apps}, INTEGRATION)
def test_both_apps_get_their_side(self):
merged = self.merge(["authentik", "nextcloud"])
assert merged["authentik"]["uncomment"] == ["compose.nextcloud.yml"]
assert merged["nextcloud"]["uncomment"] == ["compose.authentik.yml"]
def test_a_lone_app_gets_nothing(self):
assert "uncomment" not in self.merge(["authentik"])["authentik"]
class TestPrecedence:
"""An integration is the lowest layer, as combine.yml was before it."""
def test_the_group_configuration_beats_an_integration(self):
group = {"nextcloud": {"env": {"SOURCE": "group"}}}
sets = {"integrate": {"apply-when": ["authentik", "nextcloud"],
"nextcloud": {"env": {"SOURCE": "integration"}}}}
merged = merge_instance_configs(group, "example.com", {"authentik": None, "nextcloud": None}, sets)
assert merged["nextcloud"]["env"]["SOURCE"] == "group"
def test_the_instance_beats_everything(self):
group = {"nextcloud": {"env": {"SOURCE": "group"}}}
sets = {"integrate": {"apply-when": ["authentik", "nextcloud"],
"nextcloud": {"env": {"SOURCE": "integration"}}}}
merged = merge_instance_configs(
group, "example.com", {"authentik": None, "nextcloud": {"env": {"SOURCE": "instance"}}}, sets)
assert merged["nextcloud"]["env"]["SOURCE"] == "instance"
def test_an_integration_still_adds_what_nobody_else_sets(self):
group = {"nextcloud": {"env": {"OTHER": "group"}}}
sets = {"integrate": {"apply-when": ["authentik", "nextcloud"],
"nextcloud": {"env": {"SOURCE": "integration"}}}}
merged = merge_instance_configs(group, "example.com", {"authentik": None, "nextcloud": None}, sets)
assert merged["nextcloud"]["env"] == {"SOURCE": "integration", "OTHER": "group"}
class TestHookOrder:
"""An integration adds to an app that already configures itself, and must come after it."""
SETS = {"integrate-authentik-wordpress": {
"apply-when": ["authentik", "wordpress"],
"wordpress": {"initial-hooks": ["app set_authentik"]}}}
def test_the_integration_hook_runs_last(self):
"""set_authentik configures SSO in a WordPress that core_install has to create first."""
group = {"wordpress": {"initial-hooks": ["app core_install", "app enable_auto_updates"]}}
merged = merge_instance_configs(group, "example.com", {"authentik": None, "wordpress": None}, self.SETS)
assert merged["wordpress"]["initial-hooks"] == [
"app core_install", "app enable_auto_updates", "app set_authentik"]
def test_an_instance_hook_also_comes_first(self):
instance = {"authentik": None, "wordpress": {"initial-hooks": ["app from_instance"]}}
merged = merge_instance_configs({}, "example.com", instance, self.SETS)
assert merged["wordpress"]["initial-hooks"] == ["app from_instance", "app set_authentik"]
+4 -2
View File
@@ -109,9 +109,11 @@ class TestRelevantConfigPaths:
assert config_root / "group" / "alaka-versions.yml" in paths
assert config_root / "group" / "example.com.yml" in paths
def test_covers_the_connection_configuration(self, config_root):
def test_covers_every_config_set_file(self, config_root):
(config_root / "config-sets-authentik.yml").write_text("integrate:\n apply-when: [a]\n")
paths = get_relevant_config_paths(config_root, config_root / "group", [])
assert alakazam.Path(alakazam.COMBINE_PATH) in paths
assert config_root / "config-sets.yml" in paths
assert config_root / "config-sets-authentik.yml" in paths
def test_a_single_instance_file_pulls_in_its_ancestors(self, config_root):
paths = get_relevant_config_paths(config_root, config_root / "group" / "example.com.yml", [])