diff --git a/roles/web-app-akaunting/tasks/main.yml b/roles/web-app-akaunting/tasks/main.yml index ac6a0b2e..f836afd1 100644 --- a/roles/web-app-akaunting/tasks/main.yml +++ b/roles/web-app-akaunting/tasks/main.yml @@ -11,10 +11,6 @@ - name: "For '{{ application_id }}': load docker, db and proxy" include_role: name: sys-stk-full-stateful - vars: - # Forward flag into compose templating - cmp_extra_facts: - akaunting_setup_enabled: "{{ akaunting_setup_enabled }}" - name: "Akaunting | Create first-run marker to disable future setup" ansible.builtin.file: diff --git a/roles/web-opt-rdr-domains/README.md b/roles/web-opt-rdr-domains/README.md index f93c0111..cd0c81f4 100644 --- a/roles/web-opt-rdr-domains/README.md +++ b/roles/web-opt-rdr-domains/README.md @@ -2,15 +2,5 @@ This Ansible role configures Nginx to perform 301 redirects from one domain to another. It handles SSL certificate retrieval for the source domains and sets up the Nginx configuration to redirect to the specified target domains. -## Role Variables - -- `domain_mappings`: A list of objects with `source` and `target` properties specifying the domains to redirect from and to. -- `users.administrator.email`: The email used for SSL certificate registration with Let's Encrypt. - -## Dependencies - -- `sys-stk-front-pure`: A role for setting up HTTPS for Nginx -- `letsencrypt`: A role for managing SSL certificates with Let's Encrypt - ## Author Information This role was created in 2023 by [Kevin Veen-Birkenbach](https://www.veen.world/). \ No newline at end of file diff --git a/roles/web-opt-rdr-www/tasks/main.yml b/roles/web-opt-rdr-www/tasks/main.yml index 079f5481..e3ad502f 100644 --- a/roles/web-opt-rdr-www/tasks/main.yml +++ b/roles/web-opt-rdr-www/tasks/main.yml @@ -10,7 +10,7 @@ include_role: name: web-opt-rdr-domains vars: - domain_mappings: "{{ REDIRECT_WWW_DOMAINS | map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}" + redirect_domain_mappings: "{{ REDIRECT_WWW_DOMAINS | map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}" when: REDIRECT_WWW_FLAVOR == 'origin' - name: Include DNS role to set redirects diff --git a/tasks/stages/01_constructor.yml b/tasks/stages/01_constructor.yml index 650ac9d2..68e6f64a 100644 --- a/tasks/stages/01_constructor.yml +++ b/tasks/stages/01_constructor.yml @@ -61,6 +61,7 @@ canonical_domains_map(PRIMARY_DOMAIN) | combine(CURRENT_PLAY_DOMAINS, recursive=True) }} + - name: Merge redirect_domain_mappings set_fact: # The following mapping is necessary to define the exceptions for domains which are created, but which aren't used diff --git a/tests/integration/test_vars_usage_in_yaml.py b/tests/integration/test_vars_usage_in_yaml.py new file mode 100644 index 00000000..dd9d7f85 --- /dev/null +++ b/tests/integration/test_vars_usage_in_yaml.py @@ -0,0 +1,173 @@ +import unittest +from pathlib import Path +import re +from typing import Any, Iterable, Set, List +import yaml + + +class TestVarsPassedAreUsed(unittest.TestCase): + """ + Integration test: + - Walk all *.yml/*.yaml and *.j2 files + - Collect variable names passed via task-level `vars:` + - Consider a var "used" if it appears in ANY of: + • Jinja output blocks: {{ ... var_name ... }} + • Jinja statement blocks: {% ... var_name ... %} + (robust against inner '}' / '%' via tempered regex) + • Ansible expressions in YAML: + - when: (string or list of strings) + - loop: + - with_*: + + Additional rule: + - Do NOT count as used if the token is immediately followed by '(' (optionally with whitespace), + i.e. treat `var_name(` as a function/macro call, not a variable usage. + """ + + REPO_ROOT = Path(__file__).resolve().parents[2] + YAML_EXTENSIONS = {".yml", ".yaml"} + JINJA_EXTENSIONS = {".j2"} + + # ---------- File iteration & YAML loading ---------- + + def _iter_files(self, extensions: set[str]) -> Iterable[Path]: + for p in self.REPO_ROOT.rglob("*"): + if p.is_file() and p.suffix in extensions: + yield p + + def _load_yaml_documents(self, path: Path) -> List[Any]: + try: + with path.open("r", encoding="utf-8") as f: + return list(yaml.safe_load_all(f)) or [] + except Exception: + # File may contain heavy templating or anchors; skip structural parse + return [] + + def _walk_mapping(self, node: Any) -> Iterable[dict]: + if isinstance(node, dict): + yield node + for v in node.values(): + yield from self._walk_mapping(v) + elif isinstance(node, list): + for item in node: + yield from self._walk_mapping(item) + + # ---------- Collect vars passed via `vars:` ---------- + + def _collect_vars_passed(self) -> Set[str]: + collected: Set[str] = set() + for yml in self._iter_files(self.YAML_EXTENSIONS): + docs = self._load_yaml_documents(yml) + for doc in docs: + for mapping in self._walk_mapping(doc): + if "vars" in mapping and isinstance(mapping["vars"], dict): + for k in mapping["vars"].keys(): + if isinstance(k, str) and k.strip(): + collected.add(k.strip()) + return collected + + # ---------- Gather text for Jinja usage scanning ---------- + + def _concat_texts(self) -> str: + parts: List[str] = [] + for f in self._iter_files(self.YAML_EXTENSIONS | self.JINJA_EXTENSIONS): + try: + parts.append(f.read_text(encoding="utf-8")) + except Exception: + # Non-UTF8 or unreadable — ignore + pass + return "\n".join(parts) + + # ---------- Extract Ansible expression strings from YAML ---------- + + def _collect_ansible_expressions(self) -> List[str]: + """ + Return a flat list of strings taken from Ansible expression-bearing fields: + - when: or when: [, , ...] + - loop: + - with_*: + """ + exprs: List[str] = [] + for yml in self._iter_files(self.YAML_EXTENSIONS): + docs = self._load_yaml_documents(yml) + for doc in docs: + for mapping in self._walk_mapping(doc): + for key, val in list(mapping.items()): + if key == "when": + if isinstance(val, str): + exprs.append(val) + elif isinstance(val, list): + exprs.extend([x for x in val if isinstance(x, str)]) + elif key == "loop": + if isinstance(val, str): + exprs.append(val) + elif isinstance(key, str) and key.startswith("with_"): + if isinstance(val, str): + exprs.append(val) + return exprs + + # ---------- Usage checks ---------- + + def _used_in_jinja_blocks(self, var_name: str, text: str) -> bool: + """ + Detect var usage inside Jinja blocks, excluding function/macro calls like `var_name(...)`. + We use a tempered regex to avoid stopping at the first '}}'/'%}' and a negative lookahead + `(?!\\s*\\()` after the token. + """ + # Word token not followed by '(' → real variable usage + token = r"\b" + re.escape(var_name) + r"\b(?!\s*\()" + + # Output blocks: {{ ... }} + pat_output = re.compile( + r"{{(?:(?!}}).)*" + token + r"(?:(?!}}).)*}}", + re.DOTALL, + ) + # Statement blocks: {% ... %} + pat_stmt = re.compile( + r"{%(?:(?!%}).)*" + token + r"(?:(?!%}).)*%}", + re.DOTALL, + ) + return pat_output.search(text) is not None or pat_stmt.search(text) is not None + + def _used_in_ansible_exprs(self, var_name: str, exprs: List[str]) -> bool: + """ + Detect var usage in Ansible expressions (when/loop/with_*), + excluding function/macro calls like `var_name(...)`. + """ + pat = re.compile(r"\b" + re.escape(var_name) + r"\b(?!\s*\()") + return any(pat.search(e) for e in exprs) + + # ---------- Test ---------- + + def test_vars_passed_are_used_in_yaml_or_jinja(self): + vars_passed = self._collect_vars_passed() + self.assertTrue( + vars_passed, + "No variables passed via `vars:` were found. " + "Check the repo root path in this test." + ) + + all_text = self._concat_texts() + ansible_exprs = self._collect_ansible_expressions() + + unused: List[str] = [] + for var_name in sorted(vars_passed): + used = ( + self._used_in_jinja_blocks(var_name, all_text) + or self._used_in_ansible_exprs(var_name, ansible_exprs) + ) + if not used: + unused.append(var_name) + + if unused: + msg = ( + "The following variables are passed via `vars:` but never referenced in:\n" + " • Jinja output/statement blocks ({{ ... }} / {% ... %}) OR\n" + " • Ansible expressions (when/loop/with_*)\n\n" + + "\n".join(f" - {v}" for v in unused) + + "\n\nNotes:\n" + " • Function-like tokens (name followed by '(') are ignored intentionally.\n" + " • If a var is only used in Python code or other file types, extend the test accordingly\n" + " or remove the var if it's truly unused." + ) + self.fail(msg)