furtka-gateway/control_plane/traefikconf.py

73 lines
2.2 KiB
Python
Raw Normal View History

"""Write/remove per-route Traefik dynamic-config files.
Mirrors furtka/furtka/https.py's snippet-write pattern: write to a temp
file, then atomically rename over the target, so Traefik's file provider
never observes a half-written route. Unlike https.py's Caddy target, no
reload call is needed here Traefik's file provider hot-reloads on change.
Config is built with plain string formatting rather than a YAML library,
to keep the control-plane's dependency footprint at zero (stdlib only, the
same choice the rest of this repo makes) the shape here is small and
fixed enough that hand-formatting is simpler than it sounds.
"""
from __future__ import annotations
import os
from pathlib import Path
from control_plane import paths
def _route_file(route_id: str) -> Path:
return paths.dynamic_dir() / f"route-{route_id}.yml"
def write_route(
route_id: str,
subdomain: str,
target_ip: str,
target_port: int,
cert_resolver: str | None,
) -> None:
"""(Re)write the dynamic-config file that makes `subdomain` proxy to
`target_ip:target_port`. Idempotent safe to call for an unchanged
route, which is what reconciler.py relies on.
"""
if cert_resolver:
tls_block = f" tls:\n certResolver: {cert_resolver}\n"
else:
tls_block = " tls: {}\n"
content = (
"http:\n"
" routers:\n"
f" route-{route_id}:\n"
f' rule: "Host(`{subdomain}`)"\n'
" entryPoints: [websecure]\n"
f" service: svc-{route_id}\n"
f"{tls_block}"
" services:\n"
f" svc-{route_id}:\n"
" loadBalancer:\n"
" servers:\n"
f' - url: "http://{target_ip}:{target_port}"\n'
)
target = _route_file(route_id)
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(".tmp")
tmp.write_text(content)
os.replace(tmp, target)
def remove_route(route_id: str) -> None:
_route_file(route_id).unlink(missing_ok=True)
def existing_route_ids() -> set[str]:
return {
f.name.removeprefix("route-").removesuffix(".yml")
for f in paths.dynamic_dir().glob("route-*.yml")
}