diff --git a/plugins/action/applications_carrier.py b/plugins/action/applications_carrier.py index db5e58109b..39bdfe8be6 100644 --- a/plugins/action/applications_carrier.py +++ b/plugins/action/applications_carrier.py @@ -14,8 +14,8 @@ the token store digest and ``inventory_hostname``:: ``tasks/stages/01_constructor.yml`` parks three of them: a block-scoped one for the merge block, which it drops again at the block's end, one after the -``add_host`` tasks for the swarm bootstrap roles, parked only when -``DEPLOYMENT_MODE`` is ``swarm``, and the stage-wide one after the token store. ``merged_applications_cache_key`` reads ``group_names``, so a +``add_host`` tasks for the pre-flight checks, the package batch and the +bootstrap roles, and the stage-wide one after the token store. ``merged_applications_cache_key`` reads ``group_names``, so a carrier whose groups no longer match is rejected and the reader renders afresh. ``merged_users_cache_key`` adds a digest of the tokens the users are hydrated from (the tokens file, or the ``sys_token_store_users`` fact once the diff --git a/tasks/stages/01_constructor.yml b/tasks/stages/01_constructor.yml index 6aeb7dee8e..4114d570ab 100644 --- a/tasks/stages/01_constructor.yml +++ b/tasks/stages/01_constructor.yml @@ -68,7 +68,6 @@ - name: 🧊 Render the applications SPOT once for the bootstrap section applications_carrier: {} - when: IS_SWARM_MODE - name: "🛫 Pre-flight checks" ansible.builtin.include_tasks: ./tasks/utils/constructor/preflight.yml diff --git a/tests/integration/ansible/playbooks/test_constructor_carriers.py b/tests/integration/ansible/playbooks/test_constructor_carriers.py new file mode 100644 index 0000000000..54e9657722 --- /dev/null +++ b/tests/integration/ansible/playbooks/test_constructor_carriers.py @@ -0,0 +1,73 @@ +import unittest + +from utils.cache.yaml import load_yaml_any + +from . import PROJECT_ROOT + +CONSTRUCTOR = PROJECT_ROOT / "tasks" / "stages" / "01_constructor.yml" +PLACEMENT = "./tasks/utils/constructor/placement.yml" +PREFLIGHT = "🛫 Pre-flight checks" + + +def _carrier(task: dict) -> dict | None: + args = task.get("applications_carrier") + return args if isinstance(args, dict) else None + + +def _parks(task: dict) -> bool: + args = _carrier(task) + return args is not None and not args.get("clear", False) + + +def _clears(task: dict) -> bool: + args = _carrier(task) + return args is not None and bool(args.get("clear", False)) + + +class TestConstructorBootstrapCarrier(unittest.TestCase): + def setUp(self) -> None: + self.tasks = load_yaml_any(str(CONSTRUCTOR)) + self.placement = next( + index + for index, task in enumerate(self.tasks) + if task.get("ansible.builtin.include_tasks") == PLACEMENT + ) + self.preflight = next( + index + for index, task in enumerate(self.tasks) + if task.get("name") == PREFLIGHT + ) + self.stage_wide = next( + index + for index, task in enumerate(self.tasks) + if _parks(task) and _carrier(task).get("provider_view") + ) + + def test_the_pre_flight_checks_read_a_carrier_in_every_deploy_mode(self): + window = self.tasks[self.placement + 1 : self.preflight] + parks = [task for task in window if _parks(task)] + + self.assertEqual( + len(parks), + 1, + "one carrier must be parked between the add_host placement and the " + "pre-flight checks, or every check renders the SPOT cold", + ) + self.assertNotIn( + "when", + parks[0], + "the pre-flight checks and the package batch read the SPOT in compose " + "as in swarm, so the carrier must not depend on the deploy mode", + ) + + def test_no_carrier_is_dropped_before_the_stage_wide_park(self): + window = self.tasks[self.placement + 1 : self.stage_wide] + + self.assertEqual( + [task["name"] for task in window if _clears(task)], + [], + ) + + +if __name__ == "__main__": + unittest.main()