fix(sys-svc-compose): generate the CA override after compose build

Handlers run in definition order, and "Generate CA trust override" was
defined before "Build compose". On a first deploy the override read the
Entrypoint and Cmd of every service image with docker image inspect
before compose build had created the locally built tags, found none,
and left those services without the with-ca-trust wrapper; on later
deploys it read the previous build's Entrypoint and Cmd instead of the
new one. The override and the compose config validation now run after
the build and rebuild handlers and before compose up; pull stays first.

The swarm handlers already build before they generate their override
and use no wrapper, so they are unchanged.

TestFirstDeployFlush replays the shell handlers of one compose-build
plus compose-up flush in the order handlers/compose.yml defines them
and asserts that compose up starts the freshly built image behind the
wrapper.

Local deploy: all six stacks with a built image carry the wrapper in
compose.ca.override.yml, nextcloud for 8 of 8 services.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-28 12:28:40 +01:00
1 parent caabc61841
commit 8135cd29ee
2 files changed
+108 -31

No files matched your search

@@ -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()