mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The version-source update in #1146 moved the bigbluebutton/docker ref from v3.0.23 to v3.0.39. That tag changes three things the role did not handle, and it exposed a fourth defect in compose_file_args. 1. generate-compose renders etherpad and bbb-pads only behind USE_INSECURE_ETHERPAD and otherwise emits bbb-shared-notes-server with a `build:` block and no `image:` (upstream adds the image only after the tag, in 9a6a9a9a4c). compose_mods drops `build` only where an image is set, so the guard "Fail if any service still builds locally" stopped every BigBlueButton deploy (main run 39749899667, Nextcloud#11: `rc: 0`, `stdout: ' build:'`, bootstrap_compose_project.yml:35). meta/services.yml now pins alangecker/bbb-shared-notes-server:v3.0.39, compose_mods takes `pinned` (BBB_PINNED_SERVICES, every service the role config pins an image for) and drops their `build`, and the etherpad leftovers go (ep_redis_publisher patch, etherpad postgres entrypoint and initdb hook, etherpad database tasks, ETHERPAD_* env, etherpad_api_key secret). 2. The notes server connects to postgres once at start and exits with 2 when it is not up yet; upstream gives it no restart policy. The override sets the role's restart policy, so it retries until postgres accepts connections. 3. Greenlight moves from v3.8.0 to v3.9.0.1, which sets `config.hosts = URL_HOST`, and the template passes `URL_HOST: https://${DOMAIN}`. Rails compares hostnames, so every request was blocked (`Blocked hosts: bbb.talk.infinito.test`, 403). Greenlight documents URL_HOST as the bare hostname; the override sets it to the role domain. 4. compose_file_args decided whether a role ships a compose override by templating "{{ application_id | abs_role_path_by_application_id }}" through the templar. ansible-core 2.21 does not render an untrusted string, so the check saw the literal and always returned False: the CA override generation and the pull handler never read BBB's compose.override.yml. That was harmless while compose.yml alone was valid; with the notes-server image living in the override, `compose config` failed ("has neither an image nor a build context"). The check now resolves the role path for the application id it is given. BBB is the only role that ships an override, so no other role's file list changes. Existing installations drop the etherpad and bbb-pads containers on the next `compose up --remove-orphans`; the old `etherpad` database stays in PostgreSQL untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
import importlib.util
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
|
|
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 _TlsResolveStub:
|
|
def __init__(self, enabled: bool, mode: str):
|
|
self._enabled = enabled
|
|
self._mode = mode
|
|
|
|
def run(self, terms, variables=None, **kwargs):
|
|
return [{"enabled": self._enabled, "mode": self._mode}]
|
|
|
|
|
|
class _DomainsResolveStub:
|
|
def run(self, terms, variables=None, **kwargs):
|
|
return [(variables or {}).get("domains", {})]
|
|
|
|
|
|
class TestComposeFArgs(unittest.TestCase):
|
|
def setUp(self):
|
|
self.m = _load_module(
|
|
"plugins/lookup/compose_file_args.py",
|
|
"compose_file_args_mod",
|
|
)
|
|
self.lookup = self.m.LookupModule()
|
|
|
|
self.lookup._loader = object()
|
|
self.lookup._templar = object()
|
|
|
|
self.vars = {
|
|
"DIR_COMPOSITIONS": "/x/",
|
|
"domains": {
|
|
"web-app-a": "example.invalid",
|
|
},
|
|
}
|
|
|
|
def _loader_dispatch(self, tls_stub):
|
|
# "domains" resolves through variables['domains'] to keep tests hermetic;
|
|
# "tls" resolves through the per-test stub.
|
|
def _get(name, *args, **kwargs):
|
|
if name == "domains":
|
|
return _DomainsResolveStub()
|
|
if name == "tls":
|
|
return tls_stub
|
|
raise AssertionError(f"unexpected lookup name: {name}")
|
|
|
|
return _get
|
|
|
|
def _stub_get_docker_paths(self, application_id: str, base_dir: str) -> dict:
|
|
self.assertEqual(application_id, "web-app-a")
|
|
self.assertEqual(base_dir, "/x/")
|
|
return {
|
|
"files": {
|
|
"compose": "/x/compose.yml",
|
|
"compose_override": "/x/compose.override.yml",
|
|
"compose_ca_override": "/x/compose.ca.override.yml",
|
|
}
|
|
}
|
|
|
|
def test_includes_base_and_override_when_role_provides_override_and_tls_off(self):
|
|
with (
|
|
patch.object(
|
|
self.m, "get_docker_paths", side_effect=self._stub_get_docker_paths
|
|
),
|
|
patch.object(self.m, "_role_provides_override", return_value=True),
|
|
patch.object(
|
|
self.m.lookup_loader,
|
|
"get",
|
|
side_effect=self._loader_dispatch(_TlsResolveStub(False, "off")),
|
|
),
|
|
):
|
|
out = self.lookup.run(["web-app-a"], variables=self.vars)[0]
|
|
|
|
self.assertEqual(out, "-f /x/compose.yml -f /x/compose.override.yml")
|
|
|
|
def test_flag_kwarg_emits_compose_file_for_stack_deploy(self):
|
|
with (
|
|
patch.object(
|
|
self.m, "get_docker_paths", side_effect=self._stub_get_docker_paths
|
|
),
|
|
patch.object(self.m, "_role_provides_override", return_value=True),
|
|
patch.object(
|
|
self.m.lookup_loader,
|
|
"get",
|
|
side_effect=self._loader_dispatch(_TlsResolveStub(False, "off")),
|
|
),
|
|
):
|
|
out = self.lookup.run(
|
|
["web-app-a"], variables=self.vars, flag="--compose-file"
|
|
)[0]
|
|
|
|
self.assertEqual(
|
|
out, "--compose-file /x/compose.yml --compose-file /x/compose.override.yml"
|
|
)
|
|
|
|
def test_invalid_flag_rejected(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run(["web-app-a"], variables=self.vars, flag="-bad")
|
|
|
|
def test_includes_ca_override_when_self_signed_and_domain_exists(self):
|
|
with (
|
|
patch.object(
|
|
self.m, "get_docker_paths", side_effect=self._stub_get_docker_paths
|
|
),
|
|
patch.object(self.m, "_role_provides_override", return_value=True),
|
|
patch.object(
|
|
self.m.lookup_loader,
|
|
"get",
|
|
side_effect=self._loader_dispatch(_TlsResolveStub(True, "self_signed")),
|
|
),
|
|
):
|
|
out = self.lookup.run(["web-app-a"], variables=self.vars)[0]
|
|
|
|
self.assertEqual(
|
|
out,
|
|
"-f /x/compose.yml -f /x/compose.override.yml -f /x/compose.ca.override.yml",
|
|
)
|
|
|
|
def test_fails_when_ca_override_missing_but_required(self):
|
|
def stub_missing_ca(application_id: str, base_dir: str) -> dict:
|
|
return {
|
|
"files": {
|
|
"compose": "/x/compose.yml",
|
|
"compose_override": "/x/compose.override.yml",
|
|
"compose_ca_override": "",
|
|
}
|
|
}
|
|
|
|
with (
|
|
patch.object(self.m, "get_docker_paths", side_effect=stub_missing_ca),
|
|
patch.object(self.m, "_role_provides_override", return_value=True),
|
|
patch.object(
|
|
self.m.lookup_loader,
|
|
"get",
|
|
side_effect=self._loader_dispatch(_TlsResolveStub(True, "self_signed")),
|
|
),
|
|
self.assertRaises(AnsibleError),
|
|
):
|
|
self.lookup.run(["web-app-a"], variables=self.vars)
|
|
|
|
def test_includes_only_base_when_role_does_not_provide_override(self):
|
|
with (
|
|
patch.object(
|
|
self.m, "get_docker_paths", side_effect=self._stub_get_docker_paths
|
|
),
|
|
patch.object(self.m, "_role_provides_override", return_value=False),
|
|
patch.object(
|
|
self.m.lookup_loader,
|
|
"get",
|
|
side_effect=self._loader_dispatch(_TlsResolveStub(False, "off")),
|
|
),
|
|
):
|
|
out = self.lookup.run(["web-app-a"], variables=self.vars)[0]
|
|
|
|
self.assertEqual(out, "-f /x/compose.yml")
|
|
|
|
def test_role_provides_override_reads_the_role_of_the_given_application(self):
|
|
with (
|
|
tempfile.TemporaryDirectory() as role,
|
|
patch.object(self.m, "abs_role_path_by_application_id", return_value=role),
|
|
):
|
|
self.assertFalse(self.m._role_provides_override(application_id="x"))
|
|
(Path(role) / "templates").mkdir()
|
|
(Path(role) / "templates" / "compose.override.yml.j2").write_text("")
|
|
self.assertTrue(self.m._role_provides_override(application_id="x"))
|
|
|
|
def test_requires_one_term(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run([], variables=self.vars)
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run(["a", "b"], variables=self.vars)
|
|
|
|
def test_requires_path_docker_compose_instances(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run(
|
|
["web-app-a"], variables={"domains": {"web-app-a": "example.invalid"}}
|
|
)
|
|
|
|
def test_requires_docker_compose_structure_from_get_docker_paths(self):
|
|
with (
|
|
patch.object(self.m, "get_docker_paths", return_value="nope"),
|
|
self.assertRaises(AnsibleError),
|
|
):
|
|
self.lookup.run(["web-app-a"], variables=self.vars)
|
|
|
|
with (
|
|
patch.object(self.m, "get_docker_paths", return_value={}),
|
|
self.assertRaises(AnsibleError),
|
|
):
|
|
self.lookup.run(["web-app-a"], variables=self.vars)
|
|
|
|
with (
|
|
patch.object(self.m, "get_docker_paths", return_value={"files": "nope"}),
|
|
self.assertRaises(AnsibleError),
|
|
):
|
|
self.lookup.run(["web-app-a"], variables=self.vars)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|