mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-15 08:30:46 +02:00
This test scans roles/*/handlers/main.yml and fails if a handler's 'name' contains a Jinja variable ({{ ... }}). Reason: - Handler names must be static to ensure reliable 'notify' resolution. - Dynamic names can break handler matching, cause undefined-variable errors, and produce unstable logs. Recommendation: - Keep handler names static and, if dynamic behavior is needed, use a static 'listen:' key. https://chatgpt.com/share/689b37dc-e1e4-800f-bd56-00b43c7701f6
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Integration test: ensure no Jinja variables are used in handler *names*.
|
||
|
||
Why this policy?
|
||
- Handler identifiers should be stable strings. If you ever notify by handler
|
||
name (instead of a dedicated `listen:` key), a templated name can fail to
|
||
resolve or silently not match what `notify` referenced.
|
||
- Even when `listen:` is used (recommended), variable-laden names make logs and
|
||
tooling brittle and can trigger undefined-variable errors at parse/run time.
|
||
- Keeping handler names static improves reliability, debuggability, and
|
||
compatibility with analysis tools.
|
||
|
||
Allowed:
|
||
- You may still template other fields or use `listen:` for dynamic trigger
|
||
routing; just keep the handler’s `name` static text.
|
||
|
||
This test scans: roles/*/handlers/main.yml
|
||
"""
|
||
|
||
import os
|
||
import glob
|
||
import re
|
||
import unittest
|
||
|
||
try:
|
||
import yaml # PyYAML
|
||
except ImportError as exc:
|
||
raise SystemExit(
|
||
"PyYAML is required to run this test. Install with: pip install pyyaml"
|
||
) from exc
|
||
|
||
|
||
JINJA_VAR_PATTERN = re.compile(r"{{.*?}}") # minimal check for any templating
|
||
|
||
|
||
def _iter_tasks(node):
|
||
"""
|
||
Yield all task-like dicts from a loaded YAML node, descending into common
|
||
task containers (`block`, `rescue`, `always`), just in case.
|
||
"""
|
||
if isinstance(node, dict):
|
||
# If this dict looks like a task (has 'name' or a module key), yield it.
|
||
if any(k in node for k in ("name", "action")):
|
||
yield node
|
||
|
||
# Dive into known task containers (handlers can include blocks too).
|
||
for key in ("block", "rescue", "always"):
|
||
if key in node and isinstance(node[key], list):
|
||
for item in node[key]:
|
||
yield from _iter_tasks(item)
|
||
|
||
elif isinstance(node, list):
|
||
for item in node:
|
||
yield from _iter_tasks(item)
|
||
|
||
|
||
class StaticHandlerNamesTest(unittest.TestCase):
|
||
"""
|
||
Ensures handler names are static strings (no Jinja variables like {{ ... }}).
|
||
"""
|
||
|
||
def test_no_templated_names_in_handlers(self):
|
||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||
pattern = os.path.join(project_root, "roles", "*", "handlers", "main.yml")
|
||
|
||
violations = []
|
||
|
||
for handler_path in sorted(glob.glob(pattern)):
|
||
# Load possibly multi-document YAML safely
|
||
try:
|
||
with open(handler_path, "r", encoding="utf-8") as f:
|
||
docs = list(yaml.safe_load_all(f))
|
||
except FileNotFoundError:
|
||
continue
|
||
except yaml.YAMLError as e:
|
||
violations.append(
|
||
f"{handler_path} -> YAML parse error: {e}"
|
||
)
|
||
continue
|
||
|
||
for doc in docs:
|
||
for task in _iter_tasks(doc):
|
||
name = task.get("name")
|
||
if not isinstance(name, str):
|
||
# ignore unnamed or non-string names
|
||
continue
|
||
if JINJA_VAR_PATTERN.search(name):
|
||
# Compose a clear, actionable message
|
||
listen = task.get("listen")
|
||
listen_hint = (
|
||
""
|
||
if listen
|
||
else " Consider using a static handler name and, if you need flexible triggers, add a static `listen:` key that your tasks `notify`."
|
||
)
|
||
violations.append(
|
||
f"{handler_path} -> Handler name contains variables: {name!r}\n"
|
||
"Reason: Handler names must be static. Using Jinja variables in the name "
|
||
"can break handler resolution (when notified by name), produces unstable logs, "
|
||
"and may cause undefined-variable errors. Keep the handler `name` constant."
|
||
f"{listen_hint}"
|
||
)
|
||
|
||
if violations:
|
||
self.fail(
|
||
"Templated handler names are not allowed.\n\n"
|
||
+ "\n\n".join(violations)
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|