fix(tor): resolve DEPLOYMENT_MODE before picking the SOCKS host

A lookup plugin receives group_vars through variables.get() untemplated, and
group_vars/all/18_swarm.yml:2 defines DEPLOYMENT_MODE as a Jinja expression.
The comparison against 'swarm' could therefore never match, so every swarm
deploy fell through to _COMPOSE_HOST and baked
socks5h://host.docker.internal:9050 into Nextcloud's proxy system value.

svc-net-tor publishes SOCKS with mode: host, so that listener exists on the
manager alone. With three Nextcloud replicas against one Tor node the failure
is node-determined, not intermittent: the 104 'cURL error 7 ... over proxy
host.docker.internal' lines in run 31340965955 split 56 on wrk-02, 48 on
wrk-01 and 0 on mgr-01, which is what made the OIDC callback 500 on 2 of 3
replicas and read as a 39% flake.

Templating first yields socks5h://tor:9050 in swarm, which every replica
reaches over the overlay it already joins through the onion_sso consumer rule.
Compose is unchanged: a single-node inventory renders the expression to
'compose', and a suppressed template error falls through to the same branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-11 12:01:44 +02:00
1 parent 05f425ea51
commit 1bb31fba41
2 files changed
+53 -3

No files matched your search

+8 -3
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import contextlib
from typing import Any
from ansible.errors import AnsibleError
@@ -46,6 +47,10 @@ class LookupModule(LookupBase):
templar=getattr(self, "_templar", None),
).run([], variables=variables, roles_dir=kwargs.get("roles_dir"))[0]
return [
resolve_proxy_url(applications, variables.get("DEPLOYMENT_MODE", "compose"))
]
raw_mode = variables.get("DEPLOYMENT_MODE", "compose")
templar = getattr(self, "_templar", None)
if templar is not None:
with contextlib.suppress(Exception):
raw_mode = templar.template(raw_mode)
return [resolve_proxy_url(applications, str(raw_mode).strip())]
@@ -1,8 +1,10 @@
from __future__ import annotations
import unittest
from unittest import mock
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from plugins.lookup.tor_socks_proxy import LookupModule, resolve_proxy_url
@@ -38,6 +40,27 @@ class TestResolveProxyUrl(unittest.TestCase):
resolve_proxy_url({"svc-net-tor": {"services": {"tor": {}}}}, "swarm")
DEPLOYMENT_MODE_EXPR = (
"{{ 'swarm' if (groups['svc-swarm-node'] | default([]) | length) > 1"
" else 'compose' }}"
)
class _StubTemplar:
"""Templar stub resolving the DEPLOYMENT_MODE expression to a fixed mode.
Args:
resolved: the mode string the expression evaluates to.
"""
def __init__(self, resolved: str) -> None:
self.resolved = resolved
self.available_variables: dict[str, object] = {}
def template(self, value):
return self.resolved if value == DEPLOYMENT_MODE_EXPR else value
class TestTorSocksProxyLookup(unittest.TestCase):
def test_terms_raise(self):
lookup = LookupModule()
@@ -45,6 +68,28 @@ class TestTorSocksProxyLookup(unittest.TestCase):
with self.assertRaises(AnsibleError):
lookup.run(["x"], variables={})
def _run(self, resolved: str) -> str:
lookup = LookupModule()
lookup._templar = _StubTemplar(resolved)
lookup._loader = None
variables = {
"DEPLOYMENT_MODE": DEPLOYMENT_MODE_EXPR,
"applications": APPS,
}
with mock.patch.object(lookup_loader, "get", return_value=_StubApplications()):
return lookup.run([], variables=variables)[0]
def test_a_swarm_deployment_reaches_tor_over_the_overlay(self):
self.assertEqual(self._run("swarm"), "socks5h://tor:9050")
def test_a_compose_deployment_reaches_tor_over_the_host_gateway(self):
self.assertEqual(self._run("compose"), "socks5h://host.docker.internal:9050")
class _StubApplications:
def run(self, terms, variables=None, roles_dir=None):
return [APPS]
if __name__ == "__main__":
unittest.main()