mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
ansible-core's template lookup deep-copies every task variable on each call. The role-local template_shallow of web-opt-rdr-domains, which hands the builtin a top-level copy whose values stay shared, moves to plugins/lookup so every role can use it. The nine template lookups of the front injectors switch to it: six in sys-front-inj-design and one each in sys-front-inj-dashboard, sys-front-inj-logout and sys-front-inj-matomo. None of the switched templates or the lookups they call writes into the task variables. Tests: - the plugin renders like the builtin, never deep-copies the caller's values and leaves the caller's variables unchanged - for every role with design sources, clearnet and onion vhosts, with and without a logo, the design facts are identical to the builtin's, or it fails with the identical error - the dashboard, logout and matomo code renders identically - the redirect loop of web-opt-rdr-domains still renders through the shared plugin The saving per deploy is not measured by a timed deploy yet. 🎯 Fix: 90 % 💥 Break: 3 % 🌱 Efficiency: – ⚡ Speed: – 🛡️ Security: ~100 % 🏆 Score: 87 🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.5 KiB
Python
150 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.parsing.dataloader import DataLoader
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.template import Templar, trust_as_template
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
PROBE_PLUGIN = """
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(self, terms, variables, **kwargs):
|
|
return [variables["domain"]]
|
|
"""
|
|
MUTATING_PLUGIN = """
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(self, terms, variables, **kwargs):
|
|
variables["nested"]["hosts"].append("leaked.example")
|
|
return [""]
|
|
"""
|
|
TEMPLATES = {
|
|
"vhost.j2": "{{ domain }} {{ lookup('shallow_probe_variables_domain') }}\n",
|
|
"broken.j2": "{{ domain }} {{ undefined_in_template }}\n",
|
|
"mutating.j2": "{{ lookup('shallow_probe_mutates_nested') }}\n",
|
|
}
|
|
|
|
|
|
class _NoDeepCopy:
|
|
def __deepcopy__(self, memo):
|
|
raise AssertionError("the variables were deep-copied")
|
|
|
|
|
|
def _causes(error: BaseException | None) -> list[str]:
|
|
causes = []
|
|
while error is not None:
|
|
causes.append(str(error))
|
|
error = error.__cause__
|
|
return causes
|
|
|
|
|
|
class TestTemplateShallow(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls._tmp = tempfile.TemporaryDirectory()
|
|
tmp = Path(cls._tmp.name)
|
|
(tmp / "plugins").mkdir()
|
|
(tmp / "plugins" / "shallow_probe_variables_domain.py").write_text(PROBE_PLUGIN)
|
|
(tmp / "plugins" / "shallow_probe_mutates_nested.py").write_text(
|
|
MUTATING_PLUGIN
|
|
)
|
|
(tmp / "templates").mkdir()
|
|
for name, body in TEMPLATES.items():
|
|
(tmp / "templates" / name).write_text(body)
|
|
lookup_loader.add_directory(str(PROJECT_ROOT / "plugins" / "lookup"))
|
|
lookup_loader.add_directory(str(tmp / "plugins"))
|
|
cls.search_path = [str(tmp)]
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
cls._tmp.cleanup()
|
|
|
|
def _variables(self) -> dict:
|
|
return {
|
|
"domain": "play.example",
|
|
"nested": {"hosts": ["a.example"]},
|
|
"ansible_search_path": self.search_path,
|
|
}
|
|
|
|
def _render(self, plugin: str, variables: dict, template: str = "vhost.j2"):
|
|
return Templar(loader=DataLoader(), variables=variables).template(
|
|
trust_as_template(
|
|
"{{ lookup('" + plugin + "', '" + template + "', "
|
|
"template_vars={'domain': 'source.example'}) }}"
|
|
)
|
|
)
|
|
|
|
def test_a_lookup_inside_the_template_reads_the_per_call_domain(self) -> None:
|
|
rendered = self._render("template_shallow", self._variables())
|
|
self.assertEqual(rendered, "source.example source.example\n")
|
|
self.assertEqual(rendered, self._render("template", self._variables()))
|
|
|
|
def test_the_caller_variables_keep_their_values_and_identity(self) -> None:
|
|
variables = self._variables()
|
|
nested = variables["nested"]
|
|
self._render("template_shallow", variables)
|
|
self.assertEqual(variables, self._variables())
|
|
self.assertIs(variables["nested"], nested)
|
|
|
|
def test_no_value_of_the_caller_is_deep_copied(self) -> None:
|
|
variables = {**self._variables(), "untouchable": _NoDeepCopy()}
|
|
self.assertEqual(
|
|
self._render("template_shallow", variables),
|
|
"source.example source.example\n",
|
|
)
|
|
|
|
def test_the_builtin_template_lookup_deep_copies_every_value(self) -> None:
|
|
variables = {**self._variables(), "untouchable": _NoDeepCopy()}
|
|
with self.assertRaises(AnsibleError) as raised:
|
|
self._render("template", variables)
|
|
causes = _causes(raised.exception)
|
|
self.assertTrue(any("deep-copied" in cause for cause in causes), causes)
|
|
|
|
def test_a_lookup_mutating_a_nested_value_leaks_only_through_the_shallow_copy(
|
|
self,
|
|
) -> None:
|
|
builtin = self._variables()
|
|
self._render("template", builtin, "mutating.j2")
|
|
self.assertEqual(builtin, self._variables())
|
|
shallow = self._variables()
|
|
self._render("template_shallow", shallow, "mutating.j2")
|
|
self.assertEqual(shallow["nested"]["hosts"], ["a.example", "leaked.example"])
|
|
|
|
def test_an_undefined_variable_fails_with_the_builtin_message(self) -> None:
|
|
messages = {}
|
|
for plugin in ("template", "template_shallow"):
|
|
with self.assertRaises(AnsibleError) as raised:
|
|
self._render(plugin, self._variables(), "broken.j2")
|
|
messages[plugin] = [
|
|
cause.replace("template_shallow", "template")
|
|
for cause in _causes(raised.exception)
|
|
]
|
|
self.assertIn("undefined_in_template", " ".join(messages["template"]))
|
|
self.assertEqual(messages["template_shallow"], messages["template"])
|
|
|
|
def test_a_missing_template_fails_with_the_builtin_message(self) -> None:
|
|
messages = {}
|
|
for plugin in ("template", "template_shallow"):
|
|
with self.assertRaises(AnsibleError) as raised:
|
|
self._render(plugin, self._variables(), "absent.j2")
|
|
messages[plugin] = [
|
|
cause.replace("template_shallow", "template")
|
|
for cause in _causes(raised.exception)
|
|
]
|
|
self.assertIn("absent.j2", " ".join(messages["template"]))
|
|
self.assertEqual(messages["template_shallow"], messages["template"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|