mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Fourteen real conflicts plus the 66 generated READMEs.
utils/cache/files.py and its test: both sides added a skip-dir rule, resolved
as a union so the root `build/` prune and the per-root walk key coexist.
plugins/lookup/database.py and postgres_libraries.py: main's added lookups kept,
with get_entity_name renamed to entity_name as this branch renamed it.
roles/web-app-{mailu,matrix}/meta/services.yml: main's `covers: [email]` and its
MAIL_PROVIDER-gated email entry, plus this branch's gitlab/jira/n8n/jitsi
consumers, which the merge base carried on neither side.
group_vars/all/17_resource.yml: this branch's lookup('resource', ...) refactor;
main's single added literal is already covered by _POSTGRES_MAX_LOCKS_PER_TRANSACTION.
docs/requirements/035-mcp-proxy-expansion.md: this branch moved the design into
ADR 0003, so the requirement keeps only the user story and the criteria. Main's
delta inside the moved block was two new roles, carried into the ADR audit:
web-app-stalwart as adapter_candidate, svc-net-tor-smtp as privileged_control_plane.
The no_surface total said 99 while its own subcategories summed to 112.
inventories/development/default.yml: biber keeps no explicit email. Both sides fixed
the same CI failure; this branch fixed it in _apply_account_defaults, which renders
biber@{{ DOMAIN_PRIMARY }} (utils/cache/users/__init__.py:68) — byte-identical to
the address main hardcoded, so the inventory line is a second copy of one fact.
roles/web-app-{gitlab,keycloak}/files/playwright/playwright.spec.js and
roles/web-app-gitlab/templates/playwright.env.j2: union of both import sets, with
requireDotenvValue kept over decodeDotenvQuotedValue for CANONICAL_DOMAIN because
an empty value must fail rather than match every substring.
The 66 role READMEs take this side: aceb5295ef moved Cosmos, Quick Setup and
Credits to the documentation build, and test_readme.py now rejects them in a
committed README. Main's prose edits inside them are carried over by hand (the
Mailu deprecation banner, Mailu -> Stalwart in erpnext and zammad), and the two
READMEs main added fresh are stripped of the three generated sections.
roles/web-app-sphinx/README.md stays deleted; 6eb9a59d14 merged that role into
web-app-docs. The root README's generated role table was regenerated from the
merged metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
286 lines
10 KiB
Python
286 lines
10 KiB
Python
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from plugins.lookup.container_extra_hosts import LookupModule
|
|
|
|
ONION = "auth." + "b" * 56 + ".onion"
|
|
MAIL_ONION = "mail." + "c" * 56 + ".onion"
|
|
|
|
|
|
class TestContainerExtraHostsLookup(unittest.TestCase):
|
|
def setUp(self):
|
|
self.lookup = LookupModule()
|
|
|
|
def _get(name, *args, **kwargs):
|
|
plugin = MagicMock()
|
|
|
|
def _run(terms, variables=None, **_kwargs):
|
|
vars_ = variables or {}
|
|
if name == "config":
|
|
if "services.email.enabled" in terms:
|
|
return [vars_.get("_email_enabled", False)]
|
|
return [vars_.get("_sso_enabled", False)]
|
|
if name == "tls":
|
|
return [vars_.get("_provider_domain", "")]
|
|
if name == "email":
|
|
return [{"host": vars_.get("_mail_host", "")}]
|
|
raise AssertionError(f"unexpected lookup '{name}'")
|
|
|
|
plugin.run.side_effect = _run
|
|
return plugin
|
|
|
|
patcher = patch(
|
|
"plugins.lookup.container_extra_hosts.lookup_loader.get", side_effect=_get
|
|
)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def _run(self, extra_hosts=None, docker_flags=None, **overrides):
|
|
variables = {
|
|
"application_id": "web-app-espocrm",
|
|
"_sso_enabled": True,
|
|
"_provider_domain": ONION,
|
|
"DEPLOYMENT_MODE": "compose",
|
|
"IS_COMPOSE_MODE": True,
|
|
"IS_SWARM_MODE": False,
|
|
}
|
|
variables.update(overrides)
|
|
kwargs = {} if extra_hosts is None else {"extra_hosts": extra_hosts}
|
|
if docker_flags is not None:
|
|
kwargs["docker_flags"] = docker_flags
|
|
return self.lookup.run(None, variables=variables, **kwargs)[0]
|
|
|
|
def test_an_onion_mail_relay_is_pinned_so_a_proxyless_client_can_reach_it(self):
|
|
"""GitLab's Net::SMTP takes no proxy, so an unpinned .onion relay
|
|
swallows every message: the send is queued and reported successful."""
|
|
block = self._run(
|
|
_sso_enabled=False, _email_enabled=True, _mail_host=MAIL_ONION
|
|
)
|
|
self.assertEqual(
|
|
block,
|
|
'extra_hosts:\n - "host.docker.internal:host-gateway"\n'
|
|
f' - "{MAIL_ONION}:host-gateway"',
|
|
)
|
|
|
|
def test_docker_flags_emit_add_host_for_a_launcher_built_container(self):
|
|
"""Discourse has no compose service -- its launcher builds the
|
|
container from docker_args, so the pins must reach it as run flags."""
|
|
block = self._run(
|
|
_sso_enabled=False,
|
|
_email_enabled=True,
|
|
_mail_host=MAIL_ONION,
|
|
docker_flags=True,
|
|
)
|
|
self.assertEqual(
|
|
block,
|
|
" - --add-host=host.docker.internal:host-gateway\n"
|
|
f" - --add-host={MAIL_ONION}:host-gateway",
|
|
)
|
|
|
|
def test_docker_flags_stay_empty_when_there_is_nothing_to_pin(self):
|
|
self.assertEqual(
|
|
self._run(_sso_enabled=False, _email_enabled=False, docker_flags=True), ""
|
|
)
|
|
|
|
def test_a_clearnet_mail_relay_is_left_alone(self):
|
|
self.assertEqual(
|
|
self._run(
|
|
_sso_enabled=False, _email_enabled=True, _mail_host="mail.example.org"
|
|
),
|
|
"",
|
|
)
|
|
|
|
def test_mail_disabled_emits_nothing(self):
|
|
self.assertEqual(
|
|
self._run(_sso_enabled=False, _email_enabled=False, _mail_host=MAIL_ONION),
|
|
"",
|
|
)
|
|
|
|
def test_swarm_pins_the_mail_relay_to_the_node_address(self):
|
|
block = self._run(
|
|
_sso_enabled=False,
|
|
_email_enabled=True,
|
|
_mail_host=MAIL_ONION,
|
|
DEPLOYMENT_MODE="swarm",
|
|
ansible_facts={"default_ipv4": {"address": "10.0.0.7"}},
|
|
)
|
|
self.assertIn(f' - "{MAIL_ONION}:10.0.0.7"', block)
|
|
|
|
def test_the_sso_and_mail_pins_coexist_without_repeating_the_alias(self):
|
|
block = self._run(_email_enabled=True, _mail_host=MAIL_ONION)
|
|
self.assertEqual(
|
|
block,
|
|
'extra_hosts:\n - "host.docker.internal:host-gateway"\n'
|
|
f' - "{ONION}:host-gateway"\n'
|
|
f' - "{MAIL_ONION}:host-gateway"',
|
|
)
|
|
|
|
def test_compose_pins_the_provider_to_the_host_gateway(self):
|
|
self.assertEqual(
|
|
self._run(),
|
|
'extra_hosts:\n - "host.docker.internal:host-gateway"\n'
|
|
f' - "{ONION}:host-gateway"',
|
|
)
|
|
|
|
def test_swarm_pins_the_provider_to_the_node_address(self):
|
|
block = self._run(
|
|
DEPLOYMENT_MODE="swarm",
|
|
ansible_facts={"default_ipv4": {"address": "10.0.0.7"}},
|
|
)
|
|
self.assertIn(f' - "{ONION}:10.0.0.7"', block)
|
|
self.assertNotIn(f"{ONION}:host-gateway", block)
|
|
|
|
def test_docker_internal_alias_is_always_present(self):
|
|
self.assertIn(' - "host.docker.internal:host-gateway"', self._run())
|
|
|
|
def test_sso_disabled_emits_nothing(self):
|
|
self.assertEqual(self._run(_sso_enabled=False), "")
|
|
|
|
def test_clearnet_provider_emits_nothing(self):
|
|
self.assertEqual(self._run(_provider_domain="auth.example.org"), "")
|
|
|
|
def test_onion_substring_that_is_not_the_suffix_emits_nothing(self):
|
|
self.assertEqual(self._run(_provider_domain="auth.onion.example.org"), "")
|
|
|
|
def test_empty_provider_domain_emits_nothing(self):
|
|
self.assertEqual(self._run(_provider_domain=""), "")
|
|
|
|
def test_compose_mode_force_overrides_the_cluster_mode(self):
|
|
block = self._run(compose_mode_force="compose", DEPLOYMENT_MODE="swarm")
|
|
self.assertIn(f' - "{ONION}:host-gateway"', block)
|
|
|
|
def test_deployment_mode_is_used_without_an_override(self):
|
|
block = self._run(
|
|
compose_mode_force="",
|
|
DEPLOYMENT_MODE="swarm",
|
|
ansible_facts={"default_ipv4": {"address": "10.0.0.8"}},
|
|
)
|
|
self.assertIn(f' - "{ONION}:10.0.0.8"', block)
|
|
|
|
def test_absent_deployment_mode_defaults_to_compose(self):
|
|
variables = {
|
|
"application_id": "web-app-espocrm",
|
|
"_sso_enabled": True,
|
|
"_provider_domain": ONION,
|
|
}
|
|
block = self.lookup.run(None, variables=variables)[0]
|
|
self.assertIn(f' - "{ONION}:host-gateway"', block)
|
|
|
|
def test_swarm_without_facts_raises(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self._run(DEPLOYMENT_MODE="swarm")
|
|
|
|
def test_positional_terms_raise(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run(["web-app-espocrm"], variables={})
|
|
|
|
def test_missing_application_id_raises(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run(None, variables={})
|
|
|
|
def test_application_id_kwarg_overrides_the_play_var(self):
|
|
seen = []
|
|
|
|
def _get(name, *args, **kwargs):
|
|
plugin = MagicMock()
|
|
|
|
def _run(terms, variables=None, **_kwargs):
|
|
if name == "config":
|
|
seen.append(terms[0])
|
|
return [False]
|
|
return [""]
|
|
|
|
plugin.run.side_effect = _run
|
|
return plugin
|
|
|
|
with patch(
|
|
"plugins.lookup.container_extra_hosts.lookup_loader.get", side_effect=_get
|
|
):
|
|
self.lookup.run(
|
|
None,
|
|
variables={"application_id": "web-app-joomla"},
|
|
application_id="web-app-penpot",
|
|
)
|
|
self.assertEqual(set(seen), {"web-app-penpot"})
|
|
|
|
def test_caller_entries_alone_emit_a_block_without_sso(self):
|
|
self.assertEqual(
|
|
self._run(_sso_enabled=False, extra_hosts=["seaweedfs.example:1.2.3.4"]),
|
|
'extra_hosts:\n - "seaweedfs.example:1.2.3.4"',
|
|
)
|
|
|
|
def test_caller_entries_are_appended_after_the_sso_pins(self):
|
|
block = self._run(extra_hosts=["db.example:10.0.0.9"])
|
|
self.assertEqual(
|
|
block.splitlines(),
|
|
[
|
|
"extra_hosts:",
|
|
' - "host.docker.internal:host-gateway"',
|
|
f' - "{ONION}:host-gateway"',
|
|
' - "db.example:10.0.0.9"',
|
|
],
|
|
)
|
|
|
|
def test_a_bare_string_is_accepted(self):
|
|
self.assertIn(
|
|
' - "one.example:192.0.2.1"',
|
|
self._run(_sso_enabled=False, extra_hosts="one.example:192.0.2.1"),
|
|
)
|
|
|
|
def test_duplicates_collapse_in_first_seen_order(self):
|
|
block = self._run(
|
|
extra_hosts=["host.docker.internal:host-gateway", "db.example:10.0.0.9"]
|
|
)
|
|
self.assertEqual(block.count("host.docker.internal"), 1)
|
|
self.assertEqual(
|
|
block.splitlines()[1], ' - "host.docker.internal:host-gateway"'
|
|
)
|
|
|
|
def test_empty_entries_are_dropped(self):
|
|
self.assertEqual(
|
|
self._run(_sso_enabled=False, extra_hosts=["", None, " "]), ""
|
|
)
|
|
|
|
def test_entry_without_a_separator_raises(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self._run(_sso_enabled=False, extra_hosts=["no-address-here"])
|
|
|
|
def test_no_sources_at_all_emit_nothing(self):
|
|
self.assertEqual(self._run(_sso_enabled=False, extra_hosts=[]), "")
|
|
|
|
def _run_entries(self, **overrides):
|
|
variables = {
|
|
"application_id": "web-app-discourse",
|
|
"_sso_enabled": True,
|
|
"_provider_domain": ONION,
|
|
"DEPLOYMENT_MODE": "compose",
|
|
"IS_COMPOSE_MODE": True,
|
|
"IS_SWARM_MODE": False,
|
|
}
|
|
variables.update(overrides)
|
|
return self.lookup.run(None, variables=variables, entries_only=True)
|
|
|
|
def test_entries_only_returns_the_raw_mappings(self):
|
|
self.assertEqual(
|
|
self._run_entries(),
|
|
["host.docker.internal:host-gateway", f"{ONION}:host-gateway"],
|
|
)
|
|
|
|
def test_entries_only_follows_the_swarm_address(self):
|
|
self.assertEqual(
|
|
self._run_entries(
|
|
DEPLOYMENT_MODE="swarm",
|
|
ansible_facts={"default_ipv4": {"address": "10.0.0.7"}},
|
|
),
|
|
["host.docker.internal:host-gateway", f"{ONION}:10.0.0.7"],
|
|
)
|
|
|
|
def test_entries_only_is_empty_without_sso(self):
|
|
self.assertEqual(self._run_entries(_sso_enabled=False), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|