mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
extra_hosts is the one compose key a service may not carry twice. The onion SSO
back-channel pin was emitted by a Jinja partial that six roles included, while
six other services wrote the key by hand; nothing stopped a service from
receiving both. YAML then either rejects the document or keeps one list and
drops the other, silently.
The partial becomes plugins/lookup/container_extra_hosts.py, and every pin now
goes through it. Callers add their own entries with extra_hosts=[...], the way
container_volumes already takes extra_volumes=, and the lookup merges them after
the automatic pin and deduplicates in first-seen order. With nothing to emit it
returns an empty string.
The swarm address reasoning from 7b1839681 is preserved and now written down in
the plugin rather than only in that commit: the pin must NOT be host-gateway in
swarm, because that alias resolves per node while openresty publishes :80 with
mode: host under node.role == manager. It carries the stack host's address,
which reaches every task because the compose file is rendered once with the
manager as inventory host. The mode is read as compose_mode_force or
DEPLOYMENT_MODE, not the bare cluster mode, so a role with an override does not
lose its pin and its overlay at once.
Two roles kept a pin that now collides with the automatic one, and both are
resolved the same way -- the hand-written entry stays for clearnet and yields
the .onion case to the lookup, which picks the address per mode. In
web-app-mailu the three conditional entries move into a role-local
mailu_extra_hosts lookup exposed as MAILU_EXTRA_HOSTS, because an eight-line
conditional expression in the template argument is worse than the block it
replaced. It returns its entries as separate results, so vars/main.yml uses
query(); plain lookup() would join them into one comma-separated string.
tests/lint/ansible/jinja/test_compose_extra_hosts_use_lookup.py fails on any
literal extra_hosts: key in a role template, so the invariant survives the next
author who needs a pin. It follows test_compose_ports_use_container_ports_lookup:
the raw construct is forbidden, the lookup is not mandated per service -- a
service with no pins is correct without it, and the swarm branch raises when
facts are absent, which would turn a no-op into a failure surface for containers
that never speak OIDC.
Equivalence checked by rendering rather than by reading: under the trim_blocks +
lstrip_blocks the stack-host action forces for compose.yml.j2, both former call
site shapes (indented, and matrix's column 0) emit byte-identical text to the
new form when the block applies. The no-op case differs -- a whitespace-only
line where the include emitted none -- and yaml.safe_load returns the same
document for both.
31 unit tests; make test-lint 325 passed. Does not touch web-app-espocrm, whose
missing pin is a separate claim and gets its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""Lookup ``mailu_extra_hosts``: the host pins Mailu's admin container needs.
|
|
|
|
Each pin carries its own condition, and the list is handed to the
|
|
``container_extra_hosts`` SPOT as its ``extra_hosts=`` argument rather than
|
|
written as a second ``extra_hosts:`` key:
|
|
|
|
* the Keycloak vhost, when Mailu speaks OIDC from inside the containerised rig
|
|
and reaches the provider through the host gateway. Only on clearnet: an
|
|
``.onion`` provider is pinned by ``container_extra_hosts`` itself, which
|
|
picks the address per deploy mode, and emitting a second line for the same
|
|
name here would leave two addresses in ``/etc/hosts`` under swarm;
|
|
* the central database, and
|
|
* the central Redis, both only in swarm and only as literal addresses: the
|
|
overlay VIPs are resolved in ``tasks/01_swarm_resolve_peer_vips.yml`` because
|
|
``extra_hosts`` rejects names. Redis is skipped when the role runs its own.
|
|
|
|
Returns the entries as separate results, so the call site must use ``query``;
|
|
plain ``lookup`` would join them into one comma-separated string.
|
|
|
|
Usage, through the constant in ``vars/main.yml``:
|
|
|
|
MAILU_EXTRA_HOSTS: "{{ query('mailu_extra_hosts') }}"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.module_utils.parsing.convert_bool import boolean
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.networks.lookup_context import resolve_var
|
|
|
|
HOST_GATEWAY = "host-gateway"
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(
|
|
self,
|
|
terms: list[Any] | None,
|
|
variables: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> list[str]:
|
|
if terms:
|
|
raise AnsibleError("mailu_extra_hosts lookup takes no positional terms")
|
|
|
|
self._vars = (
|
|
variables or getattr(self._templar, "available_variables", {}) or {}
|
|
)
|
|
|
|
pins: list[str] = []
|
|
oidc_host = self._var("MAILU_OIDC_HOST")
|
|
if (
|
|
self._flag("DOCKER_IN_CONTAINER")
|
|
and self._flag("MAILU_OIDC_ENABLED")
|
|
and not oidc_host.endswith(".onion")
|
|
):
|
|
pins.append(f"{oidc_host}:{HOST_GATEWAY}")
|
|
|
|
if self._var("DEPLOYMENT_MODE") == "swarm":
|
|
pins.append(
|
|
f"{self._var('MAILU_SWARM_DB_HOST')}:{self._var('MAILU_SWARM_DB_ADDR')}"
|
|
)
|
|
if not self._redis_is_local():
|
|
pins.append(
|
|
f"{self._var('MAILU_SWARM_REDIS_HOST')}:"
|
|
f"{self._var('MAILU_SWARM_REDIS_ADDR')}"
|
|
)
|
|
|
|
return pins
|
|
|
|
def _redis_is_local(self) -> bool:
|
|
application_id = self._var("application_id")
|
|
return bool(
|
|
lookup_loader.get(
|
|
"engine", loader=self._loader, templar=getattr(self, "_templar", None)
|
|
).run(["redis", application_id, "local"], variables=self._vars)[0]
|
|
)
|
|
|
|
def _var(self, name: str) -> str:
|
|
"""Read one play var, rendered, as a stripped string.
|
|
|
|
Args:
|
|
name: variable name; role vars reach a lookup as templates.
|
|
"""
|
|
templar = getattr(self, "_templar", None)
|
|
return str(resolve_var(templar, self._vars.get(name, "")) or "").strip()
|
|
|
|
def _flag(self, name: str) -> bool:
|
|
"""Read one play var as a boolean the way Jinja's ``| bool`` would.
|
|
|
|
Args:
|
|
name: variable name; an unrecognised value counts as false.
|
|
"""
|
|
templar = getattr(self, "_templar", None)
|
|
return boolean(resolve_var(templar, self._vars.get(name, False)), strict=False)
|