berth init
This commit is contained in:
94
berth/ctrl/lib/config.sh
Normal file
94
berth/ctrl/lib/config.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
# Shared config loading. Sourced, never executed. Run from ctrl/.
|
||||
#
|
||||
# Precedence, weakest first:
|
||||
# ctrl/versions.env pinned toolchain (committed)
|
||||
# ctrl/env.d/<target>.env provider shape: aws|gcp (committed)
|
||||
# ctrl/.env machine-local + secrets (gitignored)
|
||||
# the caller's env `make estate plan TARGET=gcp` (always wins)
|
||||
|
||||
CONFIG_OVERRIDABLE="TARGET ESTATE CLOUD REGION INSTANCE_TYPE
|
||||
HOST HOST_ADMIN AWS_PROFILE AWS_HOSTED_ZONE_ID
|
||||
GCP_PROJECT GCP_ZONE TOFU_WORKSPACE"
|
||||
|
||||
# rig's port formula, reproduced rather than imported. cksum because it is
|
||||
# POSIX and gives the same value on every machine. Used to verify, not allocate.
|
||||
derive_port_base() {
|
||||
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
||||
echo $((20000 + (h % 200) * 10))
|
||||
}
|
||||
|
||||
_config_restore() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
eval "export $line"
|
||||
fi
|
||||
done <<< "$1"
|
||||
# A while loop returns its last body command's status; the trailing empty
|
||||
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
||||
return 0
|
||||
}
|
||||
|
||||
load_config() {
|
||||
local k saved=""
|
||||
for k in $CONFIG_OVERRIDABLE; do
|
||||
# ${!k+x} distinguishes "set but empty" from "unset" — an explicit
|
||||
# FOO= on the command line is a real choice and must survive.
|
||||
if [ -n "${!k+x}" ]; then
|
||||
saved+="$k=$(printf '%q' "${!k}")"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
set -a
|
||||
source ./versions.env
|
||||
[ -f ./.env ] && source ./.env
|
||||
set +a
|
||||
|
||||
# Re-apply overrides now so TARGET is the caller's before we pick the file.
|
||||
_config_restore "$saved"
|
||||
|
||||
local target="${TARGET:-aws}"
|
||||
if [ ! -f "./env.d/${target}.env" ]; then
|
||||
echo "no such target: env.d/${target}.env" >&2
|
||||
echo "available: $(ls env.d/*.env 2>/dev/null | xargs -n1 basename | sed 's/\.env$//' | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
source "./env.d/${target}.env"
|
||||
[ -f ./.env ] && source ./.env
|
||||
set +a
|
||||
|
||||
_config_restore "$saved"
|
||||
|
||||
TARGET="$target"
|
||||
|
||||
# Identity is explicit: berth never guesses which estate it is acting on.
|
||||
# The one convenience: a single estate/*.json is used without being asked.
|
||||
if [ -z "${ESTATE:-}" ]; then
|
||||
local n; n=$(ls ../estate/*.json 2>/dev/null | wc -l)
|
||||
if [ "$n" = "1" ]; then
|
||||
ESTATE=$(basename "$(ls ../estate/*.json)" .json)
|
||||
else
|
||||
echo "ESTATE is not set and estate/ holds $n candidates — refusing to guess." >&2
|
||||
echo "available: $(ls ../estate/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | tr '\n' ' ')" >&2
|
||||
echo "set it: make estate show ESTATE=<name>, or ESTATE= in ctrl/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ESTATE_FILE="../estate/${ESTATE}.json"
|
||||
if [ ! -f "$ESTATE_FILE" ]; then
|
||||
echo "no such estate: estate/${ESTATE}.json" >&2
|
||||
echo "available: $(ls ../estate/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Facts come from the estate file, never restated in a target env.
|
||||
DOMAIN=$(estate_get "domain")
|
||||
HOST="${HOST:-$(estate_get "host")}"
|
||||
HOST_ADMIN="${HOST_ADMIN:-$(estate_get "host_admin")}"
|
||||
|
||||
# Workspace == target, so the two can never mean different things.
|
||||
TOFU_WORKSPACE="${TOFU_WORKSPACE:-$TARGET}"
|
||||
}
|
||||
125
berth/ctrl/lib/estate.sh
Normal file
125
berth/ctrl/lib/estate.sh
Normal file
@@ -0,0 +1,125 @@
|
||||
# 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"
|
||||
}
|
||||
Reference in New Issue
Block a user