diff --git a/Makefile b/Makefile index 4d363d84..d2ec31d3 100644 --- a/Makefile +++ b/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 diff --git a/ansible.cfg b/ansible.cfg index 99c620d9..1465cf11 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,2 +1,3 @@ [defaults] -lookup_plugins = ./lookup_plugins \ No newline at end of file +lookup_plugins = ./lookup_plugins +filter_plugins = ./filter_plugins \ No newline at end of file diff --git a/cli/generate-applications-defaults.py b/cli/generate-applications-defaults.py index 102727d8..7863bdae 100644 --- a/cli/generate-applications-defaults.py +++ b/cli/generate-applications-defaults.py @@ -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() diff --git a/docs/guides/developer/Ansible_Directory_Guide.md b/docs/guides/developer/Ansible_Directory_Guide.md new file mode 100644 index 00000000..b52797cb --- /dev/null +++ b/docs/guides/developer/Ansible_Directory_Guide.md @@ -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). diff --git a/filter_plugins/configuration_filters.py b/filter_plugins/configuration_filters.py index 65c85ddd..041ff871 100644 --- a/filter_plugins/configuration_filters.py +++ b/filter_plugins/configuration_filters.py @@ -1,6 +1,6 @@ -def is_feature_enabled(applications, feature: str, application_id: str) -> bool: +def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool: """ - Check if a generic feature is enabled for the given application. + Return True if applications[application_id].features[feature] is truthy. """ app = applications.get(application_id, {}) return bool(app.get('features', {}).get(feature, False)) @@ -31,4 +31,4 @@ class FilterModule(object): return { 'is_feature_enabled': is_feature_enabled, 'get_docker_compose': get_docker_compose, - } \ No newline at end of file + } diff --git a/filter_plugins/csp_filters.py b/filter_plugins/csp_filters.py index a0382c01..38799ce1 100644 --- a/filter_plugins/csp_filters.py +++ b/filter_plugins/csp_filters.py @@ -12,9 +12,10 @@ class FilterModule(object): 'build_csp_header': self.build_csp_header, } - def is_feature_enabled(self, applications, feature: str, application_id: str) -> bool: + @staticmethod + def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool: """ - Check if a generic feature is enabled for the given application. + Return True if applications[application_id].features[feature] is truthy. """ app = applications.get(application_id, {}) return bool(app.get('features', {}).get(feature, False)) @@ -99,7 +100,7 @@ class FilterModule(object): for directive in directives: tokens = ["'self'"] - + # unsafe-eval / unsafe-inline flags flags = self.get_csp_flags(applications, application_id, directive) tokens += flags diff --git a/filter_plugins/domain_filters.py b/filter_plugins/domain_filters.py index abb24043..d6ac6c30 100644 --- a/filter_plugins/domain_filters.py +++ b/filter_plugins/domain_filters.py @@ -3,16 +3,101 @@ from ansible.errors import AnsibleFilterError class FilterModule(object): """ - Custom Ansible filter plugin: - - generate_all_domains: Flatten, dedupe, sort domains with optional www prefixes - - generate_base_sld_domains: Extract unique sld.tld domains from values and redirect sources + Ansible Filter Plugin for Domain Processing + + This plugin provides filters to manage and transform domain configurations for applications: + + - generate_all_domains(domains_dict, include_www=True): + Flattens nested domain values (string, list, or dict), optionally adds 'www.' prefixes, + removes duplicates, and returns a sorted list of unique domains. + + - generate_base_sld_domains(domains_dict, redirect_mappings): + Flattens domains and redirect mappings, extracts second-level + top-level domains (SLDs), + deduplicates, and returns a sorted list of base domains. + + - canonical_domains_map(apps, primary_domain): + Builds a mapping of application IDs to their canonical domains using + DomainUtils.canonical_list, enforcing uniqueness and detecting conflicts. + + - alias_domains_map(apps, primary_domain): + Generates alias domains for each application via DomainUtils.alias_list, + based on their canonical domains and provided configurations. """ def filters(self): return { - 'generate_all_domains': self.generate_all_domains, - 'generate_base_sld_domains': self.generate_base_sld_domains, + 'generate_all_domains': self.generate_all_domains, + 'generate_base_sld_domains': self.generate_base_sld_domains, + 'canonical_domains_map': self.canonical_domains_map, + 'alias_domains_map': self.alias_domains_map, } + + @staticmethod + def parse_entry(domains_cfg, key, app_id): + """ + Extract list of strings from domains_cfg[key], which may be dict or list. + Returns None if key not in domains_cfg. + Raises AnsibleFilterError on invalid type or empty/invalid values. + """ + if key not in domains_cfg: + return None + entry = domains_cfg[key] + if isinstance(entry, dict): + values = list(entry.values()) + elif isinstance(entry, list): + values = entry + else: + raise AnsibleFilterError( + f"Unexpected type for 'domains.{key}' in application '{app_id}': {type(entry).__name__}" + ) + for d in values: + if not isinstance(d, str) or not d.strip(): + raise AnsibleFilterError( + f"Invalid domain entry in '{key}' for application '{app_id}': {d!r}" + ) + return values + + @staticmethod + def default_domain(app_id, primary_domain): + """ + Returns the default domain string for an application. + """ + return f"{app_id}.{primary_domain}" + + @classmethod + def canonical_list(cls, domains_cfg, app_id, primary_domain): + """ + Returns the list of canonical domains: parsed entry or default. + """ + domains = cls.parse_entry(domains_cfg, 'canonical', app_id) + if domains is None: + return [cls.default_domain(app_id, primary_domain)] + return domains + + @classmethod + def alias_list(cls, domains_cfg, app_id, primary_domain, canonical_domains=None): + """ + Returns the list of alias domains based on: + - explicit aliases entry + - presence of canonical entry and default not in canonical + Always ensures default domain in aliases when appropriate. + """ + default = cls.default_domain(app_id, primary_domain) + aliases = cls.parse_entry(domains_cfg, 'aliases', app_id) or [] + has_aliases = 'aliases' in domains_cfg + has_canonical = 'canonical' in domains_cfg + + if has_aliases: + if default not in aliases: + aliases.append(default) + elif has_canonical: + # use provided canonical_domains if given otherwise parse + canon = canonical_domains if canonical_domains is not None else cls.parse_entry(domains_cfg, 'canonical', app_id) + if default not in (canon or []): + aliases.append(default) + # else: neither defined -> empty list + return aliases + @staticmethod def generate_all_domains(domains_dict, include_www=True): @@ -67,3 +152,33 @@ class FilterModule(object): elif isinstance(val, dict): flat.extend(val.values()) return flat + + def canonical_domains_map(self, apps, primary_domain): + result = {} + seen = {} + for app_id, app_cfg in apps.items(): + domains_cfg = app_cfg.get('domains', {}) or {} + domains = self.canonical_list(domains_cfg, app_id, primary_domain) + for d in domains: + if d in seen: + raise AnsibleFilterError( + f"Domain '{d}' is configured for both '{seen[d]}' and '{app_id}'" + ) + seen[d] = app_id + result[app_id] = domains + return result + + def alias_domains_map(self, apps, primary_domain): + result = {} + # wir kΓΆnnen die canonical_map vorab holen… + canonical_map = self.canonical_domains_map(apps, primary_domain) + for app_id, app_cfg in apps.items(): + domains_cfg = app_cfg.get('domains', {}) or {} + aliases = self.alias_list( + domains_cfg, + app_id, + primary_domain, + canonical_domains=canonical_map.get(app_id), + ) + result[app_id] = aliases + return result diff --git a/filter_plugins/group_domain_filters.py b/filter_plugins/group_domain_filters.py index 99d00bba..c8b0f464 100644 --- a/filter_plugins/group_domain_filters.py +++ b/filter_plugins/group_domain_filters.py @@ -1,16 +1,9 @@ +from ansible.errors import AnsibleFilterError +import sys import os import yaml -from ansible.errors import AnsibleFilterError class FilterModule(object): - """ - Custom filters for conditional domain assignments, handling both direct group matches - and recursive role dependency resolution. - - Determines if a given application_id (domain_key) should have its domain added by checking: - - If domain_key is explicitly listed in group_names, or - - If domain_key matches any application_id of roles reachable from active groups via dependencies. - """ def filters(self): return { @@ -39,9 +32,27 @@ class FilterModule(object): result[domain_key] = domain_value return result - # Setup roles directory path - plugin_dir = os.path.dirname(__file__) - project_root = os.path.abspath(os.path.join(plugin_dir, '..')) + # Determine plugin directory based on filter plugin module if available + plugin_dir = None + for module in sys.modules.values(): + fm = getattr(module, 'FilterModule', None) + if fm is not None: + try: + # Access staticmethod, compare underlying function + if getattr(fm, 'add_domain_if_group') is DomainFilterUtil.add_domain_if_group: + plugin_dir = os.path.dirname(module.__file__) + break + except Exception: + continue + + if plugin_dir: + # The plugin_dir is the filter_plugins directory; project_root is one level up + project_root = os.path.abspath(os.path.join(plugin_dir, '..')) + else: + # Fallback: locate project root relative to this utility file + plugin_dir = os.path.dirname(__file__) + project_root = os.path.abspath(os.path.join(plugin_dir, '..')) + roles_dir = os.path.join(project_root, 'roles') # Collect all roles reachable from the active groups @@ -83,4 +94,4 @@ class FilterModule(object): return result except Exception as exc: - raise AnsibleFilterError(f"add_domain_if_group failed: {exc}") + raise AnsibleFilterError(f"add_domain_if_group failed: {exc}") \ No newline at end of file diff --git a/group_vars/all/03_domains.yml b/group_vars/all/03_domains.yml deleted file mode 100644 index 32446291..00000000 --- a/group_vars/all/03_domains.yml +++ /dev/null @@ -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: [] \ No newline at end of file diff --git a/group_vars/all/11_domains.yml b/group_vars/all/11_domains.yml new file mode 100644 index 00000000..df5a0ed0 --- /dev/null +++ b/group_vars/all/11_domains.yml @@ -0,0 +1,10 @@ +defaults_domains: "{{ defaults_applications | canonical_domains_map(primary_domain) }}" + +defaults_redirect_domain_mappings: >- + {{ [] + | add_redirect_if_group('lam', domains.ldap, domains.lam, group_names) + | add_redirect_if_group('phpmyldapadmin', domains.ldap, domains.phpmyldapadmin,group_names) + }} + +# Domains which are deprecated and should be cleaned up +deprecated_domains: [] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5c66c901..b4e3a594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip -simpleaudio numpy \ No newline at end of file diff --git a/requirements.yml b/requirements.yml index e533c761..ebd4a5d6 100644 --- a/requirements.yml +++ b/requirements.yml @@ -4,4 +4,6 @@ collections: pacman: - ansible - python-passlib - - python-pytest \ No newline at end of file + - python-pytest +yay: + - python-simpleaudio \ No newline at end of file diff --git a/roles/docker-akaunting/vars/configuration.yml b/roles/docker-akaunting/vars/configuration.yml index 8f1e6bdb..70375bcb 100644 --- a/roles/docker-akaunting/vars/configuration.yml +++ b/roles/docker-akaunting/vars/configuration.yml @@ -9,4 +9,8 @@ features: central_database: true credentials: # database_password: Needs to be defined in inventory file -# setup_admin_password: Needs to be defined in inventory file \ No newline at end of file +# setup_admin_password: Needs to be defined in inventory file +domains: + canonical: + - "accounting.{{ primary_domain }}" + diff --git a/roles/docker-attendize/vars/configuration.yml b/roles/docker-attendize/vars/configuration.yml index b001e32b..0cb96cbf 100644 --- a/roles/docker-attendize/vars/configuration.yml +++ b/roles/docker-attendize/vars/configuration.yml @@ -7,3 +7,7 @@ features: css: true portfolio_iframe: false central_database: true + +domains: + canonical: + - "tickets.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-baserow/vars/configuration.yml b/roles/docker-baserow/vars/configuration.yml index e1b198f0..bdff87c2 100644 --- a/roles/docker-baserow/vars/configuration.yml +++ b/roles/docker-baserow/vars/configuration.yml @@ -2,5 +2,5 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: true + portfolio_iframe: true central_database: true \ No newline at end of file diff --git a/roles/docker-bigbluebutton/vars/configuration.yml b/roles/docker-bigbluebutton/vars/configuration.yml index db9a5cdb..a207bd5c 100644 --- a/roles/docker-bigbluebutton/vars/configuration.yml +++ b/roles/docker-bigbluebutton/vars/configuration.yml @@ -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 \ No newline at end of file + central_database: false +domains: + canonical: + - "meet.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-bluesky/vars/configuration.yml b/roles/docker-bluesky/vars/configuration.yml index 08ff1015..a89b94d8 100644 --- a/roles/docker-bluesky/vars/configuration.yml +++ b/roles/docker-bluesky/vars/configuration.yml @@ -1,14 +1,15 @@ users: administrator: - email: "{{users.administrator.email}}" + email: "{{users.administrator.email}}" pds: - version: "latest" -credentials: - #jwt_secret: # Needs to be defined in inventory file - Use: openssl rand -base64 64 | tr -d '\n' - #plc_rotation_key_k256_private_key_hex: # Needs to be defined in inventory file - Use: openssl rand -hex 32 - #admin_password: # Needs to be defined in inventory file - Use: openssl rand -base64 16 + version: "latest" +credentials: features: - matomo: true - css: true + matomo: true + css: true portfolio_iframe: true - central_database: true \ No newline at end of file + central_database: true +domains: + canonical: + web: "bskyweb.{{ primary_domain }}" + api: "bluesky.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-discourse/vars/configuration.yml b/roles/docker-discourse/vars/configuration.yml index a2bacbd9..22b26e97 100644 --- a/roles/docker-discourse/vars/configuration.yml +++ b/roles/docker-discourse/vars/configuration.yml @@ -16,4 +16,7 @@ csp: unsafe-inline: true whitelist: font-src: - - "http://*.{{primary_domain}}" \ No newline at end of file + - "http://*.{{primary_domain}}" +domains: + canonical: + - "forum.{{ primary_domain }}" diff --git a/roles/docker-elk/templates/configuration.yml.j2 b/roles/docker-elk/templates/configuration.yml.j2 deleted file mode 100644 index e5559102..00000000 --- a/roles/docker-elk/templates/configuration.yml.j2 +++ /dev/null @@ -1,3 +0,0 @@ -# Jinja2 configuration template -# Define your variables here - diff --git a/roles/docker-elk/vars/configuration.yml b/roles/docker-elk/vars/configuration.yml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/roles/docker-elk/vars/configuration.yml @@ -0,0 +1 @@ + diff --git a/roles/docker-espocrm/vars/configuration.yml b/roles/docker-espocrm/vars/configuration.yml index 4950256b..4f30fe7e 100644 --- a/roles/docker-espocrm/vars/configuration.yml +++ b/roles/docker-espocrm/vars/configuration.yml @@ -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 \ No newline at end of file + unsafe-inline: true + unsafe-eval: true +domains: + aliases: + - "crm.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-friendica/vars/configuration.yml b/roles/docker-friendica/vars/configuration.yml index 656bed32..0ca77d11 100644 --- a/roles/docker-friendica/vars/configuration.yml +++ b/roles/docker-friendica/vars/configuration.yml @@ -2,6 +2,9 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: true + portfolio_iframe: true oidc: true - central_database: true \ No newline at end of file + central_database: true +domains: + aliases: + - "social.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-funkwhale/vars/configuration.yml b/roles/docker-funkwhale/vars/configuration.yml index e5c69d83..a7e25f9a 100644 --- a/roles/docker-funkwhale/vars/configuration.yml +++ b/roles/docker-funkwhale/vars/configuration.yml @@ -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 \ No newline at end of file +domains: + canonical: + - "audio.{{ primary_domain }}" + aliases: + - "music.{{ primary_domain }}" + - "sound.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-gitea/vars/configuration.yml b/roles/docker-gitea/vars/configuration.yml index cc16386a..78929fbb 100644 --- a/roles/docker-gitea/vars/configuration.yml +++ b/roles/docker-gitea/vars/configuration.yml @@ -22,4 +22,7 @@ csp: worker-src: - "blob:" manifest-src: - - "data:" \ No newline at end of file + - "data:" +domains: + aliases: + - "git.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-gitlab/vars/configuration.yml b/roles/docker-gitlab/vars/configuration.yml index e1b198f0..bdff87c2 100644 --- a/roles/docker-gitlab/vars/configuration.yml +++ b/roles/docker-gitlab/vars/configuration.yml @@ -2,5 +2,5 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: true + portfolio_iframe: true central_database: true \ No newline at end of file diff --git a/roles/docker-jenkins/vars/configuration.yml b/roles/docker-jenkins/vars/configuration.yml index e5559102..e69de29b 100644 --- a/roles/docker-jenkins/vars/configuration.yml +++ b/roles/docker-jenkins/vars/configuration.yml @@ -1,3 +0,0 @@ -# Jinja2 configuration template -# Define your variables here - diff --git a/roles/docker-joomla/vars/configuration.yml b/roles/docker-joomla/vars/configuration.yml index f8bd2793..e33fd0ee 100644 --- a/roles/docker-joomla/vars/configuration.yml +++ b/roles/docker-joomla/vars/configuration.yml @@ -2,4 +2,7 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: true \ No newline at end of file + portfolio_iframe: true +domains: + canonical: + - "cms.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-keycloak/vars/configuration.yml b/roles/docker-keycloak/vars/configuration.yml index 734bb1f2..7c546efc 100644 --- a/roles/docker-keycloak/vars/configuration.yml +++ b/roles/docker-keycloak/vars/configuration.yml @@ -16,4 +16,7 @@ csp: script-src: unsafe-inline: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "auth.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-lam/vars/configuration.yml b/roles/docker-lam/vars/configuration.yml index f38de6d7..0f5052fd 100644 --- a/roles/docker-lam/vars/configuration.yml +++ b/roles/docker-lam/vars/configuration.yml @@ -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 \ No newline at end of file + unsafe-inline: true + unsafe-eval: true +domains: + aliases: + - "ldap.{{primary_domain}}" + diff --git a/roles/docker-ldap/vars/configuration.yml b/roles/docker-ldap/vars/configuration.yml index fdb522ec..c2792830 100644 --- a/roles/docker-ldap/vars/configuration.yml +++ b/roles/docker-ldap/vars/configuration.yml @@ -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 \ No newline at end of file diff --git a/roles/docker-listmonk/vars/configuration.yml b/roles/docker-listmonk/vars/configuration.yml index 1d4fdbcf..ec362d8b 100644 --- a/roles/docker-listmonk/vars/configuration.yml +++ b/roles/docker-listmonk/vars/configuration.yml @@ -6,6 +6,9 @@ version: "latest" # Docker Image features: matomo: true css: true - portfolio_iframe: true + portfolio_iframe: true central_database: true - oidc: true \ No newline at end of file + oidc: true +domains: + canonical: + - "newsletter.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-mailu/vars/configuration.yml b/roles/docker-mailu/vars/configuration.yml index 356843ab..bdb5fbcd 100644 --- a/roles/docker-mailu/vars/configuration.yml +++ b/roles/docker-mailu/vars/configuration.yml @@ -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 \ No newline at end of file + central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary +domains: + canonical: + - "mail.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-mailu/vars/main.yml b/roles/docker-mailu/vars/main.yml index 2a89feda..52f8a81b 100644 --- a/roles/docker-mailu/vars/main.yml +++ b/roles/docker-mailu/vars/main.yml @@ -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] }}" \ No newline at end of file +domain: "{{ domains | get_domain(application_id) }}" +http_port: "{{ ports.localhost.http[application_id] }}" \ No newline at end of file diff --git a/roles/docker-mastodon/vars/configuration.yml b/roles/docker-mastodon/vars/configuration.yml index e908af2e..d5082a8e 100644 --- a/roles/docker-mastodon/vars/configuration.yml +++ b/roles/docker-mastodon/vars/configuration.yml @@ -1,19 +1,13 @@ -version: "latest" -single_user_mode: false # Set true for initial setup -setup: false # Set true in inventory file to execute the setup and initializing procedures -credentials: -# Check out the README.md of the docker-mastodon role to get detailled instructions about how to setup the credentials -# database_password: -# secret_key_base: -# otp_secret: -# vapid_private_key: -# vapid_public_key: -# active_record_encryption_deterministic_key: -# active_record_encryption_key_derivation_salt: -# active_record_encryption_primary_key: +version: "latest" +single_user_mode: false # Set true for initial setup +setup: false # Set true in inventory file to execute the setup and initializing procedures +credentials: features: - matomo: true - css: true - portfolio_iframe: false - oidc: true - central_database: true + matomo: true + css: true + portfolio_iframe: false + oidc: true + central_database: true +domains: + canonical: + - "microblog.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-matomo/vars/configuration.yml b/roles/docker-matomo/vars/configuration.yml index ccb5a924..b88b0fc0 100644 --- a/roles/docker-matomo/vars/configuration.yml +++ b/roles/docker-matomo/vars/configuration.yml @@ -2,7 +2,7 @@ version: "latest" features: matomo: true css: false - portfolio_iframe: false + portfolio_iframe: false central_database: true oauth2: false csp: @@ -16,4 +16,7 @@ csp: unsafe-inline: true unsafe-eval: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + aliases: + - "analytics.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-matrix/vars/configuration.yml b/roles/docker-matrix/vars/configuration.yml index d6f7fb5d..b38e5669 100644 --- a/roles/docker-matrix/vars/configuration.yml +++ b/roles/docker-matrix/vars/configuration.yml @@ -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 }}" \ No newline at end of file diff --git a/roles/docker-mediawiki/vars/configuration.yml b/roles/docker-mediawiki/vars/configuration.yml new file mode 100644 index 00000000..b8c41ba0 --- /dev/null +++ b/roles/docker-mediawiki/vars/configuration.yml @@ -0,0 +1,3 @@ +domains: + canonical: + - "wiki.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-moodle/vars/configuration.yml b/roles/docker-moodle/vars/configuration.yml index 34843923..8e5c4b37 100644 --- a/roles/docker-moodle/vars/configuration.yml +++ b/roles/docker-moodle/vars/configuration.yml @@ -22,4 +22,7 @@ csp: - "data:" - "blob:" script-src: - - "https://cdn.jsdelivr.net" \ No newline at end of file + - "https://cdn.jsdelivr.net" +domains: + canonical: + - "academy.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-mybb/vars/configuration.yml b/roles/docker-mybb/vars/configuration.yml index f8c2956e..5eb1cdc9 100644 --- a/roles/docker-mybb/vars/configuration.yml +++ b/roles/docker-mybb/vars/configuration.yml @@ -3,5 +3,5 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: false - central_database: true + portfolio_iframe: false + central_database: true \ No newline at end of file diff --git a/roles/docker-nextcloud/vars/configuration.yml b/roles/docker-nextcloud/vars/configuration.yml index 4ef8bbfd..83dc5f47 100644 --- a/roles/docker-nextcloud/vars/configuration.yml +++ b/roles/docker-nextcloud/vars/configuration.yml @@ -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 diff --git a/roles/docker-nextcloud/vars/plugins/bbb.yml b/roles/docker-nextcloud/vars/plugins/bbb.yml index 545fbfbf..9d5a22ee 100644 --- a/roles/docker-nextcloud/vars/plugins/bbb.yml +++ b/roles/docker-nextcloud/vars/plugins/bbb.yml @@ -4,4 +4,4 @@ plugin_configuration: configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}" - appid: "bbb" configkey: "api.url" - configvalue: "{{ applications.bigbluebutton.urls.api }}" \ No newline at end of file + configvalue: "{{ web_protocol }}://{{domains | get_domain(''bigbluebutton'')}}{{applications.bigbluebutton.api_suffix}}" \ No newline at end of file diff --git a/roles/docker-oauth2-proxy/vars/configuration.yml b/roles/docker-oauth2-proxy/vars/configuration.yml index 2d382ca6..7e5ad8bc 100644 --- a/roles/docker-oauth2-proxy/vars/configuration.yml +++ b/roles/docker-oauth2-proxy/vars/configuration.yml @@ -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 diff --git a/roles/docker-openproject/vars/configuration.yml b/roles/docker-openproject/vars/configuration.yml index 69c04e21..06f7d03d 100644 --- a/roles/docker-openproject/vars/configuration.yml +++ b/roles/docker-openproject/vars/configuration.yml @@ -16,4 +16,7 @@ features: csp: flags: script-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "project.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-peertube/vars/configuration.yml b/roles/docker-peertube/vars/configuration.yml index 1636ae6d..e3de3283 100644 --- a/roles/docker-peertube/vars/configuration.yml +++ b/roles/docker-peertube/vars/configuration.yml @@ -9,4 +9,9 @@ csp: script-src: unsafe-inline: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "video.{{ primary_domain }}" + aliases: + - "videos.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-phpldapadmin/vars/configuration.yml b/roles/docker-phpldapadmin/vars/configuration.yml index c6394085..7719b4bc 100644 --- a/roles/docker-phpldapadmin/vars/configuration.yml +++ b/roles/docker-phpldapadmin/vars/configuration.yml @@ -5,6 +5,6 @@ oauth2_proxy: features: matomo: true css: true - portfolio_iframe: false + portfolio_iframe: false ldap: true oauth2: true \ No newline at end of file diff --git a/roles/docker-phpmyadmin/vars/configuration.yml b/roles/docker-phpmyadmin/vars/configuration.yml index d20e4314..05ad7e48 100644 --- a/roles/docker-phpmyadmin/vars/configuration.yml +++ b/roles/docker-phpmyadmin/vars/configuration.yml @@ -6,7 +6,7 @@ oauth2_proxy: features: matomo: true css: false - portfolio_iframe: false + portfolio_iframe: false central_database: true oauth2: true hostname: central-mariadb @@ -15,4 +15,8 @@ csp: style-src: unsafe-inline: true script-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + aliases: + - "mysql.{{ primary_domain }}" + - "mariadb.{{ primary_domain }}" diff --git a/roles/docker-pixelfed/vars/configuration.yml b/roles/docker-pixelfed/vars/configuration.yml index aa6aaefe..a0523ae8 100644 --- a/roles/docker-pixelfed/vars/configuration.yml +++ b/roles/docker-pixelfed/vars/configuration.yml @@ -3,7 +3,7 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: false + portfolio_iframe: false central_database: true csp: flags: @@ -11,4 +11,9 @@ csp: unsafe-inline: true unsafe-eval: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "picture.{{ primary_domain }}" + aliases: + - "pictures.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-portfolio/vars/configuration.yml b/roles/docker-portfolio/vars/configuration.yml index c16f6f44..1735c61b 100644 --- a/roles/docker-portfolio/vars/configuration.yml +++ b/roles/docker-portfolio/vars/configuration.yml @@ -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 }}" + diff --git a/roles/docker-presentation/vars/configuration.yml b/roles/docker-presentation/vars/configuration.yml index eebdcda9..99e0df89 100644 --- a/roles/docker-presentation/vars/configuration.yml +++ b/roles/docker-presentation/vars/configuration.yml @@ -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 \ No newline at end of file + unsafe-eval: true +domains: + canonical: + - "slides.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-roulette-wheel/vars/configuration.yml b/roles/docker-roulette-wheel/vars/configuration.yml new file mode 100644 index 00000000..0af1d539 --- /dev/null +++ b/roles/docker-roulette-wheel/vars/configuration.yml @@ -0,0 +1,3 @@ +domains: + canonical: + - "wheel.{{ primary_domain }}" diff --git a/roles/docker-snipe_it/vars/configuration.yml b/roles/docker-snipe_it/vars/configuration.yml index 0b765ad7..b2e985d2 100644 --- a/roles/docker-snipe_it/vars/configuration.yml +++ b/roles/docker-snipe_it/vars/configuration.yml @@ -2,5 +2,8 @@ version: "latest" features: matomo: true css: true - portfolio_iframe: false - central_database: true \ No newline at end of file + portfolio_iframe: false + central_database: true +domains: + canonical: + - "inventory.{{ primary_domain }}" diff --git a/roles/docker-sphinx/vars/configuration.yml b/roles/docker-sphinx/vars/configuration.yml index c19bde52..90bca16c 100644 --- a/roles/docker-sphinx/vars/configuration.yml +++ b/roles/docker-sphinx/vars/configuration.yml @@ -1,6 +1,6 @@ features: - matomo: true - css: true + matomo: true + css: true portfolio_iframe: false csp: flags: @@ -8,4 +8,7 @@ csp: unsafe-inline: true unsafe-eval: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "docs.{{ primary_domain }}" diff --git a/roles/docker-taiga/vars/configuration.yml b/roles/docker-taiga/vars/configuration.yml index 09eefe31..0cc74d9c 100644 --- a/roles/docker-taiga/vars/configuration.yml +++ b/roles/docker-taiga/vars/configuration.yml @@ -19,4 +19,7 @@ csp: unsafe-inline: true unsafe-eval: true style-src: - unsafe-inline: true \ No newline at end of file + unsafe-inline: true +domains: + canonical: + - "kanban.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-wordpress/vars/configuration.yml b/roles/docker-wordpress/vars/configuration.yml index 7a02d4d5..9632be1b 100644 --- a/roles/docker-wordpress/vars/configuration.yml +++ b/roles/docker-wordpress/vars/configuration.yml @@ -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" \ No newline at end of file + - "https://fonts.bunny.net" +domains: + canonical: + - "blog.{{ primary_domain }}" diff --git a/roles/docker-xmpp/vars/configuration.yml b/roles/docker-xmpp/vars/configuration.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/docker-yourls/vars/configuration.yml b/roles/docker-yourls/vars/configuration.yml index 360bcbea..ed394205 100644 --- a/roles/docker-yourls/vars/configuration.yml +++ b/roles/docker-yourls/vars/configuration.yml @@ -9,6 +9,11 @@ oauth2_proxy: features: matomo: true css: true - portfolio_iframe: false + portfolio_iframe: false central_database: true - oauth2: true \ No newline at end of file + oauth2: true +domains: + canonical: + - "s.{{ primary_domain }}" + aliases: + - "short.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/nginx-serve-files/vars/configuration.yml b/roles/nginx-serve-files/vars/configuration.yml index 5cad484b..3be8d0e3 100644 --- a/roles/nginx-serve-files/vars/configuration.yml +++ b/roles/nginx-serve-files/vars/configuration.yml @@ -1,4 +1,7 @@ features: matomo: true css: true - portfolio_iframe: true \ No newline at end of file + portfolio_iframe: true +domains: + canonical: + - "files.{{ primary_domain }}" diff --git a/roles/nginx-serve-html/vars/configuration.yml b/roles/nginx-serve-html/vars/configuration.yml index f30a6270..36ee1f3c 100644 --- a/roles/nginx-serve-html/vars/configuration.yml +++ b/roles/nginx-serve-html/vars/configuration.yml @@ -1,4 +1,7 @@ features: - matomo: true - css: true - portfolio_iframe: false \ No newline at end of file + matomo: true + css: true + portfolio_iframe: false +domains: + canonical: + - "html.{{ primary_domain }}" diff --git a/tasks/constructor.yml b/tasks/constructor.yml index bb218357..febf1471 100644 --- a/tasks/constructor.yml +++ b/tasks/constructor.yml @@ -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: diff --git a/tests/integration/test_domain_uniqueness.py b/tests/integration/test_domain_uniqueness.py new file mode 100644 index 00000000..9b0b46f1 --- /dev/null +++ b/tests/integration/test_domain_uniqueness.py @@ -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() diff --git a/tests/integration/test_domains_structure.py b/tests/integration/test_domains_structure.py new file mode 100644 index 00000000..ae358b94 --- /dev/null +++ b/tests/integration/test_domains_structure.py @@ -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() diff --git a/tests/unit/test_domain_filters.py b/tests/unit/test_domain_filters.py new file mode 100644 index 00000000..c42310b0 --- /dev/null +++ b/tests/unit/test_domain_filters.py @@ -0,0 +1,123 @@ +import os +import sys +import unittest + +# Add the filter_plugins directory to the import path +dir_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), '../../filter_plugins') +) +sys.path.insert(0, dir_path) + +from ansible.errors import AnsibleFilterError +from domain_filters import FilterModule + +class TestDomainFilters(unittest.TestCase): + def setUp(self): + self.filter_module = FilterModule() + # Sample primary domain + self.primary = 'example.com' + + def test_canonical_empty_apps(self): + apps = {} + expected = {} + result = self.filter_module.canonical_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_canonical_without_domains(self): + apps = {'app1': {}} + expected = {'app1': ['app1.example.com']} + result = self.filter_module.canonical_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_canonical_with_list(self): + apps = { + 'app1': { + 'domains': {'canonical': ['foo.com', 'bar.com']} + } + } + result = self.filter_module.canonical_domains_map(apps, self.primary) + self.assertCountEqual(result['app1'], ['foo.com', 'bar.com']) + + def test_canonical_with_dict(self): + apps = { + 'app1': { + 'domains': {'canonical': {'one': 'one.com', 'two': 'two.com'}} + } + } + result = self.filter_module.canonical_domains_map(apps, self.primary) + self.assertCountEqual(result['app1'], ['one.com', 'two.com']) + + def test_canonical_duplicate_raises(self): + apps = { + 'app1': {'domains': {'canonical': ['dup.com']}}, + 'app2': {'domains': {'canonical': ['dup.com']}}, + } + with self.assertRaises(AnsibleFilterError) as cm: + self.filter_module.canonical_domains_map(apps, self.primary) + self.assertIn("configured for both", str(cm.exception)) + + def test_alias_empty_apps(self): + apps = {} + expected = {} + result = self.filter_module.alias_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_alias_without_aliases_and_no_canonical(self): + apps = {'app1': {}} + # canonical defaults to ['app1.example.com'], so alias should be [] + expected = {'app1': []} + result = self.filter_module.alias_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_alias_with_explicit_aliases(self): + apps = { + 'app1': { + 'domains': {'aliases': ['alias.com']} + } + } + # canonical defaults to ['app1.example.com'], so alias should include alias.com and default + expected = {'app1': ['alias.com', 'app1.example.com']} + result = self.filter_module.alias_domains_map(apps, self.primary) + self.assertCountEqual(result['app1'], expected['app1']) + + def test_alias_with_canonical_not_default(self): + apps = { + 'app1': { + 'domains': {'canonical': ['foo.com']} + } + } + # foo.com is canonical, default not in canonical so added as alias + expected = {'app1': ['app1.example.com']} + result = self.filter_module.alias_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_alias_with_existing_default(self): + apps = { + 'app1': { + 'domains': { + 'canonical': ['foo.com'], + 'aliases': ['app1.example.com'] + } + } + } + # default present in aliases, should not be duplicated + expected = {'app1': ['app1.example.com']} + result = self.filter_module.alias_domains_map(apps, self.primary) + self.assertEqual(result, expected) + + def test_invalid_canonical_type(self): + apps = { + 'app1': {'domains': {'canonical': 123}} + } + with self.assertRaises(AnsibleFilterError): + self.filter_module.canonical_domains_map(apps, self.primary) + + def test_invalid_aliases_type(self): + apps = { + 'app1': {'domains': {'aliases': 123}} + } + with self.assertRaises(AnsibleFilterError): + self.filter_module.alias_domains_map(apps, self.primary) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_generate_default_applications.py b/tests/unit/test_generate_default_applications.py index 9d325fb9..92c7186d 100644 --- a/tests/unit/test_generate_default_applications.py +++ b/tests/unit/test_generate_default_applications.py @@ -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)