mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
A sibling container is reached at http://<entity name>:<internal port>,
and that string was written out per call site: a literal protocol, an
entity_name interpolation for the host, and a lookup('config', ...,
'services.<key>.ports.internal.http') for the port. Nine of them across
seven roles, each needing its own `# nocheck: literal-protocol-lookup`,
because the sibling lint rightly objects to a protocol glued in front of
a lookup - the protocol is supposed to come from wherever the TLS
posture is decided.
A container network has no TLS posture. It speaks plaintext by
construction, which is a property of the network rather than of any
caller, so stating it nine times stated it eight times too often.
lookup('container_url', application_id, service_key) holds all three
facts, and because no literal protocol is left at the call site the
suppression markers go with them.
The port is deliberately the internal one. A sibling that goes out to
the published port leaves the container network and comes back through
the ingress, which the dns-search suffix turns into the public FQDN.
The lookup refuses a service that declares no internal http port rather
than falling back to anything.
test_container_url_lookup.py keeps the shape from coming back: a
protocol, an interpolation mentioning entity_name, a colon, a second
interpolation. Nothing else matches that, and `# nocheck:
container-url-lookup` covers an origin this lookup genuinely cannot
express - a port that is not ports.internal.http, or a host that is not
the application's entity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
"""Lookup `container_url`: the origin a sibling container is reached on.
|
|
|
|
One place for the three facts a container-network URL is made of: the
|
|
hostname Docker resolves, the port the service listens on inside the
|
|
network, and the protocol that network speaks.
|
|
|
|
The protocol is the reason this is a lookup rather than a two-term
|
|
expression. ``tests/lint/repository/yaml/test_literal_protocol_with_lookup.py``
|
|
forbids gluing a literal ``http://`` in front of a value a lookup
|
|
produced, because the protocol must come from wherever the TLS posture
|
|
is decided. A container network has no TLS posture: it speaks plaintext
|
|
by construction. Writing that out per call site meant repeating the same
|
|
``# nocheck: literal-protocol-lookup`` next to every one of them, which
|
|
states a decision nine times instead of once. Holding it here states it
|
|
once, and the call sites carry no literal protocol for the lint to see.
|
|
|
|
Reached with the ``entity_name`` of the application rather than the
|
|
service key, matching how every existing caller of this shape addresses
|
|
a sibling and how ``docker stack deploy`` names the stack.
|
|
|
|
The port is ``services.<service_key>.ports.internal.http``. Internal,
|
|
never published: the published port exists for the host, and a sibling
|
|
that goes out to it leaves the container network and comes back through
|
|
the ingress, which the dns-search suffix turns into the public FQDN.
|
|
|
|
The result carries no trailing slash and no path, so a caller appends
|
|
its own:
|
|
|
|
GITEA_INTERNAL_API_URL: "{{ lookup('container_url', application_id, 'gitea') }}/api/v1"
|
|
|
|
Args:
|
|
application_id: the role whose container is addressed.
|
|
service_key: its ``meta/services.yml`` key.
|
|
|
|
Returns:
|
|
``http://<entity name>:<internal http port>``.
|
|
|
|
Raises:
|
|
AnsibleError: the application is unknown, the service key is absent,
|
|
or it declares no internal http port.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.roles.entity.name import entity_name
|
|
|
|
SCHEME = "http"
|
|
|
|
|
|
def _as_str(value: Any) -> str:
|
|
return "" if value is None else str(value).strip()
|
|
|
|
|
|
def _internal_http_port(
|
|
applications: dict[str, Any], application_id: str, service_key: str
|
|
) -> int:
|
|
app = applications.get(application_id)
|
|
if not isinstance(app, dict):
|
|
raise AnsibleError(f"container_url: unknown application_id '{application_id}'")
|
|
services = app.get("services")
|
|
entry = services.get(service_key) if isinstance(services, dict) else None
|
|
if not isinstance(entry, dict):
|
|
raise AnsibleError(
|
|
f"container_url: service '{service_key}' missing in "
|
|
f"'{application_id}' services config"
|
|
)
|
|
ports = entry.get("ports")
|
|
internal = ports.get("internal") if isinstance(ports, dict) else None
|
|
port = internal.get("http") if isinstance(internal, dict) else None
|
|
try:
|
|
return int(port)
|
|
except (TypeError, ValueError):
|
|
raise AnsibleError(
|
|
f"container_url: services.{service_key}.ports.internal.http is "
|
|
f"{port!r} for '{application_id}', which is not a port"
|
|
) from None
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(
|
|
self,
|
|
terms: list[Any] | None,
|
|
variables: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> list[str]:
|
|
if not terms or len(terms) != 2:
|
|
raise AnsibleError(
|
|
"container_url lookup requires exactly two terms: "
|
|
"application_id and service_key"
|
|
)
|
|
|
|
application_id = _as_str(terms[0])
|
|
service_key = _as_str(terms[1])
|
|
if not application_id:
|
|
raise AnsibleError(
|
|
"container_url: application_id must be a non-empty string"
|
|
)
|
|
if not service_key:
|
|
raise AnsibleError("container_url: service_key must be a non-empty string")
|
|
|
|
vars_ = variables or getattr(self._templar, "available_variables", {}) or {}
|
|
applications = lookup_loader.get(
|
|
"applications", loader=self._loader, templar=getattr(self, "_templar", None)
|
|
).run([], variables=vars_)[0]
|
|
|
|
port = _internal_http_port(applications, application_id, service_key)
|
|
|
|
host = entity_name(application_id)
|
|
if not host:
|
|
raise AnsibleError(
|
|
f"container_url: cannot derive a hostname from application_id "
|
|
f"'{application_id}'"
|
|
)
|
|
|
|
return [f"{SCHEME}://{host}:{port}"]
|