diff --git a/roles/sys-svc-compose/handlers/compose.yml b/roles/sys-svc-compose/handlers/compose.yml index ceca93dd55..307c0171c9 100644 --- a/roles/sys-svc-compose/handlers/compose.yml +++ b/roles/sys-svc-compose/handlers/compose.yml @@ -8,36 +8,6 @@ - container-recreate - compose-service-up -- name: 📝 Generate CA trust override - ansible.builtin.shell: | - set -euo pipefail - {{ lookup('compose_ca_inject_cmd', application_id) }} - args: - chdir: "{{ lookup('container', application_id, 'directories.instance') }}" - executable: /bin/bash - register: compose_ca_override - timeout: "{{ lookup('timeout', 1200) | int }}" - retries: 7 - delay: 20 - until: compose_ca_override is succeeded - listen: - - compose-up - when: lookup('ca_injected', application_id) - -- name: ✅ Validate Docker Compose configuration - ansible.builtin.shell: | - set -euo pipefail - compose config --quiet - args: - chdir: "{{ lookup('container', application_id, 'directories.instance') }}" - executable: /bin/bash - register: dc_validate - changed_when: false - failed_when: dc_validate.rc != 0 - listen: - - compose-up - when: MODE_ASSERT_LOCAL | bool - - name: 📥 compose pull ansible.builtin.script: cmd: >- @@ -92,6 +62,36 @@ listen: - compose-build +- name: 📝 Generate CA trust override + ansible.builtin.shell: | + set -euo pipefail + {{ lookup('compose_ca_inject_cmd', application_id) }} + args: + chdir: "{{ lookup('container', application_id, 'directories.instance') }}" + executable: /bin/bash + register: compose_ca_override + timeout: "{{ lookup('timeout', 1200) | int }}" + retries: 7 + delay: 20 + until: compose_ca_override is succeeded + listen: + - compose-up + when: lookup('ca_injected', application_id) + +- name: ✅ Validate Docker Compose configuration + ansible.builtin.shell: | + set -euo pipefail + compose config --quiet + args: + chdir: "{{ lookup('container', application_id, 'directories.instance') }}" + executable: /bin/bash + register: dc_validate + changed_when: false + failed_when: dc_validate.rc != 0 + listen: + - compose-up + when: MODE_ASSERT_LOCAL | bool + - name: "🌐 compose: pre-create external provider networks for the stack" ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/network/create.yml') }}" vars: diff --git a/tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca.py b/tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca.py index dc94fb3b47..7abc96f495 100644 --- a/tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca.py +++ b/tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca.py @@ -6,7 +6,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from utils.cache.yaml import load_yaml, load_yaml_str +from utils.cache.yaml import load_yaml, load_yaml_any, load_yaml_str from . import PROJECT_ROOT @@ -15,6 +15,24 @@ _CA_TRUST = load_yaml(str(PROJECT_ROOT / "group_vars" / "all" / "02_tls.yml"))[ ] CA_CERT_CONTAINER = _CA_TRUST["inject_cert_container"] CA_WRAPPER_CONTAINER = _CA_TRUST["inject_wrapper_container"] +COMPOSE_HANDLERS = PROJECT_ROOT / "roles/sys-svc-compose/handlers/compose.yml" + + +def _flushed_shells(*topics: str) -> list[str]: + """Shell bodies of the compose handlers one flush runs, in run order. + + Args: + topics: the notified listen topics. + + Returns: + the `ansible.builtin.shell` body of every handler listening on any + topic, in definition order. + """ + return [ + handler.get("ansible.builtin.shell", "") + for handler in load_yaml_any(str(COMPOSE_HANDLERS)) + if set(topics) & set(handler["listen"]) + ] def _load_module(rel_path: str, name: str): @@ -1183,5 +1201,64 @@ class TestComposeCaInject(unittest.TestCase): ) +class TestFirstDeployFlush(unittest.TestCase): + def setUp(self): + self.m = _load_module( + "roles/sys-svc-compose-ca/files/python/compose_ca.py", + "compose_ca_first_deploy_mod", + ) + + def test_compose_up_starts_a_freshly_built_image_behind_the_wrapper(self): + image = "app_custom:1" + built: set[str] = set() + + def fake_run(cmd, *, cwd, env, timeout=None, capture=True): + if cmd[:3] == ["docker", "image", "inspect"]: + if cmd[3] not in built: + return 1, "", f"No such image: {cmd[3]}" + cfg = [{"Config": {"Entrypoint": ["/entry.sh"], "Cmd": ["serve"]}}] + return 0, json.dumps(cfg), "" + if cmd[:5] == ["docker", "run", "--rm", "--entrypoint", "/bin/sh"]: + return 0, "", "" + return 1, "", "unexpected" + + override: dict = {} + started_with = None + for shell in _flushed_shells("compose-build", "compose-up"): + if "compose build" in shell: + built.add(image) + elif "compose_ca_inject_cmd" in shell: + with patch.object(self.m, "run", side_effect=fake_run): + override = self.m.render_override( + {"app": {"image": image, "build": {"context": "."}}}, + {"app": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}, + cwd=Path("/tmp"), + env={}, + ca_host="/host/ca.crt", + wrapper_host="/host/with-ca-trust.sh", + ca_container=CA_CERT_CONTAINER, + wrapper_container=CA_WRAPPER_CONTAINER, + trust_name="infinito.local", + ) + elif "compose up -d --force-recreate" in shell: + started_with = dict(override) + + self.assertIsNotNone(started_with, "the flush never reached compose up") + self.assertIn( + "services", + started_with, + "compose up ran before the CA override was generated", + ) + service = started_with["services"]["app"] + self.assertEqual( + service.get("entrypoint"), + [CA_WRAPPER_CONTAINER], + "compose up started the image this flush built without the CA " + "wrapper: the override was generated before compose build, when " + "the locally built tag could not be inspected yet", + ) + self.assertEqual(service.get("command"), ["/entry.sh", "serve"]) + + if __name__ == "__main__": unittest.main()