mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-06-24 19:25:32 +02:00
In between commit domain restruturing
This commit is contained in:
parent
cc3f5d75ea
commit
37dcc5f74e
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
||||
ROLES_DIR := ./roles
|
||||
APPLICATIONS_OUT := ./group_vars/all/11_applications.yml
|
||||
APPLICATIONS_OUT := ./group_vars/all/03_applications.yml
|
||||
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
|
||||
INCLUDES_OUT := ./tasks/include-docker-roles.yml
|
||||
INCLUDES_SCRIPT := ./cli/generate-role-includes.py
|
||||
|
@ -1,2 +1,3 @@
|
||||
[defaults]
|
||||
lookup_plugins = ./lookup_plugins
|
||||
lookup_plugins = ./lookup_plugins
|
||||
filter_plugins = ./filter_plugins
|
@ -15,7 +15,7 @@ def load_yaml_file(path):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate defaults_applications YAML from docker roles.")
|
||||
parser.add_argument("--roles-dir", default="roles", help="Path to the roles directory (default: roles)")
|
||||
parser.add_argument("--output-file", default="group_vars/all/11_applications.yml", help="Path to output YAML file")
|
||||
parser.add_argument("--output-file", default="group_vars/all/03_applications.yml", help="Path to output YAML file")
|
||||
|
||||
args = parser.parse_args()
|
||||
cwd = Path.cwd()
|
||||
|
111
docs/guides/developer/Ansible_Directory_Guide.md
Normal file
111
docs/guides/developer/Ansible_Directory_Guide.md
Normal file
@ -0,0 +1,111 @@
|
||||
## 📖 CyMaIS.Cloud Ansible & Python Directory Guide
|
||||
|
||||
This document provides a **decision matrix** for when to use each default Ansible plugin and module directory in the context of **CyMaIS.Cloud development** with Ansible and Python. It links to official docs, explains use-cases, and points back to our conversation.
|
||||
|
||||
---
|
||||
|
||||
### 🔗 Links & References
|
||||
|
||||
* Official Ansible Plugin Guide: [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html)
|
||||
* Official Ansible Module Guide: [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html)
|
||||
* This conversation: [Link to this conversation](https://chat.openai.com/)
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ Repo Layout & Default Directories
|
||||
|
||||
```plaintext
|
||||
ansible-repo/
|
||||
├── library/ # 📦 Custom Ansible modules
|
||||
├── filter_plugins/ # 🔍 Custom Jinja2 filters
|
||||
├── lookup_plugins/ # 👉 Custom lookup plugins
|
||||
├── module_utils/ # 🛠️ Shared Python helpers for modules
|
||||
├── action_plugins/ # ⚙️ Task-level orchestration logic
|
||||
├── callback_plugins/ # 📣 Event callbacks (logging, notifications)
|
||||
├── inventory_plugins/ # 🌐 Dynamic inventory sources
|
||||
├── strategy_plugins/ # 🧠 Task execution strategies
|
||||
└── ... # Other plugin dirs (connection, cache, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Decision Matrix: Which Folder for What?
|
||||
|
||||
| Folder | Type | Use-Case | Example (CyMaIS.Cloud) | Emoji |
|
||||
| -------------------- | -------------------- | ---------------------------------------- | ----------------------------------------------------- | ----- |
|
||||
| `library/` | **Module** | Write idempotent actions | `cloud_network.py`: manage VPCs, subnets | 📦 |
|
||||
| `filter_plugins/` | **Filter plugin** | Jinja2 data transforms in templates/vars | `to_camel_case.py`: convert keys for API calls | 🔍 |
|
||||
| `lookup_plugins/` | **Lookup plugin** | Fetch external/secure data at runtime | `vault_lookup.py`: pull secrets from CyMaIS Vault | 👉 |
|
||||
| `module_utils/` | **Utility library** | Shared Python code for modules | `cymais_client.py`: common API client base class | 🛠️ |
|
||||
| `action_plugins/` | **Action plugin** | Complex task orchestration wrappers | `deploy_stack.py`: sequence Terraform + Ansible steps | ⚙️ |
|
||||
| `callback_plugins/` | **Callback plugin** | Customize log/report behavior | `notify_slack.py`: send playbook status to Slack | 📣 |
|
||||
| `inventory_plugins/` | **Inventory plugin** | Dynamic host/group sources | `azure_inventory.py`: list hosts from Azure tags | 🌐 |
|
||||
| `strategy_plugins/` | **Strategy plugin** | Control task execution order/parallelism | `rolling_batch.py`: phased rollout of VMs | 🧠 |
|
||||
|
||||
---
|
||||
|
||||
### 📝 Detailed Guidance
|
||||
|
||||
1. **library/** 📦
|
||||
|
||||
* **When?** Implement **one-off, idempotent actions** (create/delete cloud resources).
|
||||
* **Why?** Modules under `library/` are first in search path for `ansible` modules.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html)
|
||||
|
||||
2. **filter\_plugins/** 🔍
|
||||
|
||||
* **When?** You need **data manipulation** (lists, strings, dicts) inside Jinja2.
|
||||
* **Why?** Extends `|` filters in templates and variable declarations.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#filter-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#filter-plugins)
|
||||
|
||||
3. **lookup\_plugins/** 👉
|
||||
|
||||
* **When?** You must **retrieve secret/external data** during playbook compile/runtime.
|
||||
* **Why?** Lookup plugins run before tasks, enabling dynamic variable resolution.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#lookup-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#lookup-plugins)
|
||||
|
||||
4. **module\_utils/** 🛠️
|
||||
|
||||
* **When?** Multiple modules share **common Python code** (HTTP clients, validation).
|
||||
* **Why?** Avoid code duplication; modules import these utilities.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html#module-utils](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html#module-utils)
|
||||
|
||||
5. **action\_plugins/** ⚙️
|
||||
|
||||
* **When?** You need to **wrap or extend** module behavior at task invocation time.
|
||||
* **Why?** Provides hooks before/after module execution.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#action-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#action-plugins)
|
||||
|
||||
6. **callback\_plugins/** 📣
|
||||
|
||||
* **When?** You want **custom event handlers** (logging, progress, notifications).
|
||||
* **Why?** Receive play/task events for custom output.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#callback-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#callback-plugins)
|
||||
|
||||
7. **inventory\_plugins/** 🌐
|
||||
|
||||
* **When?** Hosts/groups come from **dynamic sources** (cloud APIs, databases).
|
||||
* **Why?** Replace static `inventory.ini` with code-driven inventories.
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#inventory-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#inventory-plugins)
|
||||
|
||||
8. **strategy\_plugins/** 🧠
|
||||
|
||||
* **When?** You need to **customize execution strategy** (parallelism, ordering).
|
||||
* **Why?** Override default `linear` strategy (e.g., `free`, custom batches).
|
||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#strategy-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#strategy-plugins)
|
||||
|
||||
---
|
||||
|
||||
### 🚀 CyMaIS.Cloud Best Practices
|
||||
|
||||
* **Organize modules** by service under `library/cloud/` (e.g., `vm`, `network`, `storage`).
|
||||
* **Shared client code** in `module_utils/cymais/` for authentication, request handling.
|
||||
* **Secrets lookup** via `lookup_plugins/vault_lookup.py` pointing to CyMaIS Vault.
|
||||
* **Filters** to normalize data formats from cloud APIs (e.g., `snake_to_camel`).
|
||||
* **Callbacks** to stream playbook results into CyMaIS Monitoring.
|
||||
|
||||
Use this matrix as your **single source of truth** when extending Ansible for CyMaIS.Cloud! 👍
|
||||
|
||||
---
|
||||
|
||||
This matrix was created with the help of ChatGPT 🤖—see our conversation [here](https://chatgpt.com/canvas/shared/682b1a62d6dc819184ecdc696c51290a).
|
@ -1,6 +1,6 @@
|
||||
def is_feature_enabled(applications, feature: str, application_id: str) -> bool:
|
||||
def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool:
|
||||
"""
|
||||
Check if a generic feature is enabled for the given application.
|
||||
Return True if applications[application_id].features[feature] is truthy.
|
||||
"""
|
||||
app = applications.get(application_id, {})
|
||||
return bool(app.get('features', {}).get(feature, False))
|
||||
@ -31,4 +31,4 @@ class FilterModule(object):
|
||||
return {
|
||||
'is_feature_enabled': is_feature_enabled,
|
||||
'get_docker_compose': get_docker_compose,
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,10 @@ class FilterModule(object):
|
||||
'build_csp_header': self.build_csp_header,
|
||||
}
|
||||
|
||||
def is_feature_enabled(self, applications, feature: str, application_id: str) -> bool:
|
||||
@staticmethod
|
||||
def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool:
|
||||
"""
|
||||
Check if a generic feature is enabled for the given application.
|
||||
Return True if applications[application_id].features[feature] is truthy.
|
||||
"""
|
||||
app = applications.get(application_id, {})
|
||||
return bool(app.get('features', {}).get(feature, False))
|
||||
@ -99,7 +100,7 @@ class FilterModule(object):
|
||||
|
||||
for directive in directives:
|
||||
tokens = ["'self'"]
|
||||
|
||||
|
||||
# unsafe-eval / unsafe-inline flags
|
||||
flags = self.get_csp_flags(applications, application_id, directive)
|
||||
tokens += flags
|
||||
|
@ -3,16 +3,101 @@ from ansible.errors import AnsibleFilterError
|
||||
|
||||
class FilterModule(object):
|
||||
"""
|
||||
Custom Ansible filter plugin:
|
||||
- generate_all_domains: Flatten, dedupe, sort domains with optional www prefixes
|
||||
- generate_base_sld_domains: Extract unique sld.tld domains from values and redirect sources
|
||||
Ansible Filter Plugin for Domain Processing
|
||||
|
||||
This plugin provides filters to manage and transform domain configurations for applications:
|
||||
|
||||
- generate_all_domains(domains_dict, include_www=True):
|
||||
Flattens nested domain values (string, list, or dict), optionally adds 'www.' prefixes,
|
||||
removes duplicates, and returns a sorted list of unique domains.
|
||||
|
||||
- generate_base_sld_domains(domains_dict, redirect_mappings):
|
||||
Flattens domains and redirect mappings, extracts second-level + top-level domains (SLDs),
|
||||
deduplicates, and returns a sorted list of base domains.
|
||||
|
||||
- canonical_domains_map(apps, primary_domain):
|
||||
Builds a mapping of application IDs to their canonical domains using
|
||||
DomainUtils.canonical_list, enforcing uniqueness and detecting conflicts.
|
||||
|
||||
- alias_domains_map(apps, primary_domain):
|
||||
Generates alias domains for each application via DomainUtils.alias_list,
|
||||
based on their canonical domains and provided configurations.
|
||||
"""
|
||||
|
||||
def filters(self):
|
||||
return {
|
||||
'generate_all_domains': self.generate_all_domains,
|
||||
'generate_base_sld_domains': self.generate_base_sld_domains,
|
||||
'generate_all_domains': self.generate_all_domains,
|
||||
'generate_base_sld_domains': self.generate_base_sld_domains,
|
||||
'canonical_domains_map': self.canonical_domains_map,
|
||||
'alias_domains_map': self.alias_domains_map,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_entry(domains_cfg, key, app_id):
|
||||
"""
|
||||
Extract list of strings from domains_cfg[key], which may be dict or list.
|
||||
Returns None if key not in domains_cfg.
|
||||
Raises AnsibleFilterError on invalid type or empty/invalid values.
|
||||
"""
|
||||
if key not in domains_cfg:
|
||||
return None
|
||||
entry = domains_cfg[key]
|
||||
if isinstance(entry, dict):
|
||||
values = list(entry.values())
|
||||
elif isinstance(entry, list):
|
||||
values = entry
|
||||
else:
|
||||
raise AnsibleFilterError(
|
||||
f"Unexpected type for 'domains.{key}' in application '{app_id}': {type(entry).__name__}"
|
||||
)
|
||||
for d in values:
|
||||
if not isinstance(d, str) or not d.strip():
|
||||
raise AnsibleFilterError(
|
||||
f"Invalid domain entry in '{key}' for application '{app_id}': {d!r}"
|
||||
)
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def default_domain(app_id, primary_domain):
|
||||
"""
|
||||
Returns the default domain string for an application.
|
||||
"""
|
||||
return f"{app_id}.{primary_domain}"
|
||||
|
||||
@classmethod
|
||||
def canonical_list(cls, domains_cfg, app_id, primary_domain):
|
||||
"""
|
||||
Returns the list of canonical domains: parsed entry or default.
|
||||
"""
|
||||
domains = cls.parse_entry(domains_cfg, 'canonical', app_id)
|
||||
if domains is None:
|
||||
return [cls.default_domain(app_id, primary_domain)]
|
||||
return domains
|
||||
|
||||
@classmethod
|
||||
def alias_list(cls, domains_cfg, app_id, primary_domain, canonical_domains=None):
|
||||
"""
|
||||
Returns the list of alias domains based on:
|
||||
- explicit aliases entry
|
||||
- presence of canonical entry and default not in canonical
|
||||
Always ensures default domain in aliases when appropriate.
|
||||
"""
|
||||
default = cls.default_domain(app_id, primary_domain)
|
||||
aliases = cls.parse_entry(domains_cfg, 'aliases', app_id) or []
|
||||
has_aliases = 'aliases' in domains_cfg
|
||||
has_canonical = 'canonical' in domains_cfg
|
||||
|
||||
if has_aliases:
|
||||
if default not in aliases:
|
||||
aliases.append(default)
|
||||
elif has_canonical:
|
||||
# use provided canonical_domains if given otherwise parse
|
||||
canon = canonical_domains if canonical_domains is not None else cls.parse_entry(domains_cfg, 'canonical', app_id)
|
||||
if default not in (canon or []):
|
||||
aliases.append(default)
|
||||
# else: neither defined -> empty list
|
||||
return aliases
|
||||
|
||||
|
||||
@staticmethod
|
||||
def generate_all_domains(domains_dict, include_www=True):
|
||||
@ -67,3 +152,33 @@ class FilterModule(object):
|
||||
elif isinstance(val, dict):
|
||||
flat.extend(val.values())
|
||||
return flat
|
||||
|
||||
def canonical_domains_map(self, apps, primary_domain):
|
||||
result = {}
|
||||
seen = {}
|
||||
for app_id, app_cfg in apps.items():
|
||||
domains_cfg = app_cfg.get('domains', {}) or {}
|
||||
domains = self.canonical_list(domains_cfg, app_id, primary_domain)
|
||||
for d in domains:
|
||||
if d in seen:
|
||||
raise AnsibleFilterError(
|
||||
f"Domain '{d}' is configured for both '{seen[d]}' and '{app_id}'"
|
||||
)
|
||||
seen[d] = app_id
|
||||
result[app_id] = domains
|
||||
return result
|
||||
|
||||
def alias_domains_map(self, apps, primary_domain):
|
||||
result = {}
|
||||
# wir können die canonical_map vorab holen…
|
||||
canonical_map = self.canonical_domains_map(apps, primary_domain)
|
||||
for app_id, app_cfg in apps.items():
|
||||
domains_cfg = app_cfg.get('domains', {}) or {}
|
||||
aliases = self.alias_list(
|
||||
domains_cfg,
|
||||
app_id,
|
||||
primary_domain,
|
||||
canonical_domains=canonical_map.get(app_id),
|
||||
)
|
||||
result[app_id] = aliases
|
||||
return result
|
||||
|
@ -1,16 +1,9 @@
|
||||
from ansible.errors import AnsibleFilterError
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
class FilterModule(object):
|
||||
"""
|
||||
Custom filters for conditional domain assignments, handling both direct group matches
|
||||
and recursive role dependency resolution.
|
||||
|
||||
Determines if a given application_id (domain_key) should have its domain added by checking:
|
||||
- If domain_key is explicitly listed in group_names, or
|
||||
- If domain_key matches any application_id of roles reachable from active groups via dependencies.
|
||||
"""
|
||||
|
||||
def filters(self):
|
||||
return {
|
||||
@ -39,9 +32,27 @@ class FilterModule(object):
|
||||
result[domain_key] = domain_value
|
||||
return result
|
||||
|
||||
# Setup roles directory path
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
|
||||
# Determine plugin directory based on filter plugin module if available
|
||||
plugin_dir = None
|
||||
for module in sys.modules.values():
|
||||
fm = getattr(module, 'FilterModule', None)
|
||||
if fm is not None:
|
||||
try:
|
||||
# Access staticmethod, compare underlying function
|
||||
if getattr(fm, 'add_domain_if_group') is DomainFilterUtil.add_domain_if_group:
|
||||
plugin_dir = os.path.dirname(module.__file__)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if plugin_dir:
|
||||
# The plugin_dir is the filter_plugins directory; project_root is one level up
|
||||
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
|
||||
else:
|
||||
# Fallback: locate project root relative to this utility file
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
|
||||
|
||||
roles_dir = os.path.join(project_root, 'roles')
|
||||
|
||||
# Collect all roles reachable from the active groups
|
||||
@ -83,4 +94,4 @@ class FilterModule(object):
|
||||
|
||||
return result
|
||||
except Exception as exc:
|
||||
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")
|
||||
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")
|
@ -1,73 +0,0 @@
|
||||
defaults_domains: >-
|
||||
{{ {}
|
||||
| add_domain_if_group('akaunting', 'accounting.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('attendize', 'tickets.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('baserow', 'baserow.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('bigbluebutton', 'meet.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('bluesky', {'web': 'bskyweb.' ~ primary_domain,'api':'bluesky.' ~ primary_domain}, group_names)
|
||||
| add_domain_if_group('discourse', 'forum.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('elk', 'elk.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('espocrm', 'espocrm.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('file_server', 'files.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('friendica', 'friendica.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('funkwhale', 'music.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('gitea', 'git.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('gitlab', 'gitlab.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('html_server', 'html.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('keycloak', 'auth.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('lam', 'lam.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('ldap', 'ldap.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('listmonk', 'newsletter.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('mailu', 'mail.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('mastodon', ['microblog.' ~ primary_domain], group_names)
|
||||
| add_domain_if_group('matomo', 'matomo.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('matrix', {'synapse': 'matrix.' ~ primary_domain, 'element':'element.' ~ primary_domain}, group_names)
|
||||
| add_domain_if_group('moodle', 'academy.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('mediawiki', 'wiki.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('nextcloud', 'cloud.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('openproject', 'project.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('peertube', ['video.' ~ primary_domain], group_names)
|
||||
| add_domain_if_group('pgadmin', 'pgadmin.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('phpmyadmin', 'phpmyadmin.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('phpmyldapadmin', 'phpmyldap.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('pixelfed', 'picture.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('portfolio', primary_domain, group_names)
|
||||
| add_domain_if_group('presentation', 'slides.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('roulette-wheel', 'roulette.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('snipe_it', 'inventory.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('sphinx', 'docs.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('syncope', 'syncope.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('taiga', 'kanban.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('yourls', 's.' ~ primary_domain, group_names)
|
||||
| add_domain_if_group('wordpress', ['blog.' ~ primary_domain], group_names)
|
||||
}}
|
||||
|
||||
defaults_redirect_domain_mappings: >-
|
||||
{{ []
|
||||
| add_redirect_if_group('akaunting', "akaunting." ~ primary_domain, domains.akaunting, group_names)
|
||||
| add_redirect_if_group('bigbluebutton', "bbb." ~ primary_domain, domains.bigbluebutton, group_names)
|
||||
| add_redirect_if_group('discourse', "discourse." ~ primary_domain, domains.discourse, group_names)
|
||||
| add_redirect_if_group('espocrm', "crm." ~ primary_domain, domains.espocrm, group_names)
|
||||
| add_redirect_if_group('funkwhale', "funkwhale." ~ primary_domain, domains.funkwhale, group_names)
|
||||
| add_redirect_if_group('gitea', "gitea." ~ primary_domain, domains.gitea, group_names)
|
||||
| add_redirect_if_group('keycloak', "keycloak." ~ primary_domain, domains.keycloak, group_names)
|
||||
| add_redirect_if_group('lam', domains.ldap, domains.lam, group_names)
|
||||
| add_redirect_if_group('phpmyldapadmin', domains.ldap, domains.phpmyldapadmin,group_names)
|
||||
| add_redirect_if_group('listmonk', "listmonk." ~ primary_domain, domains.listmonk, group_names)
|
||||
| add_redirect_if_group('mailu', "mailu." ~ primary_domain, domains.mailu, group_names)
|
||||
| add_redirect_if_group('mastodon', "mastodon." ~ primary_domain, domains.mastodon[0], group_names)
|
||||
| add_redirect_if_group('moodle', "moodle." ~ primary_domain, domains.moodle, group_names)
|
||||
| add_redirect_if_group('nextcloud', "nextcloud." ~ primary_domain, domains.nextcloud, group_names)
|
||||
| add_redirect_if_group('openproject', "openproject." ~ primary_domain, domains.openproject, group_names)
|
||||
| add_redirect_if_group('peertube', "peertube." ~ primary_domain, domains.peertube[0], group_names)
|
||||
| add_redirect_if_group('pixelfed', "pictures." ~ primary_domain, domains.pixelfed, group_names)
|
||||
| add_redirect_if_group('pixelfed', "pixelfed." ~ primary_domain, domains.pixelfed, group_names)
|
||||
| add_redirect_if_group('yourls', "short." ~ primary_domain, domains.yourls, group_names)
|
||||
| add_redirect_if_group('snipe-it', "snipe-it." ~ primary_domain, domains.snipe_it, group_names)
|
||||
| add_redirect_if_group('taiga', "taiga." ~ primary_domain, domains.taiga, group_names)
|
||||
| add_redirect_if_group('peertube', "videos." ~ primary_domain, domains.peertube[0], group_names)
|
||||
| add_redirect_if_group('wordpress', "wordpress." ~ primary_domain, domains.wordpress[0], group_names)
|
||||
}}
|
||||
|
||||
# Domains which are deprecated and should be cleaned up
|
||||
deprecated_domains: []
|
10
group_vars/all/11_domains.yml
Normal file
10
group_vars/all/11_domains.yml
Normal file
@ -0,0 +1,10 @@
|
||||
defaults_domains: "{{ defaults_applications | canonical_domains_map(primary_domain) }}"
|
||||
|
||||
defaults_redirect_domain_mappings: >-
|
||||
{{ []
|
||||
| add_redirect_if_group('lam', domains.ldap, domains.lam, group_names)
|
||||
| add_redirect_if_group('phpmyldapadmin', domains.ldap, domains.phpmyldapadmin,group_names)
|
||||
}}
|
||||
|
||||
# Domains which are deprecated and should be cleaned up
|
||||
deprecated_domains: []
|
@ -1,3 +1,2 @@
|
||||
colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
|
||||
simpleaudio
|
||||
numpy
|
@ -4,4 +4,6 @@ collections:
|
||||
pacman:
|
||||
- ansible
|
||||
- python-passlib
|
||||
- python-pytest
|
||||
- python-pytest
|
||||
yay:
|
||||
- python-simpleaudio
|
@ -9,4 +9,8 @@ features:
|
||||
central_database: true
|
||||
credentials:
|
||||
# database_password: Needs to be defined in inventory file
|
||||
# setup_admin_password: Needs to be defined in inventory file
|
||||
# setup_admin_password: Needs to be defined in inventory file
|
||||
domains:
|
||||
canonical:
|
||||
- "accounting.{{ primary_domain }}"
|
||||
|
||||
|
@ -7,3 +7,7 @@ features:
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- "tickets.{{ primary_domain }}"
|
@ -2,5 +2,5 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
@ -1,21 +1,17 @@
|
||||
enable_greenlight: "true"
|
||||
setup: false # Set to true in inventory file for initial setup
|
||||
setup: false
|
||||
credentials:
|
||||
# shared_secret: # Needs to be defined in inventory file
|
||||
# etherpad_api_key: # Needs to be defined in inventory file
|
||||
# rails_secret: # Needs to be defined in inventory file
|
||||
# postgresql_secret: # Needs to be defined in inventory file
|
||||
# fsesl_password: # Needs to be defined in inventory file
|
||||
# turn_secret: # Needs to be defined in inventory file
|
||||
database:
|
||||
name: "multiple_databases"
|
||||
username: "postgres2"
|
||||
urls:
|
||||
api: "{{ web_protocol }}://{{domains | get_domain('bigbluebutton')}}/bigbluebutton/" # API Address used by Nextcloud Integration
|
||||
name: "multiple_databases"
|
||||
username: "postgres2"
|
||||
api_suffix: "/bigbluebutton/"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
ldap: false
|
||||
oidc: true
|
||||
central_database: false
|
||||
central_database: false
|
||||
domains:
|
||||
canonical:
|
||||
- "meet.{{ primary_domain }}"
|
@ -1,14 +1,15 @@
|
||||
users:
|
||||
administrator:
|
||||
email: "{{users.administrator.email}}"
|
||||
email: "{{users.administrator.email}}"
|
||||
pds:
|
||||
version: "latest"
|
||||
credentials:
|
||||
#jwt_secret: # Needs to be defined in inventory file - Use: openssl rand -base64 64 | tr -d '\n'
|
||||
#plc_rotation_key_k256_private_key_hex: # Needs to be defined in inventory file - Use: openssl rand -hex 32
|
||||
#admin_password: # Needs to be defined in inventory file - Use: openssl rand -base64 16
|
||||
version: "latest"
|
||||
credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
central_database: true
|
||||
domains:
|
||||
canonical:
|
||||
web: "bskyweb.{{ primary_domain }}"
|
||||
api: "bluesky.{{ primary_domain }}"
|
@ -16,4 +16,7 @@ csp:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
font-src:
|
||||
- "http://*.{{primary_domain}}"
|
||||
- "http://*.{{primary_domain}}"
|
||||
domains:
|
||||
canonical:
|
||||
- "forum.{{ primary_domain }}"
|
||||
|
@ -1,3 +0,0 @@
|
||||
# Jinja2 configuration template
|
||||
# Define your variables here
|
||||
|
1
roles/docker-elk/vars/configuration.yml
Normal file
1
roles/docker-elk/vars/configuration.yml
Normal file
@ -0,0 +1 @@
|
||||
|
@ -1,19 +1,22 @@
|
||||
version: "latest"
|
||||
version: "latest"
|
||||
users:
|
||||
administrator:
|
||||
username: "{{ users.administrator.username }}"
|
||||
email: "{{ users.administrator.email }}"
|
||||
username: "{{ users.administrator.username }}"
|
||||
email: "{{ users.administrator.email }}"
|
||||
|
||||
credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
ldap: false
|
||||
oidc: true
|
||||
central_database: true
|
||||
csp:
|
||||
flags:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
domains:
|
||||
aliases:
|
||||
- "crm.{{ primary_domain }}"
|
@ -2,6 +2,9 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
central_database: true
|
||||
domains:
|
||||
aliases:
|
||||
- "social.{{ primary_domain }}"
|
@ -6,5 +6,9 @@ features:
|
||||
ldap: true
|
||||
central_database: true
|
||||
credentials:
|
||||
# database_password: # Needs to be defined in inventory file
|
||||
# django_secret: # Needs to be defined in inventory file
|
||||
domains:
|
||||
canonical:
|
||||
- "audio.{{ primary_domain }}"
|
||||
aliases:
|
||||
- "music.{{ primary_domain }}"
|
||||
- "sound.{{ primary_domain }}"
|
@ -22,4 +22,7 @@ csp:
|
||||
worker-src:
|
||||
- "blob:"
|
||||
manifest-src:
|
||||
- "data:"
|
||||
- "data:"
|
||||
domains:
|
||||
aliases:
|
||||
- "git.{{ primary_domain }}"
|
@ -2,5 +2,5 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
@ -1,3 +0,0 @@
|
||||
# Jinja2 configuration template
|
||||
# Define your variables here
|
||||
|
@ -2,4 +2,7 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
domains:
|
||||
canonical:
|
||||
- "cms.{{ primary_domain }}"
|
@ -16,4 +16,7 @@ csp:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "auth.{{ primary_domain }}"
|
@ -1,21 +1,23 @@
|
||||
version: "latest"
|
||||
oauth2_proxy:
|
||||
application: application # Needs to be the same as webinterface
|
||||
port: 80 # application port
|
||||
application: application
|
||||
port: 80
|
||||
credentials:
|
||||
# oauth2_proxy_cookie_secret: None # Set via openssl rand -hex 16
|
||||
# administrator_password: "None" # CHANGE for security reasons
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
ldap: true
|
||||
central_database: false
|
||||
oauth2: false
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
domains:
|
||||
aliases:
|
||||
- "ldap.{{primary_domain}}"
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
version: "latest"
|
||||
version: "latest"
|
||||
network:
|
||||
local: True # Activates local network. Necessary for LDIF import routines
|
||||
docker: True # Activates docker network to allow other docker containers to connect
|
||||
public: False # Set to true in inventory file if you want to expose the LDAP port to the internet
|
||||
hostname: "ldap" # Hostname of the LDAP Server in the central_ldap network
|
||||
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
|
||||
local: True # Activates local network. Necessary for LDIF import routines
|
||||
docker: True # Activates docker network to allow other docker containers to connect
|
||||
public: False # Set to true in inventory file if you want to expose the LDAP port to the internet
|
||||
hostname: "ldap" # Hostname of the LDAP Server in the central_ldap network
|
||||
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}" # Administrator username
|
||||
username: "{{users.administrator.username}}" # Administrator username
|
||||
credentials:
|
||||
features:
|
||||
ldap: true
|
||||
ldap: true
|
@ -6,6 +6,9 @@ version: "latest" # Docker Image
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
oidc: true
|
||||
oidc: true
|
||||
domains:
|
||||
canonical:
|
||||
- "newsletter.{{ primary_domain }}"
|
@ -7,14 +7,12 @@ oidc:
|
||||
enable_user_creation: true # Users will be created if not existing
|
||||
domain: "{{primary_domain}}" # The main domain from which mails will be send \ email suffix behind @
|
||||
credentials:
|
||||
# secret_key: # Set to a randomly generated 16 bytes string
|
||||
# database_password: # Needs to be set in inventory file
|
||||
# api_token: # Configures the authentication token. The minimum length is 3 characters. This is a mandatory setting for using the RESTful API.
|
||||
# initial_administrator_password: # Initial administrator password for setup
|
||||
# dkim_public_key: # Must be set in inventory file
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false # Deactivated mailu iframe loading until keycloak supports it
|
||||
portfolio_iframe: false # Deactivated mailu iframe loading until keycloak supports it
|
||||
oidc: true
|
||||
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
||||
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
||||
domains:
|
||||
canonical:
|
||||
- "mail.{{ primary_domain }}"
|
@ -1,14 +1,14 @@
|
||||
application_id: "mailu"
|
||||
application_id: "mailu"
|
||||
|
||||
# Database Configuration
|
||||
database_password: "{{applications.mailu.credentials.database_password}}"
|
||||
database_type: "mariadb"
|
||||
database_password: "{{applications.mailu.credentials.database_password}}"
|
||||
database_type: "mariadb"
|
||||
|
||||
cert_mount_directory: "{{docker_compose.directories.volumes}}certs/"
|
||||
cert_mount_directory: "{{docker_compose.directories.volumes}}certs/"
|
||||
|
||||
# Use dedicated source for oidc if activated
|
||||
# @see https://github.com/heviat/Mailu-OIDC/tree/2024.06
|
||||
docker_source: "{{ 'ghcr.io/heviat' if applications[application_id].features.oidc | bool else 'ghcr.io/mailu' }}"
|
||||
docker_source: "{{ 'ghcr.io/heviat' if applications[application_id].features.oidc | bool else 'ghcr.io/mailu' }}"
|
||||
|
||||
domain: "{{ domains | get_domain(application_id) }}"
|
||||
http_port: "{{ ports.localhost.http[application_id] }}"
|
||||
domain: "{{ domains | get_domain(application_id) }}"
|
||||
http_port: "{{ ports.localhost.http[application_id] }}"
|
@ -1,19 +1,13 @@
|
||||
version: "latest"
|
||||
single_user_mode: false # Set true for initial setup
|
||||
setup: false # Set true in inventory file to execute the setup and initializing procedures
|
||||
credentials:
|
||||
# Check out the README.md of the docker-mastodon role to get detailled instructions about how to setup the credentials
|
||||
# database_password:
|
||||
# secret_key_base:
|
||||
# otp_secret:
|
||||
# vapid_private_key:
|
||||
# vapid_public_key:
|
||||
# active_record_encryption_deterministic_key:
|
||||
# active_record_encryption_key_derivation_salt:
|
||||
# active_record_encryption_primary_key:
|
||||
version: "latest"
|
||||
single_user_mode: false # Set true for initial setup
|
||||
setup: false # Set true in inventory file to execute the setup and initializing procedures
|
||||
credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
oidc: true
|
||||
central_database: true
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
oidc: true
|
||||
central_database: true
|
||||
domains:
|
||||
canonical:
|
||||
- "microblog.{{ primary_domain }}"
|
@ -2,7 +2,7 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
oauth2: false
|
||||
csp:
|
||||
@ -16,4 +16,7 @@ csp:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
aliases:
|
||||
- "analytics.{{ primary_domain }}"
|
@ -25,9 +25,9 @@ csp:
|
||||
whitelist:
|
||||
connect-src:
|
||||
- "{{ primary_domain }}"
|
||||
- "{{ domains.matrix.synapse | safe_var }}"
|
||||
- "matrix.{{ primary_domain }}"
|
||||
script-src:
|
||||
- "{{ domains.matrix.synapse | safe_var }}"
|
||||
- "element.{{ primary_domain }}"
|
||||
- "https://cdn.jsdelivr.net"
|
||||
plugins:
|
||||
# You need to enable them in the inventory file
|
||||
@ -39,3 +39,8 @@ plugins:
|
||||
slack: false
|
||||
telegram: false
|
||||
whatsapp: false
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
synapse: "matrix.{{ primary_domain }}"
|
||||
element: "element.{{ primary_domain }}"
|
3
roles/docker-mediawiki/vars/configuration.yml
Normal file
3
roles/docker-mediawiki/vars/configuration.yml
Normal file
@ -0,0 +1,3 @@
|
||||
domains:
|
||||
canonical:
|
||||
- "wiki.{{ primary_domain }}"
|
@ -22,4 +22,7 @@ csp:
|
||||
- "data:"
|
||||
- "blob:"
|
||||
script-src:
|
||||
- "https://cdn.jsdelivr.net"
|
||||
- "https://cdn.jsdelivr.net"
|
||||
domains:
|
||||
canonical:
|
||||
- "academy.{{ primary_domain }}"
|
@ -3,5 +3,5 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
@ -1,6 +1,4 @@
|
||||
version: "production" # @see https://nextcloud.com/blog/nextcloud-release-channels-and-how-to-track-them/
|
||||
ldap:
|
||||
enabled: True # Enables LDAP by default
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
@ -10,6 +8,10 @@ csp:
|
||||
whitelist:
|
||||
font-src:
|
||||
- "data:"
|
||||
domains:
|
||||
canonical:
|
||||
- "cloud.{{ primary_domain }}"
|
||||
|
||||
oidc:
|
||||
enabled: "{{ applications.nextcloud.features.oidc | default(true) }}" # Activate OIDC for Nextcloud
|
||||
# floavor decides which OICD plugin should be used.
|
||||
@ -23,7 +25,7 @@ credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
ldap: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
|
@ -4,4 +4,4 @@ plugin_configuration:
|
||||
configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}"
|
||||
- appid: "bbb"
|
||||
configkey: "api.url"
|
||||
configvalue: "{{ applications.bigbluebutton.urls.api }}"
|
||||
configvalue: "{{ web_protocol }}://{{domains | get_domain(''bigbluebutton'')}}{{applications.bigbluebutton.api_suffix}}"
|
@ -1,7 +1,6 @@
|
||||
configuration_file: "oauth2-proxy-keycloak.cfg" # Needs to be set true in the roles which use it
|
||||
version: "latest" # Docker Image version
|
||||
redirect_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/auth/realms/{{primary_domain}}/protocol/openid-connect/auth" # The redirect URL for the OAuth2 flow. It should match the redirect URL configured in Keycloak.
|
||||
allowed_roles: admin # Restrict it default to admin role. Use the vars/main.yml to open the specific role for other groups
|
||||
configuration_file: "oauth2-proxy-keycloak.cfg" # Needs to be set true in the roles which use it
|
||||
version: "latest" # Docker Image version
|
||||
allowed_roles: admin # Restrict it default to admin role. Use the vars/main.yml to open the specific role for other groups
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
|
@ -16,4 +16,7 @@ features:
|
||||
csp:
|
||||
flags:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "project.{{ primary_domain }}"
|
@ -9,4 +9,9 @@ csp:
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "video.{{ primary_domain }}"
|
||||
aliases:
|
||||
- "videos.{{ primary_domain }}"
|
@ -5,6 +5,6 @@ oauth2_proxy:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
ldap: true
|
||||
oauth2: true
|
@ -6,7 +6,7 @@ oauth2_proxy:
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
oauth2: true
|
||||
hostname: central-mariadb
|
||||
@ -15,4 +15,8 @@ csp:
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
aliases:
|
||||
- "mysql.{{ primary_domain }}"
|
||||
- "mariadb.{{ primary_domain }}"
|
||||
|
@ -3,7 +3,7 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
csp:
|
||||
flags:
|
||||
@ -11,4 +11,9 @@ csp:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "picture.{{ primary_domain }}"
|
||||
aliases:
|
||||
- "pictures.{{ primary_domain }}"
|
@ -1,6 +1,6 @@
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
csp:
|
||||
whitelist:
|
||||
@ -19,3 +19,7 @@ csp:
|
||||
flags:
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "{{ primary_domain }}"
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
|
||||
csp:
|
||||
whitelist:
|
||||
@ -18,4 +18,7 @@ csp:
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
unsafe-eval: true
|
||||
unsafe-eval: true
|
||||
domains:
|
||||
canonical:
|
||||
- "slides.{{ primary_domain }}"
|
3
roles/docker-roulette-wheel/vars/configuration.yml
Normal file
3
roles/docker-roulette-wheel/vars/configuration.yml
Normal file
@ -0,0 +1,3 @@
|
||||
domains:
|
||||
canonical:
|
||||
- "wheel.{{ primary_domain }}"
|
@ -2,5 +2,8 @@ version: "latest"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
domains:
|
||||
canonical:
|
||||
- "inventory.{{ primary_domain }}"
|
||||
|
@ -1,6 +1,6 @@
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
csp:
|
||||
flags:
|
||||
@ -8,4 +8,7 @@ csp:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "docs.{{ primary_domain }}"
|
||||
|
@ -19,4 +19,7 @@ csp:
|
||||
unsafe-inline: true
|
||||
unsafe-eval: true
|
||||
style-src:
|
||||
unsafe-inline: true
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
- "kanban.{{ primary_domain }}"
|
@ -31,6 +31,9 @@ csp:
|
||||
- "https://fonts.bunny.net"
|
||||
script-src:
|
||||
- "https://cdn.gtranslate.net"
|
||||
- "{{ domains | get_domain('wordpress') }}"
|
||||
- "blog.{{ primary_domain }}"
|
||||
style-src:
|
||||
- "https://fonts.bunny.net"
|
||||
- "https://fonts.bunny.net"
|
||||
domains:
|
||||
canonical:
|
||||
- "blog.{{ primary_domain }}"
|
||||
|
0
roles/docker-xmpp/vars/configuration.yml
Normal file
0
roles/docker-xmpp/vars/configuration.yml
Normal file
@ -9,6 +9,11 @@ oauth2_proxy:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: false
|
||||
central_database: true
|
||||
oauth2: true
|
||||
oauth2: true
|
||||
domains:
|
||||
canonical:
|
||||
- "s.{{ primary_domain }}"
|
||||
aliases:
|
||||
- "short.{{ primary_domain }}"
|
@ -1,4 +1,7 @@
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: true
|
||||
portfolio_iframe: true
|
||||
domains:
|
||||
canonical:
|
||||
- "files.{{ primary_domain }}"
|
||||
|
@ -1,4 +1,7 @@
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
domains:
|
||||
canonical:
|
||||
- "html.{{ primary_domain }}"
|
||||
|
@ -8,6 +8,11 @@
|
||||
- name: Merge system_email definitions
|
||||
set_fact:
|
||||
system_email: "{{ default_system_email | combine(system_email | default({}, true), recursive=True) }}"
|
||||
|
||||
- name: Merge application definitions
|
||||
set_fact:
|
||||
applications: "{{ defaults_applications | combine(applications | default({}, true), recursive=True) }}"
|
||||
|
||||
- name: Merge domain definitions
|
||||
set_fact:
|
||||
domains: "{{ defaults_domains | combine(domains | default({}, true), recursive=True) }}"
|
||||
@ -28,10 +33,6 @@
|
||||
redirect_domain_mappings: "{{ redirect_domain_mappings | default([]) + [ {'source': item.key, 'target': item.value} ] }}"
|
||||
loop: "{{ combined_mapping | dict2items }}"
|
||||
|
||||
- name: Merge application definitions
|
||||
set_fact:
|
||||
applications: "{{ defaults_applications | combine(applications | default({}, true), recursive=True) }}"
|
||||
|
||||
# @todo implement
|
||||
# - name: Ensure features.integrated is set based on group membership
|
||||
# set_fact:
|
||||
|
55
tests/integration/test_domain_uniqueness.py
Normal file
55
tests/integration/test_domain_uniqueness.py
Normal file
@ -0,0 +1,55 @@
|
||||
import unittest
|
||||
import yaml
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
class TestDomainUniqueness(unittest.TestCase):
|
||||
def test_no_duplicate_domains(self):
|
||||
"""
|
||||
Load the applications YAML (generating it via `make build` if missing),
|
||||
collect all entries under domains.canonical and domains.aliases across all applications,
|
||||
and assert that no domain appears more than once.
|
||||
"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
yaml_file = repo_root / 'group_vars' / 'all' / '03_applications.yml'
|
||||
|
||||
# Generate the file if it doesn't exist
|
||||
if not yaml_file.exists():
|
||||
subprocess.run(['make', 'build'], cwd=repo_root, check=True)
|
||||
|
||||
# Load the applications configuration
|
||||
cfg = yaml.safe_load(yaml_file.read_text(encoding='utf-8')) or {}
|
||||
apps = cfg.get('defaults_applications', {})
|
||||
|
||||
all_domains = []
|
||||
for app_name, app_cfg in apps.items():
|
||||
domains_cfg = app_cfg.get('domains', {})
|
||||
|
||||
# canonical entries may be a list or a mapping
|
||||
canonical = domains_cfg.get('canonical', [])
|
||||
if isinstance(canonical, dict):
|
||||
values = list(canonical.values())
|
||||
else:
|
||||
values = canonical or []
|
||||
all_domains.extend(values)
|
||||
|
||||
# aliases entries may be a list or a mapping
|
||||
aliases = domains_cfg.get('aliases', [])
|
||||
if isinstance(aliases, dict):
|
||||
values = list(aliases.values())
|
||||
else:
|
||||
values = aliases or []
|
||||
all_domains.extend(values)
|
||||
|
||||
# Filter out any empty or non-string entries
|
||||
domain_list = [d for d in all_domains if isinstance(d, str) and d.strip()]
|
||||
counts = Counter(domain_list)
|
||||
|
||||
# Find duplicates
|
||||
duplicates = [domain for domain, count in counts.items() if count > 1]
|
||||
if duplicates:
|
||||
self.fail(f"Duplicate domain entries found: {duplicates}\n (May 'make build' solves this issue.)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
89
tests/integration/test_domains_structure.py
Normal file
89
tests/integration/test_domains_structure.py
Normal file
@ -0,0 +1,89 @@
|
||||
import os
|
||||
import yaml
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
ROLES_DIR = Path(__file__).resolve().parent.parent.parent / "roles"
|
||||
|
||||
class TestDomainsStructure(unittest.TestCase):
|
||||
def test_domains_keys_types_and_uniqueness(self):
|
||||
"""Ensure that under 'domains' only 'canonical' and 'aliases' keys exist,
|
||||
'aliases' is a list of strings, 'canonical' is either a list of strings
|
||||
or a dict with string values, and no domain is defined more than once
|
||||
across all roles."""
|
||||
failed_roles = []
|
||||
all_domains = []
|
||||
|
||||
for role_path in ROLES_DIR.iterdir():
|
||||
if not role_path.is_dir():
|
||||
continue
|
||||
vars_dir = role_path / "vars"
|
||||
if not vars_dir.exists():
|
||||
continue
|
||||
|
||||
for vars_file in vars_dir.glob("*.yml"):
|
||||
try:
|
||||
with open(vars_file, 'r') as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
except yaml.YAMLError as e:
|
||||
failed_roles.append((role_path.name, vars_file.name, f"YAML error: {e}"))
|
||||
continue
|
||||
|
||||
if 'domains' not in data:
|
||||
continue
|
||||
|
||||
domains = data['domains']
|
||||
if not isinstance(domains, dict):
|
||||
failed_roles.append((role_path.name, vars_file.name, "'domains' should be a dict"))
|
||||
continue
|
||||
|
||||
# Check allowed keys
|
||||
allowed_keys = {'canonical', 'aliases'}
|
||||
extra_keys = set(domains.keys()) - allowed_keys
|
||||
if extra_keys:
|
||||
failed_roles.append((role_path.name, vars_file.name,
|
||||
f"Unexpected keys in 'domains': {extra_keys}"))
|
||||
|
||||
# Validate and collect 'aliases'
|
||||
if 'aliases' in domains:
|
||||
aliases = domains['aliases']
|
||||
if not isinstance(aliases, list) or not all(isinstance(item, str) for item in aliases):
|
||||
failed_roles.append((role_path.name, vars_file.name,
|
||||
"'aliases' must be a list of strings"))
|
||||
else:
|
||||
all_domains.extend(aliases)
|
||||
|
||||
# Validate and collect 'canonical'
|
||||
if 'canonical' in domains:
|
||||
canonical = domains['canonical']
|
||||
if isinstance(canonical, list):
|
||||
if not all(isinstance(item, str) for item in canonical):
|
||||
failed_roles.append((role_path.name, vars_file.name,
|
||||
"'canonical' list items must be strings"))
|
||||
else:
|
||||
all_domains.extend(canonical)
|
||||
elif isinstance(canonical, dict):
|
||||
if not all(isinstance(k, str) and isinstance(v, str) for k, v in canonical.items()):
|
||||
failed_roles.append((role_path.name, vars_file.name,
|
||||
"All keys and values in 'canonical' dict must be strings"))
|
||||
else:
|
||||
all_domains.extend(canonical.values())
|
||||
else:
|
||||
failed_roles.append((role_path.name, vars_file.name,
|
||||
"'canonical' must be a list or a dict"))
|
||||
|
||||
# Check for duplicate domains across all roles
|
||||
duplicates = [domain for domain, count in Counter(all_domains).items() if count > 1]
|
||||
if duplicates:
|
||||
failed_roles.append(("GLOBAL", "", f"Duplicate domain entries found: {duplicates}"))
|
||||
|
||||
if failed_roles:
|
||||
messages = []
|
||||
for role, file, reason in failed_roles:
|
||||
entry = f"{role}/{file}: {reason}" if file else f"{role}: {reason}"
|
||||
messages.append(entry)
|
||||
self.fail("Domain structure errors found:\n" + "\n".join(messages))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
123
tests/unit/test_domain_filters.py
Normal file
123
tests/unit/test_domain_filters.py
Normal file
@ -0,0 +1,123 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Add the filter_plugins directory to the import path
|
||||
dir_path = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '../../filter_plugins')
|
||||
)
|
||||
sys.path.insert(0, dir_path)
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from domain_filters import FilterModule
|
||||
|
||||
class TestDomainFilters(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.filter_module = FilterModule()
|
||||
# Sample primary domain
|
||||
self.primary = 'example.com'
|
||||
|
||||
def test_canonical_empty_apps(self):
|
||||
apps = {}
|
||||
expected = {}
|
||||
result = self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_canonical_without_domains(self):
|
||||
apps = {'app1': {}}
|
||||
expected = {'app1': ['app1.example.com']}
|
||||
result = self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_canonical_with_list(self):
|
||||
apps = {
|
||||
'app1': {
|
||||
'domains': {'canonical': ['foo.com', 'bar.com']}
|
||||
}
|
||||
}
|
||||
result = self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
self.assertCountEqual(result['app1'], ['foo.com', 'bar.com'])
|
||||
|
||||
def test_canonical_with_dict(self):
|
||||
apps = {
|
||||
'app1': {
|
||||
'domains': {'canonical': {'one': 'one.com', 'two': 'two.com'}}
|
||||
}
|
||||
}
|
||||
result = self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
self.assertCountEqual(result['app1'], ['one.com', 'two.com'])
|
||||
|
||||
def test_canonical_duplicate_raises(self):
|
||||
apps = {
|
||||
'app1': {'domains': {'canonical': ['dup.com']}},
|
||||
'app2': {'domains': {'canonical': ['dup.com']}},
|
||||
}
|
||||
with self.assertRaises(AnsibleFilterError) as cm:
|
||||
self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
self.assertIn("configured for both", str(cm.exception))
|
||||
|
||||
def test_alias_empty_apps(self):
|
||||
apps = {}
|
||||
expected = {}
|
||||
result = self.filter_module.alias_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_alias_without_aliases_and_no_canonical(self):
|
||||
apps = {'app1': {}}
|
||||
# canonical defaults to ['app1.example.com'], so alias should be []
|
||||
expected = {'app1': []}
|
||||
result = self.filter_module.alias_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_alias_with_explicit_aliases(self):
|
||||
apps = {
|
||||
'app1': {
|
||||
'domains': {'aliases': ['alias.com']}
|
||||
}
|
||||
}
|
||||
# canonical defaults to ['app1.example.com'], so alias should include alias.com and default
|
||||
expected = {'app1': ['alias.com', 'app1.example.com']}
|
||||
result = self.filter_module.alias_domains_map(apps, self.primary)
|
||||
self.assertCountEqual(result['app1'], expected['app1'])
|
||||
|
||||
def test_alias_with_canonical_not_default(self):
|
||||
apps = {
|
||||
'app1': {
|
||||
'domains': {'canonical': ['foo.com']}
|
||||
}
|
||||
}
|
||||
# foo.com is canonical, default not in canonical so added as alias
|
||||
expected = {'app1': ['app1.example.com']}
|
||||
result = self.filter_module.alias_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_alias_with_existing_default(self):
|
||||
apps = {
|
||||
'app1': {
|
||||
'domains': {
|
||||
'canonical': ['foo.com'],
|
||||
'aliases': ['app1.example.com']
|
||||
}
|
||||
}
|
||||
}
|
||||
# default present in aliases, should not be duplicated
|
||||
expected = {'app1': ['app1.example.com']}
|
||||
result = self.filter_module.alias_domains_map(apps, self.primary)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_invalid_canonical_type(self):
|
||||
apps = {
|
||||
'app1': {'domains': {'canonical': 123}}
|
||||
}
|
||||
with self.assertRaises(AnsibleFilterError):
|
||||
self.filter_module.canonical_domains_map(apps, self.primary)
|
||||
|
||||
def test_invalid_aliases_type(self):
|
||||
apps = {
|
||||
'app1': {'domains': {'aliases': 123}}
|
||||
}
|
||||
with self.assertRaises(AnsibleFilterError):
|
||||
self.filter_module.alias_domains_map(apps, self.primary)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -23,7 +23,7 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
||||
(self.sample_role / "vars" / "configuration.yml").write_text("foo: bar\nbaz: 123\n")
|
||||
|
||||
# Output file path
|
||||
self.output_file = self.temp_dir / "group_vars" / "all" / "11_applications.yml"
|
||||
self.output_file = self.temp_dir / "group_vars" / "all" / "03_applications.yml"
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
Loading…
x
Reference in New Issue
Block a user