Files
computer-playbook/plugins/lookup/container_dns.py
T
kevinveenbirkenbachandClaude Opus 5 7c9a29b2a7 fix(container-dns): stop the resolver list flapping
The bridge resolver comes from ansible_facts docker0, a fact that does not exist on a first deploy: the same role installs docker, and the daemon only starts on this very template's notify. Run 1 therefore renders without the bridge and run 2 with it. Where bridge and clearnet are the same address that is one entry against two - different bytes, so the template reports changed, the handler fires, and dockerd restarts underneath a live stack. In CI run 32119014832 that restart killed postgres on the manager at 12:42:26 and the deploy unravelled from there.

Deduplicating makes both renders collide on identical bytes. Order is preserved: the bridge must stay first or dnsmasq never sees the onion names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:28:19 +02:00

53 lines
2.0 KiB
Python

from __future__ import annotations
from typing import Any
from ansible.plugins.lookup import LookupBase
_TOR_ROLE = "svc-net-tor"
def resolve_container_dns(variables: dict[str, Any]) -> list[str]:
"""Return the resolver list for the container runtime, most specific first.
On a Tor node the node dnsmasq resolves ``.onion`` through Tor's DNSPort
and everything else through the clearnet upstream, which is what makes
per-app proxy settings unnecessary. It is only reachable on the docker
bridge address because docker discards loopback resolvers, and it is
absent before docker itself is installed, hence the guard.
Both deploy modes get the bridge: a Tor node runs the same dnsmasq
listener in compose and in swarm, so both resolve onions alike.
The clearnet resolver stays as a second entry so that losing dnsmasq costs
onion names rather than all name resolution. Duplicates are dropped: where
the bridge *is* the clearnet resolver, emitting it twice makes daemon.json
differ between a run before and a run after ``docker0`` exists, and that
difference restarts the container runtime underneath a live stack.
"""
facts = variables.get("ansible_facts") or {}
bridge = ((facts.get("docker0") or {}).get("ipv4") or {}).get("address") or ""
on_tor_node = _TOR_ROLE in (variables.get("group_names") or [])
clearnet = ((variables.get("networks") or {}).get("internet") or {}).get(
"dns"
) or ""
resolvers = (bridge if on_tor_node else "", clearnet)
return list(dict.fromkeys(str(r) for r in resolvers if r))
class LookupModule(LookupBase):
"""
Usage:
{{ lookup('container_dns') }}
Resolver list for the docker daemon's ``dns`` key. Takes no terms.
"""
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
if terms:
raise ValueError("lookup('container_dns') takes no positional terms.")
variables = variables or getattr(self._templar, "available_variables", {}) or {}
return [resolve_container_dns(variables)]