Files
computer-playbook/utils/templating/jinja.py
kevinveenbirkenbachandClaude Opus 5 5137465c6f style: delete every comment the comments-valid policy rejects
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.

Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00

177 lines
4.8 KiB
Python

#
# Strict Jinja2 renderer for values that may contain Jinja markers.
#
# Why:
# - In some Ansible lookup evaluation contexts, templating can leave "{{ ... }}"
# untouched without failing.
# - We want hard-fail semantics to avoid leaking Jinja into generated configs.
#
# Additionally:
# - Support nested Jinja-in-vars by rendering multiple passes with a depth limit.
# Example:
# CA_TRUST.cert_host = "{{ CA_ROOT.cert_host }}"
# CA_ROOT.cert_host = "/etc/{{ SOFTWARE_DOMAIN }}/ca/root-ca.crt"
# We want the final result without any "{{ ... }}" left.
from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleError
def as_str(v: Any) -> str:
return "" if v is None else str(v).strip()
def looks_like_jinja(s: str) -> bool:
return ("{{" in s) or ("{%" in s)
def build_render_context(variables: dict[str, Any]) -> tuple[dict, str]:
"""
Build a robust context for rendering Jinja-in-strings.
We intentionally do NOT rely on Ansible's Templar here because in some
lookup evaluation contexts it can silently keep Jinja markers untouched.
Context priority:
1) variables dict
2) hostvars[inventory_hostname] overlays variables (wins on conflicts)
"""
ctx: dict = {}
if isinstance(variables, dict):
ctx.update(variables)
inv_host = as_str(variables.get("inventory_hostname", "")).strip()
hostvars = variables.get("hostvars") if isinstance(variables, dict) else None
if inv_host and isinstance(hostvars, dict):
hv = hostvars.get(inv_host)
if isinstance(hv, dict):
ctx.update(hv)
return ctx, inv_host
def _jinja_env():
try:
from jinja2 import Environment, StrictUndefined, select_autoescape
except Exception as exc:
raise AnsibleError(f"jinja_strict: cannot import jinja2. Error: {exc}") from exc
return Environment(
undefined=StrictUndefined,
autoescape=select_autoescape(["html", "xml"]),
)
def render_jinja2_strict_once(
raw: str, *, ctx: dict, inv_host: str, var_name: str, err_prefix: str
) -> str:
"""
Render ONE pass using plain Jinja2 with StrictUndefined.
Missing vars fail hard.
"""
if not looks_like_jinja(raw):
return raw
env = _jinja_env()
try:
rendered = env.from_string(raw).render(ctx)
except Exception as exc:
raise AnsibleError(
f"{err_prefix}: failed to render {var_name} via strict Jinja2. "
f"inventory_hostname='{inv_host}'. Raw: {raw}. Error: {exc}"
) from exc
return as_str(rendered)
def render_jinja2_strict_recursive(
raw: str,
*,
ctx: dict,
inv_host: str,
var_name: str,
err_prefix: str,
max_passes: int = 5,
) -> str:
"""
Render using strict Jinja2 repeatedly until:
- no Jinja markers remain, OR
- output stops changing, OR
- max_passes is reached
Hard-fail if markers remain after max_passes / stabilization.
"""
if not looks_like_jinja(raw):
return raw
current = raw
for i in range(max_passes):
rendered = render_jinja2_strict_once(
current,
ctx=ctx,
inv_host=inv_host,
var_name=var_name,
err_prefix=err_prefix,
)
if not looks_like_jinja(rendered):
return rendered
if rendered == current:
raise AnsibleError(
f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers) "
f"after pass {i + 1}/{max_passes}. inventory_hostname='{inv_host}'. "
f"Value: {rendered}."
)
current = rendered
raise AnsibleError(
f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers) "
f"after {max_passes} passes. inventory_hostname='{inv_host}'. "
f"Raw: {raw} | Rendered: {current}."
)
def render_strict(
value: Any,
*,
variables: dict[str, Any],
var_name: str,
err_prefix: str,
max_passes: int = 5,
) -> str:
"""
Convenience wrapper:
- converts to string
- if Jinja markers exist => render via strict Jinja2 using a robust context
(recursive with depth limit)
- hard-fail if Jinja markers remain
"""
raw = as_str(value)
if not raw:
return raw
if not looks_like_jinja(raw):
return raw
ctx, inv_host = build_render_context(variables)
rendered = render_jinja2_strict_recursive(
raw,
ctx=ctx,
inv_host=inv_host,
var_name=var_name,
err_prefix=err_prefix,
max_passes=max_passes,
)
if looks_like_jinja(rendered):
raise AnsibleError(
f"{err_prefix}: {var_name} did not fully expand (still contains Jinja markers). "
f"inventory_hostname='{inv_host}'. Raw: {raw} | Rendered: {rendered}."
)
return rendered