mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
Brings in feature/updateraccb22da75, which carries main5995766f58. Conflict resolutions: - web-app-fider: main's tenant bootstrap (01_tenant.yml) runs first on the stack host, OIDC setup moves to 02_setup_oidc.yml, design to 03_design.yml; flat includes so main's gate test reads them. - sys-front-tls test_nginx_ssl_header: the tls fake maps the domain term store-i18n passes back to its app. - group_vars 04_webserver.yml, sys-svc-proxy CSP header: store-i18n's webserver_resource lookup plus #1256's WEBSERVER_SECURITY_HEADERS and the security_headers.lua include after the CSP block. - web-app-nextcloud: updater's decoupled Talk stack (Janus, signaling, NATS, recording builds) replaces aio-talk; store-i18n's per-network Talk backends move from the wrapper script into signaling.conf.j2 urls (NEXTCLOUD_HPB_SIGNALING_BACKEND_URLS from network_urls, canonical first); IS_COMPOSE_MODE/IS_SWARM_MODE booleans kept; the integration OpenProject digest stays delegated to STACK_HOST. - users lookup docs, token-store write_many, cache README, meta info lint: both sides combined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
6.4 KiB
Python
180 lines
6.4 KiB
Python
"""Mode-aware nginx ``proxy_pass`` directive renderer and the addresses its
|
|
requests reach an application from (compose/swarm), SPOT.
|
|
|
|
Trip-wire: the swarm oauth2 endpoint must forward ``$uri$is_args$args``, not
|
|
``$request_uri`` — during the ``error_page 401 = @oauth2_start`` jump the original
|
|
``$request_uri`` stays ``/`` and oauth2-proxy 403s instead of redirecting.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from utils.networks.address import subnet_gateway
|
|
from utils.roles.applications.config import get
|
|
from utils.roles.entity.name import entity_name
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
_REGEX_OR_NAMED_LOCATION = re.compile(r"^[@~]")
|
|
_LOCATION_MODIFIER = re.compile(r"^(?:=|\^~)\s*")
|
|
_UPSTREAM_VAR = "proxy_pass_upstream"
|
|
REVERSE_PROXY_APPLICATION_ID = "svc-prx-openresty"
|
|
|
|
|
|
def _as_str(value: Any) -> str:
|
|
return "" if value is None else str(value).strip()
|
|
|
|
|
|
def resolve_upstream(
|
|
applications: dict[str, Any],
|
|
application_id: str,
|
|
service_key: str,
|
|
port_kind: str,
|
|
deployment_mode: str,
|
|
local_port: str = "",
|
|
internal_port: str = "",
|
|
host_gateway: bool = False,
|
|
) -> str:
|
|
"""Upstream ``host:port``: compose ``127.0.0.1:<local>``; swarm
|
|
``<entity>:<internal>`` (shared-overlay alias) for the app frontend, or
|
|
``<entity>_<service_key>:<internal>`` for named sidecars (sso-proxy).
|
|
A ``host_gateway`` app runs node-local under swarm and publishes its
|
|
local port to the host, so the overlay-attached proxy reaches it at
|
|
``host.docker.internal:<local>`` via the host gateway (the caller sets
|
|
this off the real DEPLOYMENT_MODE, since such apps force a compose-style
|
|
render). Missing swarm internal port hard-fails."""
|
|
entity = entity_name(application_id)
|
|
if not entity:
|
|
raise ValueError(
|
|
f"resolve_upstream: cannot derive entity from {application_id!r}"
|
|
)
|
|
service_key = _as_str(service_key) or entity
|
|
|
|
def _config_port(scope: str) -> str:
|
|
return _as_str(
|
|
get(
|
|
applications=applications,
|
|
application_id=application_id,
|
|
config_path=f"services.{entity}.ports.{scope}.{port_kind}",
|
|
strict=False,
|
|
default="",
|
|
)
|
|
)
|
|
|
|
if host_gateway or deployment_mode != "swarm":
|
|
port = _as_str(local_port) or _config_port("local")
|
|
if not port:
|
|
raise ValueError(
|
|
f"resolve_upstream: no local port for {application_id!r} "
|
|
f"(services.{entity}.ports.local.{port_kind})"
|
|
)
|
|
if host_gateway:
|
|
return f"host.docker.internal:{port}"
|
|
host = (
|
|
_as_str(
|
|
get(
|
|
applications=applications,
|
|
application_id=application_id,
|
|
config_path=f"services.{entity}.upstream_host",
|
|
strict=False,
|
|
default="",
|
|
)
|
|
)
|
|
or "127.0.0.1"
|
|
)
|
|
return f"{host}:{port}"
|
|
|
|
port = _as_str(internal_port) or _config_port("internal")
|
|
if not port:
|
|
raise ValueError(
|
|
f"resolve_upstream: swarm upstream for {application_id!r} needs "
|
|
f"services.{entity}.ports.internal.{port_kind} (or internal_port=)"
|
|
)
|
|
if service_key == entity:
|
|
return f"{entity}:{port}"
|
|
return f"{entity}_{service_key}:{port}"
|
|
|
|
|
|
def _compose_suffix(tail: str, location: str) -> str:
|
|
if tail == "request":
|
|
loc = location.strip()
|
|
if _REGEX_OR_NAMED_LOCATION.match(loc):
|
|
return ""
|
|
return _LOCATION_MODIFIER.sub("", loc)
|
|
if tail.startswith("/"):
|
|
return tail
|
|
return ""
|
|
|
|
|
|
def _swarm_tail(tail: str) -> str:
|
|
if tail == "oauth2":
|
|
return "$uri$is_args$args"
|
|
if tail.startswith("/"):
|
|
return tail
|
|
return "$request_uri"
|
|
|
|
|
|
def render_proxy_pass(
|
|
authority: str,
|
|
deployment_mode: str,
|
|
tail: str = "request",
|
|
location: str = "/",
|
|
host_gateway: bool = False,
|
|
) -> str:
|
|
"""``proxy_pass`` directive; swarm prepends a ``set`` line (request-time DNS).
|
|
|
|
A ``host_gateway`` upstream resolves through ``host.docker.internal`` in
|
|
``/etc/hosts`` (host gateway), which the request-time ``resolver`` cannot
|
|
see, so it renders as a literal directive like compose."""
|
|
if not authority:
|
|
raise ValueError("render_proxy_pass: authority must be non-empty")
|
|
|
|
if deployment_mode == "swarm" and not host_gateway:
|
|
return (
|
|
f'set ${_UPSTREAM_VAR} "{authority}";\n'
|
|
f" proxy_pass http://${_UPSTREAM_VAR}{_swarm_tail(tail)};"
|
|
)
|
|
return f"proxy_pass http://{authority}{_compose_suffix(tail, location)};"
|
|
|
|
|
|
def proxy_hops(
|
|
application_id: str,
|
|
deployment_mode: str,
|
|
network_roles: Callable[[str], list[str]],
|
|
subnets: Callable[[list[str]], list[str]],
|
|
) -> list[str]:
|
|
"""Addresses the reverse proxy's requests reach ``application_id`` from.
|
|
|
|
Args:
|
|
application_id: the proxied application.
|
|
deployment_mode: ``compose`` or ``swarm``, after any per-role override.
|
|
network_roles: role id to the provider roles whose networks it joins.
|
|
subnets: role ids to their ``networks.local.subnet``, in order.
|
|
|
|
Returns:
|
|
Under compose the gateway of the application's own network and of
|
|
every network it joins: the proxy runs on the host network and reaches
|
|
a published port, which Docker forwards from the gateway of whichever
|
|
of those networks it routes the container through. Under swarm the
|
|
subnets of the overlays the application shares with the proxy, which
|
|
reaches it from its task address there.
|
|
|
|
Raises:
|
|
ValueError: under swarm the application shares no overlay with the
|
|
proxy.
|
|
"""
|
|
if deployment_mode != "swarm":
|
|
roles = list(dict.fromkeys([application_id, *network_roles(application_id)]))
|
|
return list(dict.fromkeys(subnet_gateway(subnet) for subnet in subnets(roles)))
|
|
proxy_networks = network_roles(REVERSE_PROXY_APPLICATION_ID)
|
|
shared = [role for role in network_roles(application_id) if role in proxy_networks]
|
|
if not shared:
|
|
raise ValueError(
|
|
f"{application_id} shares no overlay with "
|
|
f"{REVERSE_PROXY_APPLICATION_ID}, so no address carries its proxied requests"
|
|
)
|
|
return list(dict.fromkeys(subnets(shared)))
|