mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-03 19:58:14 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			cc3f5d75ea
			...
			03f3a31d21
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 03f3a31d21 | |||
| 1b50f73803 | |||
| 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
 | 
			
		||||
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).
 | 
			
		||||
							
								
								
									
										86
									
								
								filter_plugins/alias_domains_map.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								filter_plugins/alias_domains_map.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
from ansible.errors import AnsibleFilterError
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'alias_domains_map': self.alias_domains_map}
 | 
			
		||||
 | 
			
		||||
    def alias_domains_map(self, apps, primary_domain):
 | 
			
		||||
        """
 | 
			
		||||
        Build a map of application IDs to their alias domains.
 | 
			
		||||
 | 
			
		||||
        - If no `domains` key → []  
 | 
			
		||||
        - If `domains` exists but is an empty dict → return the original cfg  
 | 
			
		||||
        - Explicit `aliases` are used (default appended if missing)  
 | 
			
		||||
        - If only `canonical` defined and it doesn't include default, default is added  
 | 
			
		||||
        - Invalid types raise AnsibleFilterError
 | 
			
		||||
        """
 | 
			
		||||
        def parse_entry(domains_cfg, key, app_id):
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
        def default_domain(app_id, primary):
 | 
			
		||||
            return f"{app_id}.{primary}"
 | 
			
		||||
 | 
			
		||||
        # 1) Precompute canonical domains per app (fallback to default)
 | 
			
		||||
        canonical_map = {}
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            domains_cfg = cfg.get('domains') or {}
 | 
			
		||||
            entry = domains_cfg.get('canonical')
 | 
			
		||||
            if entry is None:
 | 
			
		||||
                canonical_map[app_id] = [default_domain(app_id, primary_domain)]
 | 
			
		||||
            elif isinstance(entry, dict):
 | 
			
		||||
                canonical_map[app_id] = list(entry.values())
 | 
			
		||||
            elif isinstance(entry, list):
 | 
			
		||||
                canonical_map[app_id] = list(entry)
 | 
			
		||||
            else:
 | 
			
		||||
                raise AnsibleFilterError(
 | 
			
		||||
                    f"Unexpected type for 'domains.canonical' in application '{app_id}': {type(entry).__name__}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # 2) Build alias list per app
 | 
			
		||||
        result = {}
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            domains_cfg = cfg.get('domains')
 | 
			
		||||
 | 
			
		||||
            # no domains key → no aliases
 | 
			
		||||
            if domains_cfg is None:
 | 
			
		||||
                result[app_id] = []
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # empty domains dict → return the original cfg
 | 
			
		||||
            if isinstance(domains_cfg, dict) and not domains_cfg:
 | 
			
		||||
                result[app_id] = cfg
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # otherwise, compute aliases
 | 
			
		||||
            aliases = parse_entry(domains_cfg, 'aliases', app_id) or []
 | 
			
		||||
            default = default_domain(app_id, primary_domain)
 | 
			
		||||
            has_aliases = 'aliases' in domains_cfg
 | 
			
		||||
            has_canon   = 'canonical' in domains_cfg
 | 
			
		||||
 | 
			
		||||
            if has_aliases:
 | 
			
		||||
                if default not in aliases:
 | 
			
		||||
                    aliases.append(default)
 | 
			
		||||
            elif has_canon:
 | 
			
		||||
                canon = canonical_map.get(app_id, [])
 | 
			
		||||
                if default not in canon and default not in aliases:
 | 
			
		||||
                    aliases.append(default)
 | 
			
		||||
 | 
			
		||||
            result[app_id] = aliases
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
							
								
								
									
										75
									
								
								filter_plugins/canonical_domains_map.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								filter_plugins/canonical_domains_map.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
from ansible.errors import AnsibleFilterError
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'canonical_domains_map': self.canonical_domains_map}
 | 
			
		||||
 | 
			
		||||
    def canonical_domains_map(self, apps, primary_domain):
 | 
			
		||||
        def parse_entry(domains_cfg, key, app_id):
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
        result = {}
 | 
			
		||||
        seen = {}
 | 
			
		||||
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            domains_cfg = cfg.get('domains')
 | 
			
		||||
            if not domains_cfg or 'canonical' not in domains_cfg:
 | 
			
		||||
                default = f"{app_id}.{primary_domain}"
 | 
			
		||||
                if default in seen:
 | 
			
		||||
                    raise AnsibleFilterError(
 | 
			
		||||
                        f"Domain '{default}' is already configured for '{seen[default]}' and '{app_id}'"
 | 
			
		||||
                    )
 | 
			
		||||
                seen[default] = app_id
 | 
			
		||||
                result[app_id] = [default]
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            entry = domains_cfg['canonical']
 | 
			
		||||
 | 
			
		||||
            if isinstance(entry, dict):
 | 
			
		||||
                for name, domain in entry.items():
 | 
			
		||||
                    if not isinstance(domain, str) or not domain.strip():
 | 
			
		||||
                        raise AnsibleFilterError(
 | 
			
		||||
                            f"Invalid domain entry in 'canonical' for application '{app_id}': {domain!r}"
 | 
			
		||||
                        )
 | 
			
		||||
                    if domain in seen:
 | 
			
		||||
                        raise AnsibleFilterError(
 | 
			
		||||
                            f"Domain '{domain}' is already configured for '{seen[domain]}' and '{app_id}'"
 | 
			
		||||
                        )
 | 
			
		||||
                    seen[domain] = app_id
 | 
			
		||||
                result[app_id] = entry.copy()
 | 
			
		||||
 | 
			
		||||
            elif isinstance(entry, list):
 | 
			
		||||
                for domain in entry:
 | 
			
		||||
                    if not isinstance(domain, str) or not domain.strip():
 | 
			
		||||
                        raise AnsibleFilterError(
 | 
			
		||||
                            f"Invalid domain entry in 'canonical' for application '{app_id}': {domain!r}"
 | 
			
		||||
                        )
 | 
			
		||||
                    if domain in seen:
 | 
			
		||||
                        raise AnsibleFilterError(
 | 
			
		||||
                            f"Domain '{domain}' is already configured for '{seen[domain]}' and '{app_id}'"
 | 
			
		||||
                        )
 | 
			
		||||
                    seen[domain] = app_id
 | 
			
		||||
                result[app_id] = list(entry)
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                raise AnsibleFilterError(
 | 
			
		||||
                    f"Unexpected type for 'domains.canonical' in application '{app_id}': {type(entry).__name__}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
import re
 | 
			
		||||
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
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {
 | 
			
		||||
            'generate_all_domains': self.generate_all_domains,
 | 
			
		||||
            'generate_base_sld_domains': self.generate_base_sld_domains,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def generate_all_domains(domains_dict, include_www=True):
 | 
			
		||||
        """
 | 
			
		||||
        Transform a dict of domains (values: str, list, dict) into a flat list,
 | 
			
		||||
        optionally add 'www.' prefixes, dedupe and sort alphabetically.
 | 
			
		||||
 | 
			
		||||
        Avoids infinite loops by snapshotting initial domain list for www prefixes.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            flat = FilterModule._flatten_domain_values(domains_dict)
 | 
			
		||||
            if include_www:
 | 
			
		||||
                # Snapshot original list to avoid extending while iterating
 | 
			
		||||
                original = list(flat)
 | 
			
		||||
                flat.extend([f"www.{d}" for d in original])
 | 
			
		||||
            return sorted(set(flat))
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            raise AnsibleFilterError(f"generate_all_domains failed: {exc}")
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def generate_base_sld_domains(domains_dict, redirect_mappings):
 | 
			
		||||
        """
 | 
			
		||||
        Flatten domains_dict and redirect_mappings, extract second-level + top-level domains.
 | 
			
		||||
        redirect_mappings: list of dicts with key 'source'
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            flat = FilterModule._flatten_domain_values(domains_dict)
 | 
			
		||||
            for mapping in redirect_mappings or []:
 | 
			
		||||
                src = mapping.get('source')
 | 
			
		||||
                if isinstance(src, str):
 | 
			
		||||
                    flat.append(src)
 | 
			
		||||
                elif isinstance(src, list):
 | 
			
		||||
                    flat.extend(src)
 | 
			
		||||
 | 
			
		||||
            pattern = re.compile(r'^(?:.*\.)?([^.]+\.[^.]+)$')
 | 
			
		||||
            slds = {m.group(1) for d in flat if (m := pattern.match(d))}
 | 
			
		||||
            return sorted(slds)
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            raise AnsibleFilterError(f"generate_base_sld_domains failed: {exc}")
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _flatten_domain_values(domains_dict):
 | 
			
		||||
        """
 | 
			
		||||
        Helper to extract domain strings from dict values (str, list, dict).
 | 
			
		||||
        """
 | 
			
		||||
        flat = []
 | 
			
		||||
        for val in (domains_dict or {}).values():
 | 
			
		||||
            if isinstance(val, str):
 | 
			
		||||
                flat.append(val)
 | 
			
		||||
            elif isinstance(val, list):
 | 
			
		||||
                flat.extend(val)
 | 
			
		||||
            elif isinstance(val, dict):
 | 
			
		||||
                flat.extend(val.values())
 | 
			
		||||
        return flat
 | 
			
		||||
							
								
								
									
										94
									
								
								filter_plugins/domain_redirect_mappings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								filter_plugins/domain_redirect_mappings.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
from ansible.errors import AnsibleFilterError
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'domain_mappings': self.domain_mappings}
 | 
			
		||||
 | 
			
		||||
    def domain_mappings(self, apps, primary_domain):
 | 
			
		||||
        """
 | 
			
		||||
        Build a flat list of redirect mappings for all apps:
 | 
			
		||||
          - source: each alias domain
 | 
			
		||||
          - target: the first canonical domain (app.domains.canonical[0] or default)
 | 
			
		||||
 | 
			
		||||
        Logic for computing aliases and canonicals is identical to alias_domains_map + canonical_domains_map.
 | 
			
		||||
        """
 | 
			
		||||
        def parse_entry(domains_cfg, key, app_id):
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
        def default_domain(app_id, primary):
 | 
			
		||||
            return f"{app_id}.{primary}"
 | 
			
		||||
 | 
			
		||||
        # 1) Compute canonical domains per app (always as a list)
 | 
			
		||||
        canonical_map = {}
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            domains_cfg = cfg.get('domains') or {}
 | 
			
		||||
            entry = domains_cfg.get('canonical')
 | 
			
		||||
            if entry is None:
 | 
			
		||||
                canonical_map[app_id] = [default_domain(app_id, primary_domain)]
 | 
			
		||||
            elif isinstance(entry, dict):
 | 
			
		||||
                canonical_map[app_id] = list(entry.values())
 | 
			
		||||
            elif isinstance(entry, list):
 | 
			
		||||
                canonical_map[app_id] = list(entry)
 | 
			
		||||
            else:
 | 
			
		||||
                raise AnsibleFilterError(
 | 
			
		||||
                    f"Unexpected type for 'domains.canonical' in application '{app_id}': {type(entry).__name__}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # 2) Compute alias domains per app
 | 
			
		||||
        alias_map = {}
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            domains_cfg = cfg.get('domains')
 | 
			
		||||
            if domains_cfg is None:
 | 
			
		||||
                # no domains key → no aliases
 | 
			
		||||
                alias_map[app_id] = []
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if isinstance(domains_cfg, dict) and not domains_cfg:
 | 
			
		||||
                # empty domains dict → only default
 | 
			
		||||
                alias_map[app_id] = [default_domain(app_id, primary_domain)]
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            aliases = parse_entry(domains_cfg, 'aliases', app_id) or []
 | 
			
		||||
            default = default_domain(app_id, primary_domain)
 | 
			
		||||
            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:
 | 
			
		||||
                canon = canonical_map.get(app_id, [])
 | 
			
		||||
                if default not in canon and default not in aliases:
 | 
			
		||||
                    aliases.append(default)
 | 
			
		||||
 | 
			
		||||
            alias_map[app_id] = aliases
 | 
			
		||||
 | 
			
		||||
        # 3) Build flat list of {source, target} entries
 | 
			
		||||
        mappings = []
 | 
			
		||||
        for app_id, sources in alias_map.items():
 | 
			
		||||
            # pick first canonical domain as target
 | 
			
		||||
            canon_list = canonical_map.get(app_id, [])
 | 
			
		||||
            target = canon_list[0] if canon_list else default_domain(app_id, primary_domain)
 | 
			
		||||
            for src in sources:
 | 
			
		||||
                mappings.append({
 | 
			
		||||
                    'source': src,
 | 
			
		||||
                    'target': target
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        return mappings
 | 
			
		||||
							
								
								
									
										31
									
								
								filter_plugins/generate_all_domains.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								filter_plugins/generate_all_domains.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
from ansible.errors import AnsibleFilterError
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'generate_all_domains': self.generate_all_domains}
 | 
			
		||||
 | 
			
		||||
    def generate_all_domains(self, domains_dict, include_www=True):
 | 
			
		||||
        """
 | 
			
		||||
        Transform a dict of domains (values: str, list, dict) into a flat list,
 | 
			
		||||
        optionally add 'www.' prefixes, dedupe and sort alphabetically.
 | 
			
		||||
        """
 | 
			
		||||
        # lokaler Helfer zum Flatten
 | 
			
		||||
        def _flatten(domains):
 | 
			
		||||
            flat = []
 | 
			
		||||
            for v in (domains or {}).values():
 | 
			
		||||
                if isinstance(v, str):
 | 
			
		||||
                    flat.append(v)
 | 
			
		||||
                elif isinstance(v, list):
 | 
			
		||||
                    flat.extend(v)
 | 
			
		||||
                elif isinstance(v, dict):
 | 
			
		||||
                    flat.extend(v.values())
 | 
			
		||||
            return flat
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            flat = _flatten(domains_dict)
 | 
			
		||||
            if include_www:
 | 
			
		||||
                original = list(flat)
 | 
			
		||||
                flat.extend([f"www.{d}" for d in original])
 | 
			
		||||
            return sorted(set(flat))
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            raise AnsibleFilterError(f"generate_all_domains failed: {exc}")
 | 
			
		||||
							
								
								
									
										37
									
								
								filter_plugins/generate_base_sld_domains.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								filter_plugins/generate_base_sld_domains.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import re
 | 
			
		||||
from ansible.errors import AnsibleFilterError
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'generate_base_sld_domains': self.generate_base_sld_domains}
 | 
			
		||||
 | 
			
		||||
    def generate_base_sld_domains(self, domains_dict, redirect_mappings):
 | 
			
		||||
        """
 | 
			
		||||
        Flatten domains_dict und redirect_mappings, extrahiere SLDs (z.B. example.com),
 | 
			
		||||
        dedupe und sortiere.
 | 
			
		||||
        """
 | 
			
		||||
        def _flatten(domains):
 | 
			
		||||
            flat = []
 | 
			
		||||
            for v in (domains or {}).values():
 | 
			
		||||
                if isinstance(v, str):
 | 
			
		||||
                    flat.append(v)
 | 
			
		||||
                elif isinstance(v, list):
 | 
			
		||||
                    flat.extend(v)
 | 
			
		||||
                elif isinstance(v, dict):
 | 
			
		||||
                    flat.extend(v.values())
 | 
			
		||||
            return flat
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            flat = _flatten(domains_dict)
 | 
			
		||||
            for mapping in redirect_mappings or []:
 | 
			
		||||
                src = mapping.get('source')
 | 
			
		||||
                if isinstance(src, str):
 | 
			
		||||
                    flat.append(src)
 | 
			
		||||
                elif isinstance(src, list):
 | 
			
		||||
                    flat.extend(src)
 | 
			
		||||
 | 
			
		||||
            pattern = re.compile(r'^(?:.*\.)?([^.]+\.[^.]+)$')
 | 
			
		||||
            slds = {m.group(1) for d in flat if (m := pattern.match(d))}
 | 
			
		||||
            return sorted(slds)
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            raise AnsibleFilterError(f"generate_base_sld_domains failed: {exc}")
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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: []
 | 
			
		||||
							
								
								
									
										6
									
								
								group_vars/all/11_domains.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								group_vars/all/11_domains.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
defaults_domains: "{{ defaults_applications | canonical_domains_map(primary_domain) }}"
 | 
			
		||||
 | 
			
		||||
defaults_redirect_domain_mappings: "{{ applications | domain_mappings(primary_domain) }}"
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
@@ -5,3 +5,5 @@ pacman:
 | 
			
		||||
  - ansible
 | 
			
		||||
  - python-passlib
 | 
			
		||||
  - python-pytest
 | 
			
		||||
yay:
 | 
			
		||||
  - python-simpleaudio
 | 
			
		||||
@@ -10,3 +10,7 @@ features:
 | 
			
		||||
credentials:
 | 
			
		||||
#  database_password:     Needs to be defined in inventory file 
 | 
			
		||||
#  setup_admin_password:  Needs to be defined in inventory file
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    - "accounting.{{ primary_domain }}"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
version:                      "latest"
 | 
			
		||||
version:            "latest"
 | 
			
		||||
credentials:
 | 
			
		||||
# database_password: Password for the database
 | 
			
		||||
 | 
			
		||||
features:
 | 
			
		||||
  matomo:             true
 | 
			
		||||
  css:                true
 | 
			
		||||
  matomo:           true
 | 
			
		||||
  css:              true
 | 
			
		||||
  portfolio_iframe: false
 | 
			
		||||
  central_database:   true
 | 
			
		||||
  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
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    - "meet.{{ primary_domain }}"
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
users:
 | 
			
		||||
  administrator:
 | 
			
		||||
  email:    "{{users.administrator.email}}"
 | 
			
		||||
    email:          "{{users.administrator.email}}"
 | 
			
		||||
pds:
 | 
			
		||||
  version:              "latest"
 | 
			
		||||
  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
 | 
			
		||||
features:
 | 
			
		||||
  matomo: true
 | 
			
		||||
  css: true
 | 
			
		||||
  matomo:           true
 | 
			
		||||
  css:              true
 | 
			
		||||
  portfolio_iframe: true
 | 
			
		||||
  central_database: true
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    web:  "bskyweb.{{ primary_domain }}"
 | 
			
		||||
    api:  "bluesky.{{ primary_domain }}"
 | 
			
		||||
@@ -17,3 +17,6 @@ csp:
 | 
			
		||||
  whitelist:
 | 
			
		||||
    font-src:
 | 
			
		||||
      - "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
 | 
			
		||||
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 }}"
 | 
			
		||||
@@ -23,3 +23,6 @@ csp:
 | 
			
		||||
      - "blob:"
 | 
			
		||||
    manifest-src:
 | 
			
		||||
      - "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 }}"
 | 
			
		||||
@@ -17,3 +17,6 @@ csp:
 | 
			
		||||
      unsafe-inline: true
 | 
			
		||||
    style-src:
 | 
			
		||||
      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
 | 
			
		||||
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
 | 
			
		||||
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
 | 
			
		||||
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:                                  
 | 
			
		||||
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:
 | 
			
		||||
@@ -17,3 +17,6 @@ csp:
 | 
			
		||||
      unsafe-eval:   true
 | 
			
		||||
    style-src:
 | 
			
		||||
      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 }}"
 | 
			
		||||
@@ -23,3 +23,6 @@ csp:
 | 
			
		||||
      - "blob:"
 | 
			
		||||
    script-src:
 | 
			
		||||
      - "https://cdn.jsdelivr.net"
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    - "academy.{{ primary_domain }}"
 | 
			
		||||
@@ -3,5 +3,5 @@ version:              "latest"
 | 
			
		||||
features:
 | 
			
		||||
  matomo:             true
 | 
			
		||||
  css:                true
 | 
			
		||||
  portfolio_iframe: false
 | 
			
		||||
  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
 | 
			
		||||
 
 | 
			
		||||
@@ -17,3 +17,6 @@ csp:
 | 
			
		||||
  flags:
 | 
			
		||||
    script-src:
 | 
			
		||||
      unsafe-inline: true
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    - "project.{{ primary_domain }}"
 | 
			
		||||
@@ -10,3 +10,8 @@ csp:
 | 
			
		||||
      unsafe-inline: true
 | 
			
		||||
    style-src:
 | 
			
		||||
      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
 | 
			
		||||
@@ -16,3 +16,7 @@ csp:
 | 
			
		||||
      unsafe-inline: true
 | 
			
		||||
    script-src:
 | 
			
		||||
      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:
 | 
			
		||||
@@ -12,3 +12,8 @@ csp:
 | 
			
		||||
      unsafe-eval:   true
 | 
			
		||||
    style-src:
 | 
			
		||||
      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
 | 
			
		||||
  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:
 | 
			
		||||
@@ -9,3 +9,6 @@ csp:
 | 
			
		||||
      unsafe-eval:    true
 | 
			
		||||
    style-src:
 | 
			
		||||
      unsafe-inline:  true
 | 
			
		||||
domains:
 | 
			
		||||
  canonical:
 | 
			
		||||
    - "docs.{{ primary_domain }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -20,3 +20,6 @@ csp:
 | 
			
		||||
      unsafe-eval:   true
 | 
			
		||||
    style-src:
 | 
			
		||||
      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"
 | 
			
		||||
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
 | 
			
		||||
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
 | 
			
		||||
  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()
 | 
			
		||||
							
								
								
									
										103
									
								
								tests/unit/test_domain_filters_alias.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								tests/unit/test_domain_filters_alias.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
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 alias_domains_map import FilterModule
 | 
			
		||||
 | 
			
		||||
class TestDomainFilters(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.filter_module = FilterModule()
 | 
			
		||||
        # Sample primary domain
 | 
			
		||||
        self.primary = 'example.com'
 | 
			
		||||
 | 
			
		||||
    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_aliases_type(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {'domains': {'aliases': 123}}
 | 
			
		||||
        }
 | 
			
		||||
        with self.assertRaises(AnsibleFilterError):
 | 
			
		||||
            self.filter_module.alias_domains_map(apps, self.primary)
 | 
			
		||||
 | 
			
		||||
    def test_alias_with_empty_domains_cfg(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {
 | 
			
		||||
                'domains': {}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        expected = apps
 | 
			
		||||
        result = self.filter_module.alias_domains_map(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
        
 | 
			
		||||
    def test_alias_with_canonical_dict_not_default(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {
 | 
			
		||||
                'domains': {
 | 
			
		||||
                    'canonical': {
 | 
			
		||||
                        'one': 'one.com',
 | 
			
		||||
                        'two': 'two.com'
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        expected = {'app1': ['app1.example.com']}
 | 
			
		||||
        result = self.filter_module.alias_domains_map(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    unittest.main()
 | 
			
		||||
@@ -8,7 +8,7 @@ sys.path.insert(
 | 
			
		||||
    os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins'))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from domain_filters import FilterModule
 | 
			
		||||
from generate_all_domains import FilterModule
 | 
			
		||||
 | 
			
		||||
class TestGenerateAllDomains(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ sys.path.insert(
 | 
			
		||||
    os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins'))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from domain_filters import FilterModule
 | 
			
		||||
from generate_base_sld_domains import FilterModule
 | 
			
		||||
 | 
			
		||||
class TestGenerateBaseSldDomains(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										74
									
								
								tests/unit/test_domain_filters_canonical.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								tests/unit/test_domain_filters_canonical.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
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 canonical_domains_map 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.assertEqual(
 | 
			
		||||
            result['app1'],
 | 
			
		||||
            {'one': 'one.com', 'two': '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)
 | 
			
		||||
        # Updated to match new exception message
 | 
			
		||||
        self.assertIn("already configured for", str(cm.exception))
 | 
			
		||||
 | 
			
		||||
    def test_invalid_canonical_type(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {'domains': {'canonical': 123}}
 | 
			
		||||
        }
 | 
			
		||||
        with self.assertRaises(AnsibleFilterError):
 | 
			
		||||
            self.filter_module.canonical_domains_map(apps, self.primary)
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    unittest.main()
 | 
			
		||||
							
								
								
									
										105
									
								
								tests/unit/test_domain_mappings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								tests/unit/test_domain_mappings.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
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_redirect_mappings import FilterModule
 | 
			
		||||
 | 
			
		||||
class TestDomainMappings(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.filter = FilterModule()
 | 
			
		||||
        self.primary = 'example.com'
 | 
			
		||||
 | 
			
		||||
    def test_empty_apps(self):
 | 
			
		||||
        apps = {}
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, [])
 | 
			
		||||
 | 
			
		||||
    def test_app_without_domains(self):
 | 
			
		||||
        apps = {'app1': {}}
 | 
			
		||||
        # no domains key → no mappings
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, [])
 | 
			
		||||
 | 
			
		||||
    def test_empty_domains_cfg(self):
 | 
			
		||||
        apps = {'app1': {'domains': {}}}
 | 
			
		||||
        default = 'app1.example.com'
 | 
			
		||||
        expected = [
 | 
			
		||||
            {'source': default, 'target': default}
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_explicit_aliases(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {
 | 
			
		||||
                'domains': {'aliases': ['alias.com']}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        default = 'app1.example.com'
 | 
			
		||||
        expected = [
 | 
			
		||||
            {'source': 'alias.com',    'target': default},
 | 
			
		||||
            {'source': default,        'target': default},
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        # order not important
 | 
			
		||||
        self.assertCountEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_canonical_not_default(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {
 | 
			
		||||
                'domains': {'canonical': ['foo.com']}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        expected = [
 | 
			
		||||
            {'source': 'app1.example.com', 'target': 'foo.com'}
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_canonical_dict(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {
 | 
			
		||||
                'domains': {
 | 
			
		||||
                    'canonical': {'one': 'one.com', 'two': 'two.com'}
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        # first canonical key 'one' → one.com
 | 
			
		||||
        expected = [
 | 
			
		||||
            {'source': 'app1.example.com', 'target': 'one.com'}
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_multiple_apps(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {'domains': {'aliases': ['a1.com']}},
 | 
			
		||||
            'app2': {'domains': {'canonical': ['c2.com']}},
 | 
			
		||||
        }
 | 
			
		||||
        expected = [
 | 
			
		||||
            # app1
 | 
			
		||||
            {'source': 'a1.com',              'target': 'app1.example.com'},
 | 
			
		||||
            {'source': 'app1.example.com',    'target': 'app1.example.com'},
 | 
			
		||||
            # app2
 | 
			
		||||
            {'source': 'app2.example.com',    'target': 'c2.com'},
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.filter.domain_mappings(apps, self.primary)
 | 
			
		||||
        self.assertCountEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_aliases_type(self):
 | 
			
		||||
        apps = {
 | 
			
		||||
            'app1': {'domains': {'aliases': 123}}
 | 
			
		||||
        }
 | 
			
		||||
        with self.assertRaises(AnsibleFilterError):
 | 
			
		||||
            self.filter.domain_mappings(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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user