diff --git a/plugins/action/applications_carrier.py b/plugins/action/applications_carrier.py index 5991ff5cd8..f7711ed295 100644 --- a/plugins/action/applications_carrier.py +++ b/plugins/action/applications_carrier.py @@ -5,19 +5,31 @@ Ansible forks a fresh worker per task, so the in-process render cache of a place all later workers inherit. This action renders in the worker of one constructor task and returns the payload as a host fact; every later worker inherits that fact at fork time and ``get_merged_applications`` serves it -instead of rendering. Sits after the token store in -``tasks/stages/01_constructor.yml`` so the embedded user records already -carry the loaded tokens:: +instead of rendering:: - name: 🧊 Render the applications SPOT once and carry it into every task's worker applications_carrier: {} +``tasks/stages/01_constructor.yml`` parks two of them. The stage-wide one +sits after the token store so the embedded user records already carry the +loaded tokens, and after the ``add_host`` tasks because 114 roles gate +services on ``group_names`` and the payload bakes those in. The +merge block opens an earlier, block-scoped one and drops it again at the +block's end, because ``merged_applications_cache_key`` reads neither +``group_names`` nor the token file: a carrier left standing across either +would be served unchanged where a fresh render belongs:: + + - name: 🧊 Drop the merge-block carrier so later stages re-render + applications_carrier: + clear: true + The result is masked by the action itself: the fact is the whole rendered tree, about 0.4 MB with every application's credentials, and printing it at ``-v`` is neither credential debugging nor readable. Args: - none. Any argument is an error. + clear: drop the carrier fact instead of rendering. Defaults to false. + Any other argument is an error. """ from __future__ import annotations @@ -25,6 +37,7 @@ from __future__ import annotations from typing import Any from ansible.errors import AnsibleActionFail +from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.plugins.loader import lookup_loader @@ -39,8 +52,19 @@ class ActionModule(ActionBase): ) -> dict[str, Any]: result = super().run(tmp, task_vars) task_vars = task_vars or {} - if self._task.args: - raise AnsibleActionFail("applications_carrier takes no arguments") + args = self._task.args or {} + unexpected = sorted(set(args) - {"clear"}) + if unexpected: + raise AnsibleActionFail( + f"applications_carrier: unknown argument(s) {', '.join(unexpected)}; " + "the only argument is 'clear'" + ) + + if boolean(args.get("clear", False)): + result.update( + changed=False, ansible_facts={APPLICATIONS_RENDERED_FACT: None} + ) + return result lookup = lookup_loader.get( "applications", loader=self._loader, templar=self._templar diff --git a/tasks/stages/01_constructor.yml b/tasks/stages/01_constructor.yml index 70a1a2aab4..5f6b5c33e5 100644 --- a/tasks/stages/01_constructor.yml +++ b/tasks/stages/01_constructor.yml @@ -28,6 +28,9 @@ ansible.builtin.set_fact: networks: "{{ defaults_networks | combine(networks | default({}, true), recursive=True) }}" + - name: 🧊 Render the applications SPOT once for the merge block + applications_carrier: {} + - name: "✅ Sanity check: all group_names must exist in applications" ansible.builtin.assert: that: @@ -96,6 +99,10 @@ ansible.builtin.set_fact: service_provider: "{{ defaults_service_provider | combine(service_provider | default({}, true), recursive=True) }}" + - name: 🧊 Drop the merge-block carrier so later stages re-render + applications_carrier: + clear: true + - name: "💾 Persist {{ SOFTWARE_NAME }} version" ansible.builtin.include_role: name: sys-version diff --git a/tests/unit/python/plugins/action/test_applications_carrier.py b/tests/unit/python/plugins/action/test_applications_carrier.py index 5e80413bbb..841b2eea44 100644 --- a/tests/unit/python/plugins/action/test_applications_carrier.py +++ b/tests/unit/python/plugins/action/test_applications_carrier.py @@ -68,6 +68,46 @@ class TestApplicationsCarrierActionPlugin(unittest.TestCase): ): action.run(task_vars={}) + def test_clear_drops_the_fact_without_rendering(self): + lookup = _FakeLookup({"key": ["roles", []], "applications": {}}) + action = _make_action(_FakeTask(args={"clear": True})) + with ( + patch( + "plugins.action.applications_carrier.ActionBase.run", + autospec=True, + return_value={}, + ), + patch( + "plugins.action.applications_carrier.lookup_loader.get", + return_value=lookup, + ), + ): + result = action.run(task_vars={"applications": {}}) + + self.assertIsNone(result["ansible_facts"][APPLICATIONS_RENDERED_FACT]) + self.assertFalse(result["changed"]) + self.assertEqual(lookup.calls, []) + + def test_clear_false_still_renders(self): + carrier = {"key": ["roles", ["a", "b", "c", "d"]], "applications": {"x": {}}} + lookup = _FakeLookup(carrier) + action = _make_action(_FakeTask(args={"clear": "no"})) + with ( + patch( + "plugins.action.applications_carrier.ActionBase.run", + autospec=True, + return_value={}, + ), + patch( + "plugins.action.applications_carrier.lookup_loader.get", + return_value=lookup, + ), + ): + result = action.run(task_vars={"applications": {}}) + + self.assertIs(result["ansible_facts"][APPLICATIONS_RENDERED_FACT], carrier) + self.assertEqual(len(lookup.calls), 1) + if __name__ == "__main__": unittest.main()