184 lines
7.2 KiB
Bash
184 lines
7.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Gateway routes, projected from the estate onto one target.
|
|
#
|
|
# Usage:
|
|
# ./services.sh # list
|
|
# ./services.sh render aws # -> render/out/aws/*.conf (nginx vhosts)
|
|
# ./services.sh render local # -> render/out/local/Caddyfile
|
|
# ./services.sh deploy # refuses; ppl/ctrl/deploy.sh ships config
|
|
#
|
|
# Each target is a projection with its own rules, not a format conversion.
|
|
# The nine axes they disagree on, and the install order: ../README.md
|
|
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
source ./lib/config.sh
|
|
source ./lib/estate.sh
|
|
load_config
|
|
|
|
OUT_ROOT="./render/out"
|
|
|
|
list() {
|
|
printf '%-12s %-16s %-22s %-9s %s\n' NAME FQDN UPSTREAM PLACEMENT SOURCE
|
|
local name host up kind raw
|
|
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
[ -z "$name" ] && continue
|
|
# A placed service has no literal `upstream` — it is derived from the
|
|
# peer's overlay address, so show what it actually resolves to.
|
|
local shown; shown="$(service_upstream "$up" "$placement" "$peer" "$port")" || shown=""
|
|
printf '%-12s %-16s %-22s %-9s %s\n' \
|
|
"$name" "${host}.${DOMAIN}" "${shown:--}" "$placement" \
|
|
"$([ -n "$raw" ] && echo 'hand-written' || echo 'generated')"
|
|
done < <(estate_services "$TARGET")
|
|
echo
|
|
echo "hand-written entries are NOT generated and NOT overwritten."
|
|
echo "run 'make estate show' to see why each one is an exception."
|
|
}
|
|
|
|
render_cloud() {
|
|
# Two statements: `local a="$1" b="$a"` expands all arguments before any
|
|
# assignment, so $a would still be unset.
|
|
local target="$1"
|
|
local out="$OUT_ROOT/$target"
|
|
rm -rf "$out"; mkdir -p "$out"
|
|
local name host up kind raw uhost uport n=0 skipped=0
|
|
|
|
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
[ -z "$name" ] && continue
|
|
if [ -n "$raw" ]; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
# Placement picks the rendering. A container on the estate's own network
|
|
# is reached by NAME through docker's resolver; anything on the overlay
|
|
# is reached by ADDRESS and needs no DNS. That is one decision, not the
|
|
# three properties (upstream{}, no resolver, no set $var) it produces.
|
|
local tmpl
|
|
case "$placement" in
|
|
local|instance)
|
|
tmpl=./render/nginx-upstream.tmpl
|
|
uhost="$(overlay_get estate "peers.${peer}.address")"
|
|
uport="$port" # resolved via service_upstream's same rule
|
|
if [ -z "$uhost" ]; then
|
|
echo " ! $name: placement '$placement' names peer '$peer', which has no address" >&2
|
|
continue
|
|
fi
|
|
;;
|
|
hosted)
|
|
echo " ! $name: placement 'hosted' is declared but not rendered yet" >&2
|
|
continue
|
|
;;
|
|
*)
|
|
if [ "$kind" = "static" ]; then
|
|
tmpl=./render/nginx-static.tmpl
|
|
uhost=""; uport=""
|
|
else
|
|
tmpl=./render/nginx-proxy.tmpl
|
|
uhost="${up%%:*}"; uport="${up##*:}"
|
|
fi
|
|
;;
|
|
esac
|
|
sed -e "s|\${NAME}|${name}|g" \
|
|
-e "s|\${ESTATE}|${ESTATE}|g" \
|
|
-e "s|\${FQDN}|${host}.${DOMAIN}|g" \
|
|
-e "s|\${DOMAIN}|${DOMAIN}|g" \
|
|
-e "s|\${PLACEMENT}|${placement}|g" \
|
|
-e "s|\${PEER}|${peer}|g" \
|
|
-e "s|\${UPSTREAM_HOST}|${uhost}|g" \
|
|
-e "s|\${UPSTREAM_PORT}|${uport}|g" \
|
|
"$tmpl" > "$out/${name}.conf"
|
|
n=$((n + 1))
|
|
done < <(estate_services "$target")
|
|
|
|
echo "wrote $n vhost(s) to $out/ ($skipped hand-written, left alone)"
|
|
cat <<EONOTE
|
|
|
|
TO INSTALL THESE, THREE THINGS MUST HAPPEN IN THIS ORDER — and the order is the
|
|
whole reason this is not a one-liner:
|
|
|
|
1. These land in conf.d/generated/, NOT conf.d/. ppl/ctrl/deploy.sh rsyncs the
|
|
gateway with --delete; generated and hand-written config sharing one
|
|
directory means one of them gets erased.
|
|
|
|
2. nginx.conf needs a THIRD include line. Its conf.d/*.conf glob does not
|
|
recurse — which is exactly why conf.d/soleprint/*.conf already needs its
|
|
own line at nginx.conf:28-30.
|
|
|
|
3. That new include changes LOAD ORDER, and load order decides which :443
|
|
block catches unmatched names. So default.conf's commented-out
|
|
':443 default_server' must be restored FIRST. 'make check' fails on this
|
|
today, deliberately — it is a gate, not a warning.
|
|
EONOTE
|
|
}
|
|
|
|
render_local() {
|
|
local out="$OUT_ROOT/local"; mkdir -p "$out"
|
|
local ld; ld=$(estate_get "local_domain")
|
|
: "${ld:=local.ar}"
|
|
local name host up kind raw port drift=0
|
|
|
|
{
|
|
cat <<EOH
|
|
# GENERATED by berth from estate/${ESTATE}.json. Do not edit.
|
|
# Regenerate: make services render local
|
|
#
|
|
# Install: sudo ln -sf \$PWD/Caddyfile /etc/caddy/Caddyfile && sudo systemctl reload caddy
|
|
# All *.${ld} resolve to 127.0.0.1 via dnsmasq.
|
|
#
|
|
# Every site address carries an explicit :80. Without it Caddy 2 defaults to
|
|
# :443 with auto-HTTPS, which on *.${ld} means cert provisioning attempts that
|
|
# fail and break the listener. Plain HTTP only on this host.
|
|
#
|
|
# Caddy matches the MOST SPECIFIC site address, not the first — the opposite of
|
|
# nginx, which matches exactly and otherwise falls to default_server. A name set
|
|
# that is unambiguous here can be ambiguous on the box.
|
|
EOH
|
|
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
[ -z "$name" ] && continue
|
|
port=$(python3 -c '
|
|
import json,sys
|
|
d=json.load(open(sys.argv[1]))
|
|
for s in d.get("services",[]):
|
|
if s.get("name")==sys.argv[2]:
|
|
print(s.get("local_port") or ""); break
|
|
' "$ESTATE_FILE" "$name")
|
|
[ -z "$port" ] && continue
|
|
echo
|
|
echo "${lhost}.${ld}:80, *.${lhost}.${ld}:80 {"
|
|
echo " reverse_proxy localhost:${port}"
|
|
echo "}"
|
|
done < <(estate_services local)
|
|
} > "$out/Caddyfile"
|
|
|
|
echo "wrote $out/Caddyfile"
|
|
echo
|
|
echo "rig is not consulted and does not know this exists — its handover"
|
|
echo "scrub refuses the string '${ld}'. Where a port belongs to a rig,"
|
|
echo "'make ports verify' RECOMPUTES rig's formula to check it rather than"
|
|
echo "importing rig's code. Convention, verified; not a dependency."
|
|
}
|
|
|
|
case "${1:-list}" in
|
|
list) list ;;
|
|
render)
|
|
shift
|
|
# `case "${1:-X}"` defaults the match but leaves $1 empty.
|
|
t="${1:-$TARGET}"
|
|
case "$t" in
|
|
local) render_local ;;
|
|
aws|gcp) render_cloud "$t" ;;
|
|
*) echo "usage: $0 render [aws|gcp|local]" >&2; exit 1 ;;
|
|
esac
|
|
;;
|
|
deploy)
|
|
echo "berth does not ship config; ppl/ctrl/deploy.sh does." >&2
|
|
echo " berth's half is the DESCRIPTION and the render. Shipping is" >&2
|
|
echo " rsync + compose against a live box, and it belongs where the" >&2
|
|
echo " credentials are: berth is the tool, ppl is the estate that" >&2
|
|
echo " holds the secrets." >&2
|
|
exit 1
|
|
;;
|
|
*) echo "usage: $0 [list|render [aws|gcp|local]|deploy]" >&2; exit 1 ;;
|
|
esac
|