mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-06-25 19:55:31 +02:00
Compare commits
No commits in common. "03f3a31d217673d943241a2f7097090708295d28" and "cc3f5d75ea02142594018944d17a3fd87f5deed7" have entirely different histories.
03f3a31d21
...
cc3f5d75ea
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
|||||||
ROLES_DIR := ./roles
|
ROLES_DIR := ./roles
|
||||||
APPLICATIONS_OUT := ./group_vars/all/03_applications.yml
|
APPLICATIONS_OUT := ./group_vars/all/11_applications.yml
|
||||||
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
|
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
|
||||||
INCLUDES_OUT := ./tasks/include-docker-roles.yml
|
INCLUDES_OUT := ./tasks/include-docker-roles.yml
|
||||||
INCLUDES_SCRIPT := ./cli/generate-role-includes.py
|
INCLUDES_SCRIPT := ./cli/generate-role-includes.py
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
lookup_plugins = ./lookup_plugins
|
lookup_plugins = ./lookup_plugins
|
||||||
filter_plugins = ./filter_plugins
|
|
@ -15,7 +15,7 @@ def load_yaml_file(path):
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Generate defaults_applications YAML from docker roles.")
|
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("--roles-dir", default="roles", help="Path to the roles directory (default: roles)")
|
||||||
parser.add_argument("--output-file", default="group_vars/all/03_applications.yml", help="Path to output YAML file")
|
parser.add_argument("--output-file", default="group_vars/all/11_applications.yml", help="Path to output YAML file")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
## 📖 CyMaIS.Cloud Ansible & Python Directory Guide
|
|
||||||
|
|
||||||
This document provides a **decision matrix** for when to use each default Ansible plugin and module directory in the context of **CyMaIS.Cloud development** with Ansible and Python. It links to official docs, explains use-cases, and points back to our conversation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔗 Links & References
|
|
||||||
|
|
||||||
* Official Ansible Plugin Guide: [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html)
|
|
||||||
* Official Ansible Module Guide: [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html)
|
|
||||||
* This conversation: [Link to this conversation](https://chat.openai.com/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🛠️ Repo Layout & Default Directories
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
ansible-repo/
|
|
||||||
├── library/ # 📦 Custom Ansible modules
|
|
||||||
├── filter_plugins/ # 🔍 Custom Jinja2 filters
|
|
||||||
├── lookup_plugins/ # 👉 Custom lookup plugins
|
|
||||||
├── module_utils/ # 🛠️ Shared Python helpers for modules
|
|
||||||
├── action_plugins/ # ⚙️ Task-level orchestration logic
|
|
||||||
├── callback_plugins/ # 📣 Event callbacks (logging, notifications)
|
|
||||||
├── inventory_plugins/ # 🌐 Dynamic inventory sources
|
|
||||||
├── strategy_plugins/ # 🧠 Task execution strategies
|
|
||||||
└── ... # Other plugin dirs (connection, cache, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🎯 Decision Matrix: Which Folder for What?
|
|
||||||
|
|
||||||
| Folder | Type | Use-Case | Example (CyMaIS.Cloud) | Emoji |
|
|
||||||
| -------------------- | -------------------- | ---------------------------------------- | ----------------------------------------------------- | ----- |
|
|
||||||
| `library/` | **Module** | Write idempotent actions | `cloud_network.py`: manage VPCs, subnets | 📦 |
|
|
||||||
| `filter_plugins/` | **Filter plugin** | Jinja2 data transforms in templates/vars | `to_camel_case.py`: convert keys for API calls | 🔍 |
|
|
||||||
| `lookup_plugins/` | **Lookup plugin** | Fetch external/secure data at runtime | `vault_lookup.py`: pull secrets from CyMaIS Vault | 👉 |
|
|
||||||
| `module_utils/` | **Utility library** | Shared Python code for modules | `cymais_client.py`: common API client base class | 🛠️ |
|
|
||||||
| `action_plugins/` | **Action plugin** | Complex task orchestration wrappers | `deploy_stack.py`: sequence Terraform + Ansible steps | ⚙️ |
|
|
||||||
| `callback_plugins/` | **Callback plugin** | Customize log/report behavior | `notify_slack.py`: send playbook status to Slack | 📣 |
|
|
||||||
| `inventory_plugins/` | **Inventory plugin** | Dynamic host/group sources | `azure_inventory.py`: list hosts from Azure tags | 🌐 |
|
|
||||||
| `strategy_plugins/` | **Strategy plugin** | Control task execution order/parallelism | `rolling_batch.py`: phased rollout of VMs | 🧠 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📝 Detailed Guidance
|
|
||||||
|
|
||||||
1. **library/** 📦
|
|
||||||
|
|
||||||
* **When?** Implement **one-off, idempotent actions** (create/delete cloud resources).
|
|
||||||
* **Why?** Modules under `library/` are first in search path for `ansible` modules.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html)
|
|
||||||
|
|
||||||
2. **filter\_plugins/** 🔍
|
|
||||||
|
|
||||||
* **When?** You need **data manipulation** (lists, strings, dicts) inside Jinja2.
|
|
||||||
* **Why?** Extends `|` filters in templates and variable declarations.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#filter-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#filter-plugins)
|
|
||||||
|
|
||||||
3. **lookup\_plugins/** 👉
|
|
||||||
|
|
||||||
* **When?** You must **retrieve secret/external data** during playbook compile/runtime.
|
|
||||||
* **Why?** Lookup plugins run before tasks, enabling dynamic variable resolution.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#lookup-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#lookup-plugins)
|
|
||||||
|
|
||||||
4. **module\_utils/** 🛠️
|
|
||||||
|
|
||||||
* **When?** Multiple modules share **common Python code** (HTTP clients, validation).
|
|
||||||
* **Why?** Avoid code duplication; modules import these utilities.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_modules.html#module-utils](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html#module-utils)
|
|
||||||
|
|
||||||
5. **action\_plugins/** ⚙️
|
|
||||||
|
|
||||||
* **When?** You need to **wrap or extend** module behavior at task invocation time.
|
|
||||||
* **Why?** Provides hooks before/after module execution.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#action-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#action-plugins)
|
|
||||||
|
|
||||||
6. **callback\_plugins/** 📣
|
|
||||||
|
|
||||||
* **When?** You want **custom event handlers** (logging, progress, notifications).
|
|
||||||
* **Why?** Receive play/task events for custom output.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#callback-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#callback-plugins)
|
|
||||||
|
|
||||||
7. **inventory\_plugins/** 🌐
|
|
||||||
|
|
||||||
* **When?** Hosts/groups come from **dynamic sources** (cloud APIs, databases).
|
|
||||||
* **Why?** Replace static `inventory.ini` with code-driven inventories.
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#inventory-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#inventory-plugins)
|
|
||||||
|
|
||||||
8. **strategy\_plugins/** 🧠
|
|
||||||
|
|
||||||
* **When?** You need to **customize execution strategy** (parallelism, ordering).
|
|
||||||
* **Why?** Override default `linear` strategy (e.g., `free`, custom batches).
|
|
||||||
* **Docs:** [https://docs.ansible.com/ansible/latest/dev\_guide/developing\_plugins.html#strategy-plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#strategy-plugins)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🚀 CyMaIS.Cloud Best Practices
|
|
||||||
|
|
||||||
* **Organize modules** by service under `library/cloud/` (e.g., `vm`, `network`, `storage`).
|
|
||||||
* **Shared client code** in `module_utils/cymais/` for authentication, request handling.
|
|
||||||
* **Secrets lookup** via `lookup_plugins/vault_lookup.py` pointing to CyMaIS Vault.
|
|
||||||
* **Filters** to normalize data formats from cloud APIs (e.g., `snake_to_camel`).
|
|
||||||
* **Callbacks** to stream playbook results into CyMaIS Monitoring.
|
|
||||||
|
|
||||||
Use this matrix as your **single source of truth** when extending Ansible for CyMaIS.Cloud! 👍
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This matrix was created with the help of ChatGPT 🤖—see our conversation [here](https://chatgpt.com/canvas/shared/682b1a62d6dc819184ecdc696c51290a).
|
|
@ -1,86 +0,0 @@
|
|||||||
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
|
|
@ -1,75 +0,0 @@
|
|||||||
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: dict, feature: str, application_id: str) -> bool:
|
def is_feature_enabled(applications, feature: str, application_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if applications[application_id].features[feature] is truthy.
|
Check if a generic feature is enabled for the given application.
|
||||||
"""
|
"""
|
||||||
app = applications.get(application_id, {})
|
app = applications.get(application_id, {})
|
||||||
return bool(app.get('features', {}).get(feature, False))
|
return bool(app.get('features', {}).get(feature, False))
|
||||||
@ -31,4 +31,4 @@ class FilterModule(object):
|
|||||||
return {
|
return {
|
||||||
'is_feature_enabled': is_feature_enabled,
|
'is_feature_enabled': is_feature_enabled,
|
||||||
'get_docker_compose': get_docker_compose,
|
'get_docker_compose': get_docker_compose,
|
||||||
}
|
}
|
@ -12,10 +12,9 @@ class FilterModule(object):
|
|||||||
'build_csp_header': self.build_csp_header,
|
'build_csp_header': self.build_csp_header,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
def is_feature_enabled(self, applications, feature: str, application_id: str) -> bool:
|
||||||
def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Return True if applications[application_id].features[feature] is truthy.
|
Check if a generic feature is enabled for the given application.
|
||||||
"""
|
"""
|
||||||
app = applications.get(application_id, {})
|
app = applications.get(application_id, {})
|
||||||
return bool(app.get('features', {}).get(feature, False))
|
return bool(app.get('features', {}).get(feature, False))
|
||||||
@ -100,7 +99,7 @@ class FilterModule(object):
|
|||||||
|
|
||||||
for directive in directives:
|
for directive in directives:
|
||||||
tokens = ["'self'"]
|
tokens = ["'self'"]
|
||||||
|
|
||||||
# unsafe-eval / unsafe-inline flags
|
# unsafe-eval / unsafe-inline flags
|
||||||
flags = self.get_csp_flags(applications, application_id, directive)
|
flags = self.get_csp_flags(applications, application_id, directive)
|
||||||
tokens += flags
|
tokens += flags
|
||||||
|
69
filter_plugins/domain_filters.py
Normal file
69
filter_plugins/domain_filters.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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
|
@ -1,94 +0,0 @@
|
|||||||
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
|
|
@ -1,31 +0,0 @@
|
|||||||
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}")
|
|
@ -1,37 +0,0 @@
|
|||||||
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,9 +1,16 @@
|
|||||||
from ansible.errors import AnsibleFilterError
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
|
||||||
class FilterModule(object):
|
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):
|
def filters(self):
|
||||||
return {
|
return {
|
||||||
@ -32,27 +39,9 @@ class FilterModule(object):
|
|||||||
result[domain_key] = domain_value
|
result[domain_key] = domain_value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Determine plugin directory based on filter plugin module if available
|
# Setup roles directory path
|
||||||
plugin_dir = None
|
plugin_dir = os.path.dirname(__file__)
|
||||||
for module in sys.modules.values():
|
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
|
||||||
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')
|
roles_dir = os.path.join(project_root, 'roles')
|
||||||
|
|
||||||
# Collect all roles reachable from the active groups
|
# Collect all roles reachable from the active groups
|
||||||
@ -94,4 +83,4 @@ class FilterModule(object):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")
|
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")
|
||||||
|
73
group_vars/all/03_domains.yml
Normal file
73
group_vars/all/03_domains.yml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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: []
|
@ -1,6 +0,0 @@
|
|||||||
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,2 +1,3 @@
|
|||||||
colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
|
colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
|
||||||
|
simpleaudio
|
||||||
numpy
|
numpy
|
@ -4,6 +4,4 @@ collections:
|
|||||||
pacman:
|
pacman:
|
||||||
- ansible
|
- ansible
|
||||||
- python-passlib
|
- python-passlib
|
||||||
- python-pytest
|
- python-pytest
|
||||||
yay:
|
|
||||||
- python-simpleaudio
|
|
@ -9,8 +9,4 @@ features:
|
|||||||
central_database: true
|
central_database: true
|
||||||
credentials:
|
credentials:
|
||||||
# database_password: Needs to be defined in inventory file
|
# database_password: Needs to be defined in inventory file
|
||||||
# setup_admin_password: Needs to be defined in inventory file
|
# setup_admin_password: Needs to be defined in inventory file
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "accounting.{{ primary_domain }}"
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
credentials:
|
credentials:
|
||||||
features:
|
# database_password: Password for the database
|
||||||
matomo: true
|
|
||||||
css: true
|
|
||||||
portfolio_iframe: false
|
|
||||||
central_database: true
|
|
||||||
|
|
||||||
domains:
|
features:
|
||||||
canonical:
|
matomo: true
|
||||||
- "tickets.{{ primary_domain }}"
|
css: true
|
||||||
|
portfolio_iframe: false
|
||||||
|
central_database: true
|
||||||
|
@ -2,5 +2,5 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
central_database: true
|
central_database: true
|
@ -1,17 +1,21 @@
|
|||||||
enable_greenlight: "true"
|
enable_greenlight: "true"
|
||||||
setup: false
|
setup: false # Set to true in inventory file for initial setup
|
||||||
credentials:
|
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:
|
database:
|
||||||
name: "multiple_databases"
|
name: "multiple_databases"
|
||||||
username: "postgres2"
|
username: "postgres2"
|
||||||
api_suffix: "/bigbluebutton/"
|
urls:
|
||||||
|
api: "{{ web_protocol }}://{{domains | get_domain('bigbluebutton')}}/bigbluebutton/" # API Address used by Nextcloud Integration
|
||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
ldap: false
|
ldap: false
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: false
|
central_database: false
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "meet.{{ primary_domain }}"
|
|
@ -1,15 +1,14 @@
|
|||||||
users:
|
users:
|
||||||
administrator:
|
administrator:
|
||||||
email: "{{users.administrator.email}}"
|
email: "{{users.administrator.email}}"
|
||||||
pds:
|
pds:
|
||||||
version: "latest"
|
version: "latest"
|
||||||
credentials:
|
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:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
central_database: true
|
central_database: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
web: "bskyweb.{{ primary_domain }}"
|
|
||||||
api: "bluesky.{{ primary_domain }}"
|
|
@ -16,7 +16,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
- "http://*.{{primary_domain}}"
|
- "http://*.{{primary_domain}}"
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "forum.{{ primary_domain }}"
|
|
3
roles/docker-elk/templates/configuration.yml.j2
Normal file
3
roles/docker-elk/templates/configuration.yml.j2
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Jinja2 configuration template
|
||||||
|
# Define your variables here
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,22 +1,19 @@
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
users:
|
users:
|
||||||
administrator:
|
administrator:
|
||||||
username: "{{ users.administrator.username }}"
|
username: "{{ users.administrator.username }}"
|
||||||
email: "{{ users.administrator.email }}"
|
email: "{{ users.administrator.email }}"
|
||||||
|
|
||||||
credentials:
|
credentials:
|
||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
ldap: false
|
ldap: false
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "crm.{{ primary_domain }}"
|
|
@ -2,9 +2,6 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "social.{{ primary_domain }}"
|
|
@ -6,9 +6,5 @@ features:
|
|||||||
ldap: true
|
ldap: true
|
||||||
central_database: true
|
central_database: true
|
||||||
credentials:
|
credentials:
|
||||||
domains:
|
# database_password: # Needs to be defined in inventory file
|
||||||
canonical:
|
# django_secret: # Needs to be defined in inventory file
|
||||||
- "audio.{{ primary_domain }}"
|
|
||||||
aliases:
|
|
||||||
- "music.{{ primary_domain }}"
|
|
||||||
- "sound.{{ primary_domain }}"
|
|
@ -22,7 +22,4 @@ csp:
|
|||||||
worker-src:
|
worker-src:
|
||||||
- "blob:"
|
- "blob:"
|
||||||
manifest-src:
|
manifest-src:
|
||||||
- "data:"
|
- "data:"
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "git.{{ primary_domain }}"
|
|
@ -2,5 +2,5 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
central_database: true
|
central_database: true
|
@ -0,0 +1,3 @@
|
|||||||
|
# Jinja2 configuration template
|
||||||
|
# Define your variables here
|
||||||
|
|
@ -2,7 +2,4 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "cms.{{ primary_domain }}"
|
|
@ -16,7 +16,4 @@ csp:
|
|||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "auth.{{ primary_domain }}"
|
|
@ -1,23 +1,21 @@
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
oauth2_proxy:
|
oauth2_proxy:
|
||||||
application: application
|
application: application # Needs to be the same as webinterface
|
||||||
port: 80
|
port: 80 # application port
|
||||||
credentials:
|
credentials:
|
||||||
|
# oauth2_proxy_cookie_secret: None # Set via openssl rand -hex 16
|
||||||
|
# administrator_password: "None" # CHANGE for security reasons
|
||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
ldap: true
|
ldap: true
|
||||||
central_database: false
|
central_database: false
|
||||||
oauth2: false
|
oauth2: false
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "ldap.{{primary_domain}}"
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
network:
|
network:
|
||||||
local: True # Activates local network. Necessary for LDIF import routines
|
local: True # Activates local network. Necessary for LDIF import routines
|
||||||
docker: True # Activates docker network to allow other docker containers to connect
|
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
|
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
|
hostname: "ldap" # Hostname of the LDAP Server in the central_ldap network
|
||||||
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
|
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
|
||||||
users:
|
users:
|
||||||
administrator:
|
administrator:
|
||||||
username: "{{users.administrator.username}}" # Administrator username
|
username: "{{users.administrator.username}}" # Administrator username
|
||||||
credentials:
|
credentials:
|
||||||
features:
|
features:
|
||||||
ldap: true
|
ldap: true
|
||||||
|
@ -6,9 +6,6 @@ version: "latest" # Docker Image
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "newsletter.{{ primary_domain }}"
|
|
@ -7,12 +7,14 @@ oidc:
|
|||||||
enable_user_creation: true # Users will be created if not existing
|
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 @
|
domain: "{{primary_domain}}" # The main domain from which mails will be send \ email suffix behind @
|
||||||
credentials:
|
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:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: 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
|
oidc: true
|
||||||
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "mail.{{ primary_domain }}"
|
|
@ -1,14 +1,14 @@
|
|||||||
application_id: "mailu"
|
application_id: "mailu"
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
database_password: "{{applications.mailu.credentials.database_password}}"
|
database_password: "{{applications.mailu.credentials.database_password}}"
|
||||||
database_type: "mariadb"
|
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
|
# Use dedicated source for oidc if activated
|
||||||
# @see https://github.com/heviat/Mailu-OIDC/tree/2024.06
|
# @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) }}"
|
domain: "{{ domains | get_domain(application_id) }}"
|
||||||
http_port: "{{ ports.localhost.http[application_id] }}"
|
http_port: "{{ ports.localhost.http[application_id] }}"
|
@ -1,13 +1,19 @@
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
single_user_mode: false # Set true for initial setup
|
single_user_mode: false # Set true for initial setup
|
||||||
setup: false # Set true in inventory file to execute the setup and initializing procedures
|
setup: false # Set true in inventory file to execute the setup and initializing procedures
|
||||||
credentials:
|
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:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "microblog.{{ primary_domain }}"
|
|
||||||
|
@ -2,7 +2,7 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: false
|
oauth2: false
|
||||||
csp:
|
csp:
|
||||||
@ -16,7 +16,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "analytics.{{ primary_domain }}"
|
|
@ -25,9 +25,9 @@ csp:
|
|||||||
whitelist:
|
whitelist:
|
||||||
connect-src:
|
connect-src:
|
||||||
- "{{ primary_domain }}"
|
- "{{ primary_domain }}"
|
||||||
- "matrix.{{ primary_domain }}"
|
- "{{ domains.matrix.synapse | safe_var }}"
|
||||||
script-src:
|
script-src:
|
||||||
- "element.{{ primary_domain }}"
|
- "{{ domains.matrix.synapse | safe_var }}"
|
||||||
- "https://cdn.jsdelivr.net"
|
- "https://cdn.jsdelivr.net"
|
||||||
plugins:
|
plugins:
|
||||||
# You need to enable them in the inventory file
|
# You need to enable them in the inventory file
|
||||||
@ -39,8 +39,3 @@ plugins:
|
|||||||
slack: false
|
slack: false
|
||||||
telegram: false
|
telegram: false
|
||||||
whatsapp: false
|
whatsapp: false
|
||||||
|
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
synapse: "matrix.{{ primary_domain }}"
|
|
||||||
element: "element.{{ primary_domain }}"
|
|
@ -1,3 +0,0 @@
|
|||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "wiki.{{ primary_domain }}"
|
|
@ -22,7 +22,4 @@ csp:
|
|||||||
- "data:"
|
- "data:"
|
||||||
- "blob:"
|
- "blob:"
|
||||||
script-src:
|
script-src:
|
||||||
- "https://cdn.jsdelivr.net"
|
- "https://cdn.jsdelivr.net"
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "academy.{{ primary_domain }}"
|
|
@ -3,5 +3,5 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
version: "production" # @see https://nextcloud.com/blog/nextcloud-release-channels-and-how-to-track-them/
|
version: "production" # @see https://nextcloud.com/blog/nextcloud-release-channels-and-how-to-track-them/
|
||||||
|
ldap:
|
||||||
|
enabled: True # Enables LDAP by default
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
@ -8,10 +10,6 @@ csp:
|
|||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
- "data:"
|
- "data:"
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "cloud.{{ primary_domain }}"
|
|
||||||
|
|
||||||
oidc:
|
oidc:
|
||||||
enabled: "{{ applications.nextcloud.features.oidc | default(true) }}" # Activate OIDC for Nextcloud
|
enabled: "{{ applications.nextcloud.features.oidc | default(true) }}" # Activate OIDC for Nextcloud
|
||||||
# floavor decides which OICD plugin should be used.
|
# floavor decides which OICD plugin should be used.
|
||||||
@ -25,7 +23,7 @@ credentials:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
ldap: true
|
ldap: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
@ -4,4 +4,4 @@ plugin_configuration:
|
|||||||
configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}"
|
configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}"
|
||||||
- appid: "bbb"
|
- appid: "bbb"
|
||||||
configkey: "api.url"
|
configkey: "api.url"
|
||||||
configvalue: "{{ web_protocol }}://{{domains | get_domain(''bigbluebutton'')}}{{applications.bigbluebutton.api_suffix}}"
|
configvalue: "{{ applications.bigbluebutton.urls.api }}"
|
@ -1,6 +1,7 @@
|
|||||||
configuration_file: "oauth2-proxy-keycloak.cfg" # Needs to be set true in the roles which use it
|
configuration_file: "oauth2-proxy-keycloak.cfg" # Needs to be set true in the roles which use it
|
||||||
version: "latest" # Docker Image version
|
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
|
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
|
||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
|
@ -16,7 +16,4 @@ features:
|
|||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "project.{{ primary_domain }}"
|
|
@ -9,9 +9,4 @@ csp:
|
|||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "video.{{ primary_domain }}"
|
|
||||||
aliases:
|
|
||||||
- "videos.{{ primary_domain }}"
|
|
@ -5,6 +5,6 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
ldap: true
|
ldap: true
|
||||||
oauth2: true
|
oauth2: true
|
@ -6,7 +6,7 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
hostname: central-mariadb
|
hostname: central-mariadb
|
||||||
@ -15,8 +15,4 @@ csp:
|
|||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
aliases:
|
|
||||||
- "mysql.{{ primary_domain }}"
|
|
||||||
- "mariadb.{{ primary_domain }}"
|
|
@ -3,7 +3,7 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
@ -11,9 +11,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "picture.{{ primary_domain }}"
|
|
||||||
aliases:
|
|
||||||
- "pictures.{{ primary_domain }}"
|
|
@ -1,6 +1,6 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
@ -19,7 +19,3 @@ csp:
|
|||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "{{ primary_domain }}"
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
|
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
@ -18,7 +18,4 @@ csp:
|
|||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
script-src:
|
script-src:
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "slides.{{ primary_domain }}"
|
|
@ -1,3 +0,0 @@
|
|||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "wheel.{{ primary_domain }}"
|
|
@ -2,8 +2,5 @@ version: "latest"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "inventory.{{ primary_domain }}"
|
|
@ -1,6 +1,6 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
@ -8,7 +8,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "docs.{{ primary_domain }}"
|
|
@ -19,7 +19,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "kanban.{{ primary_domain }}"
|
|
@ -31,9 +31,6 @@ csp:
|
|||||||
- "https://fonts.bunny.net"
|
- "https://fonts.bunny.net"
|
||||||
script-src:
|
script-src:
|
||||||
- "https://cdn.gtranslate.net"
|
- "https://cdn.gtranslate.net"
|
||||||
- "blog.{{ primary_domain }}"
|
- "{{ domains | get_domain('wordpress') }}"
|
||||||
style-src:
|
style-src:
|
||||||
- "https://fonts.bunny.net"
|
- "https://fonts.bunny.net"
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "blog.{{ primary_domain }}"
|
|
@ -9,11 +9,6 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "s.{{ primary_domain }}"
|
|
||||||
aliases:
|
|
||||||
- "short.{{ primary_domain }}"
|
|
@ -1,7 +1,4 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: true
|
portfolio_iframe: true
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "files.{{ primary_domain }}"
|
|
@ -1,7 +1,4 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
portfolio_iframe: false
|
portfolio_iframe: false
|
||||||
domains:
|
|
||||||
canonical:
|
|
||||||
- "html.{{ primary_domain }}"
|
|
@ -8,11 +8,6 @@
|
|||||||
- name: Merge system_email definitions
|
- name: Merge system_email definitions
|
||||||
set_fact:
|
set_fact:
|
||||||
system_email: "{{ default_system_email | combine(system_email | default({}, true), recursive=True) }}"
|
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
|
- name: Merge domain definitions
|
||||||
set_fact:
|
set_fact:
|
||||||
domains: "{{ defaults_domains | combine(domains | default({}, true), recursive=True) }}"
|
domains: "{{ defaults_domains | combine(domains | default({}, true), recursive=True) }}"
|
||||||
@ -33,6 +28,10 @@
|
|||||||
redirect_domain_mappings: "{{ redirect_domain_mappings | default([]) + [ {'source': item.key, 'target': item.value} ] }}"
|
redirect_domain_mappings: "{{ redirect_domain_mappings | default([]) + [ {'source': item.key, 'target': item.value} ] }}"
|
||||||
loop: "{{ combined_mapping | dict2items }}"
|
loop: "{{ combined_mapping | dict2items }}"
|
||||||
|
|
||||||
|
- name: Merge application definitions
|
||||||
|
set_fact:
|
||||||
|
applications: "{{ defaults_applications | combine(applications | default({}, true), recursive=True) }}"
|
||||||
|
|
||||||
# @todo implement
|
# @todo implement
|
||||||
# - name: Ensure features.integrated is set based on group membership
|
# - name: Ensure features.integrated is set based on group membership
|
||||||
# set_fact:
|
# set_fact:
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
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()
|
|
@ -1,89 +0,0 @@
|
|||||||
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()
|
|
@ -1,103 +0,0 @@
|
|||||||
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'))
|
os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins'))
|
||||||
)
|
)
|
||||||
|
|
||||||
from generate_all_domains import FilterModule
|
from domain_filters import FilterModule
|
||||||
|
|
||||||
class TestGenerateAllDomains(unittest.TestCase):
|
class TestGenerateAllDomains(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -8,7 +8,7 @@ sys.path.insert(
|
|||||||
os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins'))
|
os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins'))
|
||||||
)
|
)
|
||||||
|
|
||||||
from generate_base_sld_domains import FilterModule
|
from domain_filters import FilterModule
|
||||||
|
|
||||||
class TestGenerateBaseSldDomains(unittest.TestCase):
|
class TestGenerateBaseSldDomains(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
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()
|
|
@ -1,105 +0,0 @@
|
|||||||
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")
|
(self.sample_role / "vars" / "configuration.yml").write_text("foo: bar\nbaz: 123\n")
|
||||||
|
|
||||||
# Output file path
|
# Output file path
|
||||||
self.output_file = self.temp_dir / "group_vars" / "all" / "03_applications.yml"
|
self.output_file = self.temp_dir / "group_vars" / "all" / "11_applications.yml"
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
shutil.rmtree(self.temp_dir)
|
shutil.rmtree(self.temp_dir)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user