"""Eager placeholder substitution for cached user dicts. Ansible 2.19's TrustedAsTemplate gate leaves untagged Jinja silently unrendered, so values like `lastname: "{{ ORGANIZATION }}"` reach downstream tasks as their literal string. These helpers substitute a small set of group-vars scalars into the merged users dict before the templar render pass, so consumers always see resolved values. Extend `_SCALAR_USER_PLACEHOLDERS` (or call sites) when new scalar group-vars appear in `roles/user-*/meta/users.yml` fields like `firstname`, `lastname`, or `description`. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from urllib.parse import urlparse _SCALAR_USER_PLACEHOLDERS: tuple[str, ...] = ( "ORGANIZATION", "SOFTWARE_NAME", ) def substitute_primary_domain_placeholder( users: dict[str, Any], variables: dict[str, Any], *, templar: Any, ) -> dict[str, Any]: raw = variables.get("DOMAIN_PRIMARY") if not raw: return users text = str(raw).strip() if not text: return users if "{{" in text or "{%" in text: from utils.templating.ansible import _templar_render_best_effort text = str(_templar_render_best_effort(templar, text, dict(variables))).strip() if "://" in text: parsed = urlparse(text) text = parsed.hostname or text text = text.split("/", 1)[0].split(":", 1)[0].strip() if not text or "{{" in text or "{%" in text: return users placeholder = "{{ DOMAIN_PRIMARY }}" def _walk(value: Any) -> Any: if isinstance(value, str): return value.replace(placeholder, text) if placeholder in value else value if isinstance(value, Mapping): return {k: _walk(v) for k, v in value.items()} if isinstance(value, list): return [_walk(v) for v in value] if isinstance(value, tuple): return tuple(_walk(v) for v in value) return value return _walk(users) def substitute_scalar_placeholders( users: dict[str, Any], variables: dict[str, Any], *, templar: Any, ) -> dict[str, Any]: from utils.templating.ansible import _templar_render_best_effort resolved: dict[str, str] = {} for var in _SCALAR_USER_PLACEHOLDERS: raw = variables.get(var) if raw is None: continue text = str(raw).strip() if not text: continue if "{{" in text or "{%" in text: text = str( _templar_render_best_effort(templar, text, dict(variables)) ).strip() if "{{" in text or "{%" in text: continue resolved[f"{{{{ {var} }}}}"] = text if not resolved: return users def _walk(value: Any) -> Any: if isinstance(value, str): out = value for placeholder, replacement in resolved.items(): if placeholder in out: out = out.replace(placeholder, replacement) return out if isinstance(value, Mapping): return {k: _walk(v) for k, v in value.items()} if isinstance(value, list): return [_walk(v) for v in value] if isinstance(value, tuple): return tuple(_walk(v) for v in value) return value return _walk(users)