Files
computer-playbook/tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca.py
T

1188 lines
44 KiB
Python

# tests/unit/python/roles/sys-svc-compose-ca/files/test_compose_ca_inject.py
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from utils.cache.yaml import load_yaml, load_yaml_str
from . import PROJECT_ROOT
_CA_TRUST = load_yaml(str(PROJECT_ROOT / "group_vars" / "all" / "02_tls.yml"))[
"CA_TRUST"
]
CA_CERT_CONTAINER = _CA_TRUST["inject_cert_container"]
CA_WRAPPER_CONTAINER = _CA_TRUST["inject_wrapper_container"]
def _load_module(rel_path: str, name: str):
path = PROJECT_ROOT / rel_path
spec = importlib.util.spec_from_file_location(name, str(path))
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
class TestComposeCaInject(unittest.TestCase):
def setUp(self):
self.m = _load_module(
"roles/sys-svc-compose-ca/files/python/compose_ca.py",
"compose_ca_inject_mod",
)
def test_normalize_cmd(self):
self.assertEqual(self.m.normalize_cmd(["a", "b"]), ["a", "b"])
self.assertEqual(self.m.normalize_cmd("echo hi"), ["/bin/sh", "-lc", "echo hi"])
self.assertEqual(
self.m.normalize_cmd(["/bin/sh", "-c", "if", "!", "grep", "foo"]),
["/bin/sh", "-c", "if ! grep foo"],
)
self.assertEqual(
self.m.normalize_cmd(
[
"/bin/sh",
"-c",
"if",
"!",
"grep",
"-qF",
"INFINITO_TAIGA_AUTH_CONFIG", # nocheck: test-fixture
"/taiga-back/settings/config.py",
";",
"then",
]
),
[
"/bin/sh",
"-c",
"if ! grep -qF INFINITO_TAIGA_AUTH_CONFIG /taiga-back/settings/config.py ; then",
],
)
self.assertEqual(self.m.normalize_cmd(None), [])
with self.assertRaises(SystemExit):
self.m.normalize_cmd(123)
def test_shell_payload_single_string_preserved(self):
self.assertEqual(
self.m._shell_payload(
[
"if ! grep -qF INFINITO_TAIGA_AUTH_CONFIG /taiga-back/settings/config.py; then cat /taiga-back/settings/config.append.py >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/entrypoint.sh"
]
),
"if ! grep -qF INFINITO_TAIGA_AUTH_CONFIG /taiga-back/settings/config.py; then cat /taiga-back/settings/config.append.py >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/entrypoint.sh",
)
def test_normalize_entrypoint(self):
self.assertEqual(self.m.normalize_entrypoint(["a", "b"]), ["a", "b"])
self.assertEqual(
self.m.normalize_entrypoint("echo hi"), ["/bin/sh", "-lc", "echo hi"]
)
self.assertEqual(
self.m.normalize_entrypoint(["/bin/sh", "-c", "echo", "hi"]),
["/bin/sh", "-c", "echo hi"],
)
self.assertEqual(self.m.normalize_entrypoint(None), [])
with self.assertRaises(SystemExit):
self.m.normalize_entrypoint(123)
def test_parse_yaml_requires_mapping(self):
doc = self.m.parse_yaml("a: 1\n", "x")
self.assertEqual(doc["a"], 1)
with self.assertRaises(SystemExit):
self.m.parse_yaml("- 1\n- 2\n", "x")
def test_has_build(self):
self.assertTrue(self.m._has_build({"build": {"context": "."}}))
self.assertTrue(self.m._has_build({"build": "./"}))
self.assertFalse(self.m._has_build({}))
self.assertFalse(self.m._has_build({"build": None}))
def test_find_builder_service_for_image(self):
services = {
"app": {"image": "custom:1", "build": {"context": "."}},
"worker": {"image": "custom:1"},
"other": {"image": "other:2", "build": {"context": "."}},
}
self.assertEqual(
self.m._find_builder_service_for_image(image="custom:1", services=services),
"app",
)
self.assertEqual(
self.m._find_builder_service_for_image(
image="missing:tag", services=services
),
"",
)
self.assertEqual(
self.m._find_builder_service_for_image(image="", services=services),
"",
)
def test_ensure_image_available_builds_self_when_build_present(self):
"""
If the service has build:, ensure_image_available should run
`compose ... build <service>` (not pull).
"""
calls = []
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
inspect_calls = [
c for c in calls if c[:3] == ["docker", "image", "inspect"]
]
if len(inspect_calls) == 1:
return 1, "", "no such image"
return 0, json.dumps([{"Config": {"Entrypoint": [], "Cmd": []}}]), ""
if cmd[:2] == ["docker", "compose"] and cmd[-2:] == ["build", "app"]:
return 0, "built", ""
return 1, "", "unexpected"
base_cmd = ["docker", "compose", "-p", "p", "-f", "compose.yml"]
with patch.object(self.m, "run", side_effect=fake_run):
self.m.ensure_image_available(
service_name="app",
svc={"image": "custom:1", "build": {"context": "."}},
image="custom:1",
services={"app": {"image": "custom:1", "build": {"context": "."}}},
service_to_compose_cmd={"app": base_cmd},
compose_base_cmd=base_cmd,
cwd=Path("/tmp"),
env={},
)
self.assertTrue(any(c[-2:] == ["build", "app"] for c in calls))
self.assertFalse(any(c[-2:] == ["pull", "app"] for c in calls))
def test_ensure_image_available_builds_builder_for_shared_image(self):
"""
New robust logic:
- 'worker' has no build but references image 'custom:1'
- 'app' has build and same image 'custom:1'
=> should run `compose ... build app` and not pull worker.
"""
calls = []
services = {
"app": {"image": "custom:1", "build": {"context": "."}},
"worker": {"image": "custom:1"},
}
base_cmd = ["docker", "compose", "-p", "p", "-f", "compose.yml"]
service_to_cmd = {"app": base_cmd, "worker": base_cmd}
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
inspect_calls = [
c for c in calls if c[:3] == ["docker", "image", "inspect"]
]
if len(inspect_calls) == 1:
return 1, "", "no such image"
return 0, json.dumps([{"Config": {"Entrypoint": [], "Cmd": []}}]), ""
if cmd[:2] == ["docker", "compose"] and cmd[-2:] == ["build", "app"]:
return 0, "built", ""
if cmd[:2] == ["docker", "compose"] and cmd[-2:] == ["pull", "worker"]:
return 0, "pulled", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
self.m.ensure_image_available(
service_name="worker",
svc=services["worker"],
image="custom:1",
services=services,
service_to_compose_cmd=service_to_cmd,
compose_base_cmd=base_cmd,
cwd=Path("/tmp"),
env={},
)
self.assertTrue(any(c[-2:] == ["build", "app"] for c in calls))
self.assertFalse(any(c[-2:] == ["pull", "worker"] for c in calls))
def test_ensure_image_available_falls_back_to_pull_when_no_builder(self):
"""
If there is no builder service for an image, fall back to pull(service).
"""
calls = []
services = {"worker": {"image": "registry.example/worker:1"}}
base_cmd = ["docker", "compose", "-p", "p", "-f", "compose.yml"]
service_to_cmd = {"worker": base_cmd}
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
inspect_calls = [
c for c in calls if c[:3] == ["docker", "image", "inspect"]
]
if len(inspect_calls) == 1:
return 1, "", "no such image"
return 0, json.dumps([{"Config": {"Entrypoint": [], "Cmd": []}}]), ""
if cmd[:2] == ["docker", "compose"] and cmd[-2:] == ["pull", "worker"]:
return 0, "pulled", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
self.m.ensure_image_available(
service_name="worker",
svc=services["worker"],
image=services["worker"]["image"],
services=services,
service_to_compose_cmd=service_to_cmd,
compose_base_cmd=base_cmd,
cwd=Path("/tmp"),
env={},
)
self.assertTrue(any(c[-2:] == ["pull", "worker"] for c in calls))
@patch.object(Path, "exists", autospec=True, return_value=True)
@patch.object(Path, "is_dir", autospec=True, return_value=True)
@patch.object(Path, "read_text", autospec=True)
@patch.object(Path, "write_text", autospec=True)
@patch.object(Path, "mkdir", autospec=True)
def test_main_generates_override(
self, _mkdir, _write_text, _read_text, _is_dir, _exists
):
"""
main():
- parses compose files to discover profiles
- runs `compose ... config`
- inspects images via `container image inspect`
- writes override with CA_TRUST_* envs
"""
_read_text.return_value = "services:\n app:\n image: myimage:latest\n"
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
if (
len(cmd) >= 3
and cmd[0:2] == ["docker", "compose"]
and cmd[-1] == "config"
):
yml = "services:\n app:\n image: myimage:latest\n"
return 0, yml, ""
if cmd[:3] == ["docker", "image", "inspect"]:
json_out = json.dumps(
[{"Config": {"Entrypoint": ["/entry"], "Cmd": ["run"]}}]
)
return 0, json_out, ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
argv = [
"compose_ca.py",
"--chdir",
"/tmp/app",
"--project",
"p",
"--compose-files",
"-f compose.yml -f compose.override.yml",
"--out",
"compose.ca.override.yml",
"--ca-host",
"/etc/infinito/ca/root-ca.crt",
"--wrapper-host",
"/etc/infinito/bin/with-ca-trust.sh",
"--ca-container",
CA_CERT_CONTAINER,
"--wrapper-container",
CA_WRAPPER_CONTAINER,
"--trust-name",
"infinito.local",
]
with patch("sys.argv", argv):
rc = self.m.main()
self.assertEqual(rc, 0)
self.assertTrue(_write_text.called)
args, _kwargs = _write_text.call_args
written = args[1] if len(args) > 1 else ""
self.assertIn("CA_TRUST_NAME", written)
self.assertIn("infinito.local", written)
@patch.object(Path, "exists", autospec=True, return_value=True)
@patch.object(Path, "is_dir", autospec=True, return_value=True)
@patch.object(Path, "read_text", autospec=True)
@patch.object(Path, "write_text", autospec=True)
@patch.object(Path, "mkdir", autospec=True)
def test_main_empty_services_writes_noop_override(
self, _mkdir, _write_text, _read_text, _is_dir, _exists
):
"""main(): an app whose merged compose has no services (a launcher-based
app such as discourse with all backends shared) writes an empty override
and exits 0 instead of dying with 'No services found'."""
_read_text.return_value = "services: {}\n"
def fake_run(cmd, *, cwd, env):
if (
len(cmd) >= 3
and cmd[0:2] == ["docker", "compose"]
and cmd[-1] == "config"
):
return 0, "services: {}\n", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
argv = [
"compose_ca.py",
"--chdir",
"/tmp/app",
"--project",
"p",
"--compose-files",
"-f compose.yml",
"--out",
"compose.ca.override.yml",
"--ca-host",
"/etc/infinito/ca/root-ca.crt",
"--wrapper-host",
"/etc/infinito/bin/with-ca-trust.sh",
"--ca-container",
CA_CERT_CONTAINER,
"--wrapper-container",
CA_WRAPPER_CONTAINER,
"--trust-name",
"infinito.local",
]
with patch("sys.argv", argv):
rc = self.m.main()
self.assertEqual(rc, 0)
self.assertTrue(_write_text.called)
args, _kwargs = _write_text.call_args
written = args[1] if len(args) > 1 else ""
self.assertIn("services", written)
self.assertNotIn("CA_TRUST_NAME", written)
@patch.object(Path, "exists", autospec=True, return_value=True)
@patch.object(Path, "is_dir", autospec=True, return_value=True)
def test_main_requires_trust_name(self, _is_dir, _exists):
argv = [
"compose_ca.py",
"--chdir",
"/tmp/app",
"--project",
"p",
"--compose-files",
"-f compose.yml",
"--out",
"compose.ca.override.yml",
"--ca-host",
"/etc/infinito/ca/root-ca.crt",
"--wrapper-host",
"/etc/infinito/bin/with-ca-trust.sh",
"--ca-container",
CA_CERT_CONTAINER,
"--wrapper-container",
CA_WRAPPER_CONTAINER,
]
with patch("sys.argv", argv), self.assertRaises(SystemExit):
self.m.main()
def test_docker_image_has_bin_sh_true(self):
"""
docker_image_has_bin_sh(): returns True when container run succeeds.
"""
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
self.assertEqual(
cmd[:6],
["docker", "run", "--rm", "--entrypoint", "/bin/sh", "img:1"],
)
self.assertEqual(cmd[6:], ["-c", "exit 0"])
return 0, "", ""
with patch.object(self.m, "run", side_effect=fake_run):
ok = self.m.docker_image_has_bin_sh("img:1", cwd=Path("/tmp"), env={})
self.assertTrue(ok)
def test_docker_image_has_bin_sh_false(self):
"""
docker_image_has_bin_sh(): returns False when container run fails (distroless-like).
"""
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
return 1, "", 'exec: "/bin/sh": stat /bin/sh: no such file or directory'
with patch.object(self.m, "run", side_effect=fake_run):
ok = self.m.docker_image_has_bin_sh("img:1", cwd=Path("/tmp"), env={})
self.assertFalse(ok)
def test_docker_image_has_bin_sh_ambiguous_on_timeout(self):
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
return self.m.TIMEOUT_RC, "", "timed out after 60s"
with patch.object(self.m, "run", side_effect=fake_run):
state = self.m.docker_image_has_bin_sh("img:1", cwd=Path("/tmp"), env={})
self.assertIsNone(state)
def test_gather_one_image_pulls_when_absent(self):
"""An absent image is pulled + re-inspected so the /bin/sh probe is accurate."""
calls = []
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
if ["docker", "pull", "img:1"] in calls:
cfg = [{"Config": {"Entrypoint": ["/entry.sh"], "Cmd": ["run"]}}]
return 0, json.dumps(cfg), ""
return 1, "", "No such image: img:1"
if cmd[:2] == ["docker", "pull"]:
return 0, "", ""
if cmd[:5] == ["docker", "run", "--rm", "--entrypoint", "/bin/sh"]:
return 0, "", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
exists, ep, cmd, has_sh = self.m._gather_one_image(
"img:1", cwd=Path("/tmp"), env={}
)
self.assertTrue(exists)
self.assertEqual(ep, ["/entry.sh"])
self.assertEqual(cmd, ["run"])
self.assertTrue(has_sh)
self.assertIn(["docker", "pull", "img:1"], calls)
def test_gather_one_image_no_pull_when_present(self):
"""When the image is already present, `_gather_one_image` must NOT pull."""
calls = []
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
cfg = [{"Config": {"Entrypoint": ["/entry.sh"], "Cmd": ["run"]}}]
return 0, json.dumps(cfg), ""
if cmd[:5] == ["docker", "run", "--rm", "--entrypoint", "/bin/sh"]:
return 0, "", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
exists, _ep, _cmd, has_sh = self.m._gather_one_image(
"img:1", cwd=Path("/tmp"), env={}
)
self.assertTrue(exists)
self.assertTrue(has_sh)
self.assertNotIn(["docker", "pull", "img:1"], calls)
def test_gather_one_image_never_pulls_built_tags(self):
"""A locally-built tag must not be pulled: the registry would serve an
unrelated base image whose CMD then gets pinned into the override."""
calls = []
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
calls.append(cmd)
if cmd[:3] == ["docker", "image", "inspect"]:
return 1, "", "No such image: python:3.14-slim"
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
exists, _ep, _cmd, _has_sh = self.m._gather_one_image(
"python:3.14-slim", cwd=Path("/tmp"), env={}, pull_if_absent=False
)
self.assertFalse(exists)
self.assertFalse(any(c[:2] == ["docker", "pull"] for c in calls))
def test_gather_image_meta_skips_pull_for_built_images(self):
"""gather_image_meta must route built tags through pull_if_absent=False."""
seen = {}
def fake_gather(image, *, cwd, env, pull_if_absent=True):
seen[image] = pull_if_absent
return (False, [], [], False)
with patch.object(self.m, "_gather_one_image", side_effect=fake_gather):
self.m.gather_image_meta(
["built:1", "pulled:1"],
cwd=Path("/tmp"),
env={},
built_images={"built:1"},
)
self.assertFalse(seen["built:1"])
self.assertTrue(seen["pulled:1"])
def test_render_override_wraps_when_probe_ambiguous(self):
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run"], None)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out.get("entrypoint"), [CA_WRAPPER_CONTAINER])
self.assertEqual(out.get("command"), ["/entry", "run"])
def test_render_override_wraps_when_inspect_ambiguous(self):
services = {
"svc": {
"image": "img:1",
"entrypoint": ["/svc-entry"],
"command": ["svc-run"],
}
}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (False, [], [], None)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out.get("entrypoint"), [CA_WRAPPER_CONTAINER])
self.assertEqual(out.get("command"), ["/svc-entry", "svc-run"])
def test_render_override_env_only_when_probe_clean_negative(self):
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run"], False)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertNotIn("entrypoint", out)
self.assertNotIn("command", out)
self.assertIn("environment", out)
self.assertIn("volumes", out)
def test_gather_one_image_inspect_timeout_is_ambiguous(self):
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
if cmd[:3] == ["docker", "image", "inspect"]:
return self.m.TIMEOUT_RC, "", "timed out after 90s"
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
meta = self.m._gather_one_image("img:1", cwd=Path("/tmp"), env={})
self.assertEqual(meta, (False, [], [], None))
def test_gather_one_image_inspect_clean_negative_no_shell(self):
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
if cmd[:3] == ["docker", "image", "inspect"]:
return 1, "", "no such image"
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
meta = self.m._gather_one_image("img:1", cwd=Path("/tmp"), env={})
self.assertEqual(meta, (False, [], [], False))
def test_render_override_sets_env_and_mounts_always(self):
"""
render_override(): must always set CA env vars + fallback envs and always mount CA+wrapper.
This must hold even for images without /bin/sh (no entrypoint override in that case).
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run"], False)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
self.assertIn("services", doc)
self.assertIn("svc", doc["services"])
out = doc["services"]["svc"]
self.assertIn("volumes", out)
self.assertIn(f"/host/ca.crt:{CA_CERT_CONTAINER}:ro", out["volumes"])
self.assertIn(
f"/host/with-ca-trust.sh:{CA_WRAPPER_CONTAINER}:ro",
out["volumes"],
)
env = out.get("environment", {})
self.assertEqual(env.get("CA_TRUST_CERT"), CA_CERT_CONTAINER)
self.assertEqual(env.get("CA_TRUST_NAME"), "infinito.local")
self.assertEqual(env.get("SSL_CERT_FILE"), CA_CERT_CONTAINER)
self.assertEqual(env.get("REQUESTS_CA_BUNDLE"), CA_CERT_CONTAINER)
self.assertEqual(env.get("CURL_CA_BUNDLE"), CA_CERT_CONTAINER)
self.assertEqual(env.get("NODE_EXTRA_CA_CERTS"), CA_CERT_CONTAINER)
self.assertNotIn("entrypoint", out)
self.assertNotIn("command", out)
def test_render_override_wraps_when_sh_exists(self):
"""
render_override(): when /bin/sh exists, it must override entrypoint to wrapper
and set command to the effective command (final entrypoint + final cmd).
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run", "--flag"], True)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out.get("entrypoint"), [CA_WRAPPER_CONTAINER])
self.assertEqual(out.get("command"), ["/entry", "run", "--flag"])
def test_render_override_uses_service_entrypoint_command_over_image(self):
"""
If the composed config explicitly sets entrypoint/command, those must win over image defaults.
effective_cmd = svc_entrypoint + svc_command
"""
services = {
"svc": {
"image": "img:1",
"entrypoint": ["/svc-entry"],
"command": ["svc-run", "x"],
}
}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/img-entry"], ["img-run"], True)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out["command"], ["/svc-entry", "svc-run", "x"])
def test_render_override_collapses_shell_command_when_entrypoint_is_shell(self):
"""
If the composed service already uses a shell entrypoint, the command must stay
a single shell payload so /bin/sh -c receives exactly one argument.
"""
shell_tokens = [
"if",
"!",
"grep",
"-qF",
"INFINITO_TAIGA_AUTH_CONFIG", # nocheck: test-fixture
"/taiga-back/settings/config.py",
";",
"then",
"cat",
"/taiga-back/settings/config.append.py",
">>",
"/taiga-back/settings/config.py",
";",
"fi",
";",
"exec",
"/taiga-back/docker/entrypoint.sh",
]
services = {
"svc": {
"image": "img:1",
"entrypoint": ["/bin/sh", "-c"],
"command": shell_tokens,
}
}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run"], True)},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out.get("entrypoint"), [CA_WRAPPER_CONTAINER])
self.assertEqual(out.get("command")[:2], ["/bin/sh", "-c"])
self.assertEqual(
out.get("command")[2],
"if ! grep -qF INFINITO_TAIGA_AUTH_CONFIG /taiga-back/settings/config.py ; then cat /taiga-back/settings/config.append.py >> /taiga-back/settings/config.py ; fi ; exec /taiga-back/docker/entrypoint.sh",
)
def test_render_override_gathers_each_unique_image_once(self):
"""Image metadata is gathered once per unique image, not per service."""
services = {
"a": {"image": "img:1"},
"b": {"image": "img:1"},
}
service_to_cmd = {
"a": ["docker", "compose", "-p", "p", "-f", "compose.yml"],
"b": ["docker", "compose", "-p", "p", "-f", "compose.yml"],
}
with patch.object(
self.m,
"gather_image_meta",
return_value={"img:1": (True, ["/entry"], ["run"], True)},
) as p_gather:
self.m.render_override(
services,
service_to_cmd,
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",
)
p_gather.assert_called_once()
self.assertEqual(p_gather.call_args.args[0], ["img:1"])
def test_main_generates_override_includes_entrypoint_when_sh_exists(self):
"""
main(): when /bin/sh exists for the image, the written override should include entrypoint wrapper.
"""
with (
patch.object(Path, "exists", autospec=True, return_value=True),
patch.object(Path, "is_dir", autospec=True, return_value=True),
patch.object(
Path,
"read_text",
autospec=True,
return_value="services:\n app:\n image: myimage:latest\n",
),
patch.object(Path, "mkdir", autospec=True),
patch.object(Path, "write_text", autospec=True) as p_write,
):
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
if (
len(cmd) >= 3
and cmd[0:2] == ["docker", "compose"]
and cmd[-1] == "config"
):
yml = "services:\n app:\n image: myimage:latest\n"
return 0, yml, ""
if cmd[:3] == ["docker", "image", "inspect"]:
json_out = json.dumps(
[{"Config": {"Entrypoint": ["/entry"], "Cmd": ["run"]}}]
)
return 0, json_out, ""
if cmd[:6] == [
"docker",
"run",
"--rm",
"--entrypoint",
"/bin/sh",
"myimage:latest",
]:
return 0, "", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
argv = [
"compose_ca.py",
"--chdir",
"/tmp/app",
"--project",
"p",
"--compose-files",
"-f compose.yml -f compose.override.yml",
"--out",
"compose.ca.override.yml",
"--ca-host",
"/etc/infinito/ca/root-ca.crt",
"--wrapper-host",
"/etc/infinito/bin/with-ca-trust.sh",
"--ca-container",
CA_CERT_CONTAINER,
"--wrapper-container",
CA_WRAPPER_CONTAINER,
"--trust-name",
"infinito.local",
]
with patch("sys.argv", argv):
rc = self.m.main()
self.assertEqual(rc, 0)
self.assertTrue(p_write.called)
args, _kwargs = p_write.call_args
written = args[1] if len(args) > 1 else ""
parsed = load_yaml_str(written)
self.assertIn("services", parsed)
self.assertIn("app", parsed["services"])
self.assertEqual(
parsed["services"]["app"].get("entrypoint"),
[CA_WRAPPER_CONTAINER],
)
def test_escape_compose_vars_replaces_single_dollar(self):
"""
escape_compose_vars(): must replace $ with $$ so Compose does not interpolate on host.
"""
self.assertEqual(
self.m.escape_compose_vars(['exec "$FOO"']),
['exec "$$FOO"'],
)
self.assertEqual(
self.m.escape_compose_vars(["$FOO", "${FOO}", "x$y", "$", "plain"]),
["$$FOO", "$${FOO}", "x$$y", "$$", "plain"],
)
def test_escape_compose_vars_keeps_strings_without_dollar_unchanged(self):
"""
escape_compose_vars(): must not modify strings without '$'.
"""
argv = ["/entry", "run", "--flag", "exec /bin/app", "x_y-z.1"]
self.assertEqual(self.m.escape_compose_vars(argv), argv)
def test_render_override_escapes_dollar_in_command_when_wrapping(self):
"""
render_override(): when wrapping is enabled (has /bin/sh),
command argv must have $ escaped to $$ to prevent host-side Compose interpolation.
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={
"img:1": (True, [], ["sh", "-lc", 'exec "$CHESS_ENTRYPOINT_INT"'], True)
},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertEqual(out.get("entrypoint"), [CA_WRAPPER_CONTAINER])
self.assertEqual(
out.get("command"),
["sh", "-lc", 'exec "$$CHESS_ENTRYPOINT_INT"'],
)
def test_render_override_points_the_replacing_vars_at_the_bundle(self):
"""A host that has a CA bundle gets the verifying vars pointed at it.
SSL_CERT_FILE, CURL_CA_BUNDLE and REQUESTS_CA_BUNDLE replace the trust
store, so leaving them on the single root CA is what stops a container
from reaching any public HTTPS endpoint. NODE_EXTRA_CA_CERTS is
additive and stays on the root CA, and CA_TRUST_CERT stays single
because the anchor installers take one certificate.
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with tempfile.TemporaryDirectory() as tmp:
ca_host = Path(tmp) / "root-ca.crt"
ca_host.write_text("root\n")
(Path(tmp) / "ca-bundle.crt").write_text("root\npublic\n")
with (
patch.object(self.m, "ensure_image_available"),
patch.object(self.m, "docker_image_inspect"),
patch.object(self.m, "docker_image_has_bin_sh"),
):
doc = self.m.render_override(
services,
service_to_cmd,
cwd=Path("/tmp"),
env={},
ca_host=str(ca_host),
wrapper_host="/host/with-ca-trust.sh",
ca_container=CA_CERT_CONTAINER,
wrapper_container=CA_WRAPPER_CONTAINER,
trust_name="infinito.local",
wrap=False,
)
out = doc["services"]["svc"]
bundle_container = str(Path(CA_CERT_CONTAINER).parent / "ca-bundle.crt")
self.assertIn(
f"{Path(tmp) / 'ca-bundle.crt'}:{bundle_container}:ro", out["volumes"]
)
for key in (
"SSL_CERT_FILE",
"CURL_CA_BUNDLE",
"REQUESTS_CA_BUNDLE",
"CA_TRUST_BUNDLE",
):
self.assertEqual(out["environment"].get(key), bundle_container)
self.assertEqual(
out["environment"].get("NODE_EXTRA_CA_CERTS"), CA_CERT_CONTAINER
)
self.assertEqual(out["environment"].get("CA_TRUST_CERT"), CA_CERT_CONTAINER)
def test_render_override_no_wrapper_emits_env_and_mounts_only(self):
"""
render_override(wrap=False): swarm path must emit only volumes + environment,
never entrypoint/command (docker stack deploy does not un-escape $$), and must
not probe/inspect/pull the image (images need not exist at generation time).
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with (
patch.object(self.m, "ensure_image_available") as p_ensure,
patch.object(self.m, "docker_image_inspect") as p_inspect,
patch.object(self.m, "docker_image_has_bin_sh") as p_has_sh,
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
wrap=False,
)
out = doc["services"]["svc"]
self.assertIn("volumes", out)
self.assertEqual(out["environment"].get("SSL_CERT_FILE"), CA_CERT_CONTAINER)
self.assertNotIn("CA_TRUST_BUNDLE", out["environment"])
self.assertNotIn("entrypoint", out)
self.assertNotIn("command", out)
p_ensure.assert_not_called()
p_inspect.assert_not_called()
p_has_sh.assert_not_called()
def test_render_override_does_not_escape_when_not_wrapping(self):
"""
render_override(): when /bin/sh does NOT exist (distroless-like),
we must not set 'command' at all (and therefore no escaping occurs).
"""
services = {"svc": {"image": "img:1"}}
service_to_cmd = {"svc": ["docker", "compose", "-p", "p", "-f", "compose.yml"]}
with patch.object(
self.m,
"gather_image_meta",
return_value={
"img:1": (
True,
[],
["sh", "-lc", 'exec "$CHESS_ENTRYPOINT_INT"'],
False,
)
},
):
doc = self.m.render_override(
services,
service_to_cmd,
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",
)
out = doc["services"]["svc"]
self.assertNotIn("entrypoint", out)
self.assertNotIn("command", out)
def test_main_writes_override_with_escaped_dollar_when_image_cmd_contains_dollar(
self,
):
"""
main(): if the image Cmd contains '$VAR', the written override must contain '$$VAR'
(Compose-escape) so the container shell can expand it at runtime.
"""
with (
patch.object(Path, "exists", autospec=True, return_value=True),
patch.object(Path, "is_dir", autospec=True, return_value=True),
patch.object(
Path,
"read_text",
autospec=True,
return_value="services:\n app:\n image: myimage:latest\n",
),
patch.object(Path, "mkdir", autospec=True),
patch.object(Path, "write_text", autospec=True) as p_write,
):
def fake_run(cmd, *, cwd, env, timeout=None, capture=True):
if (
len(cmd) >= 3
and cmd[0:2] == ["docker", "compose"]
and cmd[-1] == "config"
):
yml = "services:\n app:\n image: myimage:latest\n"
return 0, yml, ""
if cmd[:3] == ["docker", "image", "inspect"]:
json_out = json.dumps(
[
{
"Config": {
"Entrypoint": [],
"Cmd": [
"sh",
"-lc",
'exec "$CHESS_ENTRYPOINT_INT"',
],
}
}
]
)
return 0, json_out, ""
if cmd[:6] == [
"docker",
"run",
"--rm",
"--entrypoint",
"/bin/sh",
"myimage:latest",
]:
return 0, "", ""
return 1, "", "unexpected"
with patch.object(self.m, "run", side_effect=fake_run):
argv = [
"compose_ca.py",
"--chdir",
"/tmp/app",
"--project",
"p",
"--compose-files",
"-f compose.yml",
"--out",
"compose.ca.override.yml",
"--ca-host",
"/etc/infinito/ca/root-ca.crt",
"--wrapper-host",
"/etc/infinito/bin/with-ca-trust.sh",
"--ca-container",
CA_CERT_CONTAINER,
"--wrapper-container",
CA_WRAPPER_CONTAINER,
"--trust-name",
"infinito.local",
]
with patch("sys.argv", argv):
rc = self.m.main()
self.assertEqual(rc, 0)
self.assertTrue(p_write.called)
args, _kwargs = p_write.call_args
written = args[1] if len(args) > 1 else ""
parsed = load_yaml_str(written)
self.assertIn("services", parsed)
self.assertIn("app", parsed["services"])
self.assertEqual(
parsed["services"]["app"].get("command"),
["sh", "-lc", 'exec "$$CHESS_ENTRYPOINT_INT"'],
)
if __name__ == "__main__":
unittest.main()