Files
computer-playbook/plugins/lookup/tor_socks_proxy.py
kevinveenbirkenbachandClaude Opus 5 1bb31fba41 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>
2026-08-11 12:01:44 +02:00

57 lines
2.0 KiB
Python

from __future__ import annotations
import contextlib
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from plugins.lookup.tor_socks import resolve_socks_endpoint
_SWARM_HOST = "tor"
_COMPOSE_HOST = "host.docker.internal"
def resolve_proxy_url(applications: dict[str, Any], deployment_mode: str) -> str:
"""Return the ``socks5h://`` URL a sibling container uses to reach Tor:
the ``tor`` overlay alias in swarm, the host gateway in compose.
"""
host = _SWARM_HOST if str(deployment_mode).strip() == "swarm" else _COMPOSE_HOST
return f"socks5h://{resolve_socks_endpoint(applications, host)}"
class LookupModule(LookupBase):
"""
Usage:
{{ lookup('tor_socks_proxy') }} -> socks5h://tor:<port> (swarm)
-> socks5h://host.docker.internal:<port> (compose)
Single spot for the outbound Tor SOCKS proxy URL of a containerised client,
so the swarm/compose split is not restated per consuming role.
"""
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[str]:
if terms:
raise AnsibleError("lookup('tor_socks_proxy') expects no terms.")
variables = variables or getattr(self._templar, "available_variables", {}) or {}
applications = lookup_loader.get(
"applications",
loader=getattr(self, "_loader", None),
templar=getattr(self, "_templar", None),
).run([], variables=variables, roles_dir=kwargs.get("roles_dir"))[0]
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())]