Files
soleprint/berth/ctrl/lib/estate.sh
2026-09-14 07:28:50 -03:00

148 lines
4.7 KiB
Bash

# Reading and projecting estate/<name>.json. Sourced, never executed.
#
# python3 rather than jq: berth's floor already includes python3, so it is a
# dependency berth has rather than one it adds.
estate_get() {
python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
for k in sys.argv[2].split("."):
if isinstance(d, list):
try: k = int(k)
except ValueError: sys.exit(0)
try: d = d[k]
except Exception: sys.exit(0)
print("" if d is None else d if isinstance(d, str) else json.dumps(d))
' "$ESTATE_FILE" "$1"
}
# Services in scope for one target. A service names its targets; absent = all.
# Fields are US-separated (0x1f), not tab: tab is IFS whitespace, so bash
# collapses a run of them and an empty field would shift every later column.
estate_services() {
local target="${1:-$TARGET}"
python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
target = sys.argv[2]
for s in d.get("services", []):
tg = s.get("targets")
if tg is not None and target not in tg:
continue
print("\x1f".join([
s.get("name", ""),
s.get("host", ""),
str(s.get(target + "_upstream", s.get("upstream", "")) or ""),
s.get("kind", "proxy"),
"raw" if s.get("raw") else "",
s.get("placement", "box"),
s.get("peer", ""),
str(s.get("port", "") or ""),
s.get("local_host", s.get("host", "")),
]))
' "$ESTATE_FILE" "$target"
}
# The SAN list the services need, derived — never a literal list.
estate_sans() {
python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
domain = d["domain"]
sans = [domain]
depths = set()
for s in d.get("services", []):
h = s.get("host", "")
if not h:
continue
# A wildcard matches exactly ONE label. "git" needs *.domain; "dlt.spr"
# needs *.spr.domain. The parent of the leaf is what has to be covered.
parent = h.split(".", 1)[1] if "." in h else ""
depths.add(parent)
for p in sorted(depths):
sans.append("*." + (p + "." if p else "") + domain)
for s in sans:
print(s)
' "$ESTATE_FILE"
}
# Is <fqdn> covered by <san>? A wildcard matches exactly one label.
san_covers() {
local fqdn="$1" san="$2"
[ "$fqdn" = "$san" ] && return 0
case "$san" in
\*.*)
local suffix="${san#\*.}"
# Must end in .suffix AND have exactly one extra label.
case "$fqdn" in
*".$suffix") [ "${fqdn%".$suffix"}" = "${fqdn%%.*}" ] && return 0 ;;
esac
;;
esac
return 1
}
# ── the overlay ────────────────────────────────────────────────────────────
# Every overlay name, one per line.
overlay_names() {
python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
for n in d.get("vpn", {}).get("overlays", {}):
print(n)
' "$ESTATE_FILE"
}
# Peers of one overlay, US-separated:
# name, address, role, endpoint, public_key, allowed_ips, keepalive
overlay_peers() {
python3 -c '
import json, sys
d = json.load(open(sys.argv[1]))
ov = d.get("vpn", {}).get("overlays", {}).get(sys.argv[2], {})
for name, p in ov.get("peers", {}).items():
print("\x1f".join(str(x) if x is not None else "" for x in [
name, p.get("address"), p.get("role"), p.get("endpoint"),
p.get("public_key"), p.get("allowed_ips"), p.get("keepalive"),
]))
' "$ESTATE_FILE" "$1"
}
overlay_get() { estate_get "vpn.overlays.$1.$2"; }
# Is an address inside a CIDR? Pure python so there is no ipcalc dependency —
# berth's floor already includes python3 because the IaC side needs it.
addr_in_subnet() {
python3 -c '
import ipaddress, sys
try:
sys.exit(0 if ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2], strict=False) else 1)
except ValueError:
sys.exit(2)
' "$1" "$2"
}
# The upstream a service actually resolves to, as "host:port".
#
# A PLACED service has no literal `upstream` field: ✖ B9 replaced langfuse's
# hand-written `10.8.0.2:3000` with placement+peer+port, because being reached
# by address on the overlay is ONE decision, not three properties. Everything
# that asks "what does this service point at" must therefore resolve it the
# same way, or it silently sees an empty string and skips the service — which
# is exactly how vpn.sh's bindings invariant went quiet after B9 landed.
#
# usage: service_upstream <up> <placement> <peer> <port>
service_upstream() {
local up="$1" placement="$2" peer="$3" port="$4"
case "$placement" in
local|instance)
local addr; addr="$(overlay_get estate "peers.${peer}.address")"
[ -z "$addr" ] && return 1
printf '%s:%s' "$addr" "$port"
;;
*) printf '%s' "$up" ;;
esac
}