148 lines
4.7 KiB
Bash
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
|
|
}
|