big update based on real wokr

This commit is contained in:
2026-09-23 18:52:57 -03:00
parent 45018e4743
commit 8fad2223f3
23 changed files with 652 additions and 670 deletions

View File

@@ -3,6 +3,7 @@
# The pool is derived from the kind Docker network at install time.
# Notes: docs/notes/addons.md
set -euo pipefail
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
cd "${RIG_CTRL:-$(dirname "$0")/..}"
source ./lib/config.sh
@@ -61,27 +62,7 @@ $K rollout status daemonset/speaker -n metallb-system --timeout=240s
# on a race that resolves itself in seconds.
echo " configuring the address pool"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if $K apply -f - >/dev/null 2>&1 <<YAML
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- ${pool_start}-${pool_end}
---
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
# is what makes this work on a laptop.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default
YAML
if render_manifest "$ADDON_DIR/metallb.yaml" | $K apply -f - >/dev/null 2>&1
then
echo " pool ready: ${pool_start}-${pool_end}"
exit 0

View File

@@ -0,0 +1,19 @@
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- ${pool_start}-${pool_end}
---
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
# is what makes this work on a laptop.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Readiness check: is this machine ready to run rig? Reports and instructs; never fixes.
# Usage: check.sh [all | mem [status|push|all|backup|restore]] (all = every detail)
# Prints the memory budget and what to do next first, then the configuration as it is.
# Notes: docs/notes/check.md
set -euo pipefail
cd "$(dirname "$0")"
@@ -12,13 +13,11 @@ if [ "${1:-}" = mem ]; then
exec bash ./mem.sh "${@:-status}"
fi
# Compact by default: facts only with `all`; problems (!) always print.
# Facts only with `all`; problems (!) always print.
VERBOSE=""
if [ "${1:-}" = all ]; then VERBOSE=1; fi
fact() { if [ -n "$VERBOSE" ]; then echo "$@"; fi; }
bash ./deps.sh detect ${VERBOSE:+all}
source ./lib/config.sh
load_config
@@ -66,49 +65,64 @@ port_busy() {
grep -qi "^ *[0-9]*: [0-9A-F]*$hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null
}
echo
echo "rig"
echo " cluster ${CLUSTER} (${KUBECONTEXT}) profile ${PROFILE_NAME}, ${NODES} node(s), registry ${REGISTRY_MODE}"
if [ -n "${OVERLAY:-}" ]; then
echo " overlay $(basename "$(_abs_from_ctrl "$OVERLAY_DIR")") ($(_abs_from_ctrl "$OVERLAY_DIR"))"
elif [ -n "$OVERLAY_DIR" ]; then
fact " overlay none named — rig's own ${OVERLAY_DIR}"
fi
if [ -n "$VERBOSE" ] && [ -n "$OVERLAY_DIR" ]; then
ov=$(_from_ctrl "$OVERLAY_DIR") pieces=""
for piece in rig.env k8s/overlays/dev kind-config.yaml.tpl addons Tiltfile; do
[ -e "$ov/$piece" ] && pieces+="$piece "
done
echo " provides: ${pieces:-nothing rig reads}"
fi
fact " manifests ${MANIFESTS_DIR:-none}"
fact " kind config ${KIND_CONFIG}"
fact " ingress ${INGRESS_MODE}"
if [ ! -f ./.env ]; then
fact " .env none — built-in defaults (cp ctrl/.env.example ctrl/.env to set values)"
fi
if [ -n "${STALE_MANIFESTS_DIR:-}" ]; then
echo " ! .env MANIFESTS_DIR=${STALE_MANIFESTS_DIR} is the old default; rig's examples moved"
echo " to examples/ — delete that line from ctrl/.env (ignored until then)"
fi
# registry.sh points containerd at certs.d, which only works if the kind config says so,
# and a kind config is fixed at creation: a project's own file that drops it fails silently.
if [ "$REGISTRY_MODE" != none ] && ! grep -q 'config_path *= *"/etc/containerd/certs.d"' "$KIND_CONFIG"; then
echo " ! kind ${KIND_CONFIG} lacks the containerd config_path patch that registry mode"
echo " '${REGISTRY_MODE}' needs — copy it from ctrl/k8s/kind-config.yaml.tpl"
# Who listens on a port, as "pid name", when ss can tell (only for our own processes).
port_owner() {
command -v ss >/dev/null 2>&1 || return 0
ss -ltnpH "sport = :$1" 2>/dev/null \
| sed -n 's/.*users:(("\([^"]*\)",pid=\([0-9]*\).*/\2 \1/p' | head -1
}
# This environment's Tilt: a tilt started from this ctrl/ (the Makefile runs it there).
tilt_pid=""
if tilt_owner=$(port_owner "$TILT_PORT") && [ -n "$tilt_owner" ]; then
pid=${tilt_owner%% *}
if [ "${tilt_owner#* }" = tilt ] && [ "$(readlink "/proc/$pid/cwd" 2>/dev/null)" = "$(pwd -P)" ]; then
tilt_pid=$pid
fi
fi
# ── memory: does this cluster fit right now? Warns; never blocks. ──────────
# none | stopped | up | unknown (no docker)
cluster_state() {
docker info >/dev/null 2>&1 || { echo unknown; return; }
local f="label=io.x-k8s.kind.cluster=${CLUSTER}"
if [ -n "$(docker ps -q --filter "$f")" ]; then echo up
elif [ -n "$(docker ps -aq --filter "$f")" ]; then echo stopped
else echo none; fi
}
state=$(cluster_state)
# ── memory budget: what this environment needs, against what the machine has ──
# The workload's share comes from the overlay: MEMORY_BUDGET="name=MB ..." in its rig.env,
# and MEMORY_BUDGET_TIP, optionally, says how to switch its largest part off.
# rig only knows what a node costs. Warns; never blocks.
total_mb=$(mb_of MemTotal)
avail_mb=$(mb_of MemAvailable)
swap_used_mb=$(( $(mb_of SwapTotal) - $(mb_of SwapFree) ))
overcommit=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
need_mb=$(( NODES * NODE_MB ))
node_mb=$(( NODES * NODE_MB ))
budget="" work_mb=0 big_mb=0 big=""
for item in ${MEMORY_BUDGET:-}; do
name=${item%=*} mb=${item##*=}
if [ "$name" = "$item" ] || ! [[ "$mb" =~ ^[0-9]+$ ]]; then
echo " ! budget '$item' in MEMORY_BUDGET is not name=MB"
continue
fi
[ "$mb" -gt 0 ] || continue
budget+=$(printf ' %-18s %6d MB' "$name" "$mb")$'\n'
work_mb=$(( work_mb + mb ))
if [ "$mb" -gt "$big_mb" ]; then big_mb=$mb big=$name; fi
done
need_mb=$(( work_mb + node_mb ))
rows=$(container_mb)
# If our cluster is already up, its memory is already out of MemAvailable: need nothing more.
# If our cluster is already up, its memory is already out of MemAvailable.
ours_mb=$(awk -F'\t' -v c="$CLUSTER" '$2 == c { s += $1 } END { print s + 0 }' <<< "$rows")
still_mb=$(( ours_mb > 0 ? 0 : need_mb ))
if [ "$state" = up ]; then
still_mb=$(( need_mb > ours_mb ? need_mb - ours_mb : 0 ))
else
still_mb=$need_mb
fi
headroom=$(( avail_mb - still_mb ))
# The biggest things holding memory, other than this cluster: kind clusters summed, the rest by name.
@@ -117,82 +131,164 @@ others=$(awk -F'\t' -v c="$CLUSTER" '
$2 == "-" { k["container \x27" $3 "\x27"] += $1 }
END { for (n in k) printf "%d\t%s\n", k[n], n }' <<< "$rows" | sort -rn)
if [ "$still_mb" -eq 0 ] && [ "$headroom" -ge 512 ]; then
printf " memory up, holding %d MB — %d MB headroom for what you deploy\n" "$ours_mb" "$headroom"
elif [ "$still_mb" -eq 0 ]; then
printf " ! memory up, but only %d MB headroom for anything you deploy\n" "$headroom"
elif [ "$headroom" -ge 512 ]; then
printf " memory fits — ~%d MB for %s node(s), %d MB headroom\n" "$need_mb" "$NODES" "$headroom"
echo
echo "memory budget (${CLUSTER})"
printf '%s' "$budget"
if [ -z "$budget" ]; then
echo " workload ? (the overlay lists none: MEMORY_BUDGET in its rig.env)"
fi
printf ' %-18s %6d MB (kind control plane, kube-system; %s node(s))\n' "node overhead" "$node_mb" "$NODES"
echo " ---------"
printf ' %-18s %6d MB\n' "required" "$need_mb"
if [ "$state" = up ]; then
printf ' %-18s %6d MB (%s is up)\n' "held by it now" "$ours_mb" "$CLUSTER"
fi
printf ' %-18s %6d MB\n' "this machine has" "$total_mb"
printf ' %-18s %6d MB (right now)\n' "available" "$avail_mb"
echo
if [ "$headroom" -ge 512 ]; then
if [ "$state" = up ]; then
echo "fits: ${CLUSTER} holds ${ours_mb} MB of its ${need_mb} MB; ${avail_mb} MB still available — ${headroom} MB headroom."
else
echo "fits: ${need_mb} MB needed, ${avail_mb} MB available — ${headroom} MB headroom."
fi
elif [ "$headroom" -ge 0 ]; then
printf " ! memory fits, but only %d MB headroom (~%d MB for %s node(s))\n" "$headroom" "$need_mb" "$NODES"
echo "! tight: ${still_mb} MB still needed, ${avail_mb} MB available — only ${headroom} MB headroom."
else
printf " ! memory does not fit: ~%d MB needed, %d MB available\n" "$still_mb" "$avail_mb"
echo "! does not fit: ${still_mb} MB still needed, ${avail_mb} MB available."
# Two failures with opposite fixes, and telling them apart is the point.
if [ "$still_mb" -le "$total_mb" ]; then
echo " something else holds it (below) — stopping that helps, a bigger VM would not."
if [ "$need_mb" -le "$total_mb" ]; then
echo " Something else holds the memory (below): stopping it helps, a bigger machine would not."
if grep -q 'kind cluster' <<< "$others"; then
echo " 'make cluster free' stops the other kind clusters. It stops, never deletes."
echo " 'make cluster free' stops the other kind clusters. It stops, never deletes."
fi
else
echo " the machine itself is too small: ${total_mb} MB total."
echo " The machine itself is too small: ${total_mb} MB in all. Give it more memory"
echo " ('make check mem' has the WSL steps), or switch parts of the workload off."
fi
fi
if [ -n "$others" ] && { [ -n "$VERBOSE" ] || [ "$headroom" -lt 512 ]; }; then
echo " held elsewhere:"
head -6 <<< "$others" | awk -F'\t' '{ printf " %6d MB %s\n", $1, $2 }'
echo " held elsewhere:"
head -6 <<< "$others" | awk -F'\t' '{ printf " %6d MB %s\n", $1, $2 }'
n_others=$(wc -l <<< "$others")
if [ "$n_others" -gt 6 ]; then
echo " ... and $((n_others - 6)) more"
echo " ... and $((n_others - 6)) more"
fi
fi
# One part that outweighs the rest is the first thing to switch off on a smaller machine.
if [ "$work_mb" -gt 0 ] && [ $(( big_mb * 2 )) -gt "$work_mb" ] && [ "$big_mb" -ge 1024 ]; then
echo
echo "${big} is ${big_mb} of the ${work_mb} MB the workload needs — more than the rest"
echo "combined. If you are not working on it, ${MEMORY_BUDGET_TIP:-switching it off in the overlay} gets that back."
fi
if [ "$swap_used_mb" -gt 0 ]; then
fact " ${swap_used_mb} MB already in swap, which 'available' does not count: expect slow before failing"
fact " ${swap_used_mb} MB already in swap, which 'available' does not count: expect slow before failing"
fi
if [ "$overcommit" = "1" ]; then
fact " overcommit=1: allocations never fail, so read 'fits' as a ceiling (OOM killer settles up)"
fact " overcommit=1: allocations never fail, so read 'fits' as a ceiling (OOM killer settles up)"
fi
# ── where things stand, and the next step ──────────────────────────────────
echo
case "$state" in
none)
echo "${CLUSTER} does not exist yet. 'make cluster up' creates it${ADDONS:+ (with ${ADDONS})},"
echo "then 'make tilt' starts the dev loop." ;;
stopped)
echo "${CLUSTER} exists but is stopped (nothing is lost). Start it again with:"
echo " docker start \$(docker ps -aq -f label=io.x-k8s.kind.cluster=${CLUSTER})" ;;
up)
if [ -n "$tilt_pid" ]; then
echo "${CLUSTER} is up, and its Tilt is running (pid ${tilt_pid}): http://localhost:${TILT_PORT}"
else
echo "${CLUSTER} is up. 'make tilt' starts the dev loop, at http://localhost:${TILT_PORT}."
fi
echo "Through the gateway: http://localhost:${HTTP_PORT}" ;;
*)
echo "docker is not reachable, so the cluster's state is unknown (the machine block below says why)." ;;
esac
# ── the configuration, as it is ────────────────────────────────────────────
echo
bash ./deps.sh detect ${VERBOSE:+all}
echo
echo "config"
echo " cluster ${CLUSTER} ($([ "$state" = none ] && echo 'not created' || echo "$state"))"
echo " context ${KUBECONTEXT}"
if [ -n "${OVERLAY:-}" ]; then
echo " overlay $(_abs_from_ctrl "$OVERLAY_DIR")"
elif [ -n "$OVERLAY_DIR" ]; then
echo " overlay none named — rig's own ${OVERLAY_DIR}"
fi
if [ -n "$VERBOSE" ] && [ -n "$OVERLAY_DIR" ]; then
ov=$(_from_ctrl "$OVERLAY_DIR") pieces=""
for piece in rig.env k8s/overlays/dev kind-config.yaml.tpl addons Tiltfile; do
[ -e "$ov/$piece" ] && pieces+="$piece "
done
echo " provides: ${pieces:-nothing rig reads}"
fi
echo " profile ${PROFILE_NAME}"
echo " nodes ${NODES}"
echo " kind config ${KIND_CONFIG}"
echo " manifests ${MANIFESTS_DIR:-none}"
echo " ingress ${INGRESS_MODE}"
if [ ! -f ./.env ]; then
fact " .env none — built-in defaults (cp ctrl/.env.example ctrl/.env to set values)"
fi
if [ -n "${STALE_MANIFESTS_DIR:-}" ]; then
echo " ! .env MANIFESTS_DIR=${STALE_MANIFESTS_DIR} is the old default; rig's examples moved"
echo " to examples/ — delete that line from ctrl/.env (ignored until then)"
fi
# registry.sh points containerd at certs.d, which only works if the kind config says so,
# and a kind config is fixed at creation: a project's own file that drops it fails silently.
if [ "$REGISTRY_MODE" != none ] && ! grep -q 'config_path *= *"/etc/containerd/certs.d"' "$KIND_CONFIG"; then
echo " ! kind ${KIND_CONFIG} lacks the containerd config_path patch that registry mode"
echo " '${REGISTRY_MODE}' needs — copy it from ctrl/k8s/kind-config.yaml.tpl"
fi
# ── ports: checked before creation; docker reports a clash only halfway through. ──
# Ports held by our own cluster are not clashes. Second grep, not `tr -d ':->'` (a tr range).
ours=$(docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" \
--format '{{.Ports}}' 2>/dev/null | tr ',' '\n' \
| grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
clash=0 list="" mine=0
for entry in "HTTP:${HTTP_PORT}" "HTTPS:${HTTPS_PORT}" \
"TILT:${TILT_PORT}" "REGISTRY:${REGISTRY_PORT}"; do
# Ports held by our own cluster are not clashes: its node containers, and its registry,
# which is a container of its own. Second grep, not `tr -d ':->'` (a tr range).
ours=$({ docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" --format '{{.Ports}}'
docker ps --filter "name=^${CLUSTER}-registry\$" --format '{{.Ports}}'; } 2>/dev/null \
| tr ',' '\n' | grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
clash=0 list=""
for entry in "http:${HTTP_PORT}" "https:${HTTPS_PORT}" \
"tilt:${TILT_PORT}" "registry:${REGISTRY_PORT}"; do
name="${entry%%:*}"; p="${entry#*:}"
[ -n "$p" ] || continue
list+="$p "
list+="$name $p "
if ! port_busy "$p"; then
fact "$(printf " %-9s %-6s free" "$name" "$p")"
elif echo "$ours" | grep -qx "$p"; then
mine=1
fact "$(printf " %-9s %-6s in use by this environment's cluster" "$name" "$p")"
elif [ "$name" = tilt ] && [ -n "$tilt_pid" ]; then
fact "$(printf " %-9s %-6s in use by this environment's Tilt" "$name" "$p")"
else
printf " ! ports %s %s IN USE by something else\n" "$name" "$p"
clash=1
owner=$(port_owner "$p")
if [ "$name" = tilt ] && [ "${owner#* }" = tilt ]; then
printf " ! ports tilt %s is held by another Tilt (pid %s, in %s)\n" "$p" "${owner%% *}" \
"$(readlink "/proc/${owner%% *}/cwd" 2>/dev/null || echo '?')"
echo " stop it (kill ${owner%% *}) before 'make tilt'"
else
printf " ! ports %s %s IN USE by %s\n" "$name" "$p" "${owner:+pid ${owner}}${owner:-something else}"
clash=1
fi
fi
done
echo " ports ${list% }"
if [ "$clash" -eq 1 ]; then
echo " override it in ctrl/.env (e.g. HTTP_PORT=21080), or rename this directory"
elif [ "$mine" -eq 1 ]; then
echo " ports ${list% } held by this cluster"
else
echo " ports ${list% } free"
fi
if [ -n "${OVERLAY:-}" ]; then
fact " derived from the overlay's folder name"
else
fact " derived from the directory name; pin them: bash ctrl/ports.sh persist"
fi
# ── what `make cluster up` wires in beside the cluster ─────────────────────
REG_NAME="${CLUSTER}-registry"
if state=$(docker inspect -f '{{.State.Status}}' "$REG_NAME" 2>/dev/null); then
echo " registry localhost:${REGISTRY_PORT} ($state)"
if reg_state=$(docker inspect -f '{{.State.Status}}' "$REG_NAME" 2>/dev/null); then
echo " registry ${REGISTRY_MODE} localhost:${REGISTRY_PORT} (${reg_state})"
else
fact " registry no container yet — 'make cluster up' starts it"
echo " registry ${REGISTRY_MODE} (no container yet)"
fi
echo " addons ${ADDONS:-none}"
if [ -n "$VERBOSE" ]; then

View File

@@ -16,12 +16,30 @@ serve() {
echo "no docs/index.html" >&2
exit 1
fi
# One worker and no access log: a few static pages need no more, and the
# terminal then shows only real errors. The config is a temp file, gone on exit.
# Global, not local: the trap runs after serve() has returned.
DOCS_CONF=$(mktemp)
trap 'rm -f "$DOCS_CONF"' EXIT
cat > "$DOCS_CONF" <<'NGINX'
worker_processes 1;
error_log stderr error;
events { worker_connections 64; }
http {
include /etc/nginx/mime.types;
access_log off;
server { listen 80; root /usr/share/nginx/html; }
}
NGINX
chmod 644 "$DOCS_CONF"
echo "docs for '$CLUSTER' on http://localhost:${DOCS_PORT}"
echo " (ctrl-c to stop; nothing is installed and nothing persists)"
docker run --rm \
--name "${CLUSTER}-docs" \
-e NGINX_ENTRYPOINT_QUIET_LOGS=1 \
-p "${DOCS_PORT}:80" \
-v "$REPO/docs:/usr/share/nginx/html:ro" \
-v "$DOCS_CONF:/etc/nginx/nginx.conf:ro" \
nginx:alpine
}

View File

@@ -208,6 +208,29 @@ render_kind_config() {
"$KIND_CONFIG"
}
# Render an addon's manifest (a .yaml beside its .sh) to stdout. Every ${NAME}, or
# ${NAME:-default}, is filled from the loaded config or the calling script's variables;
# a NAME with neither is an error, so a typo never becomes an empty image. $VAR and
# $(VAR) are left alone: they belong to the container. Notes: docs/notes/addons.md
render_manifest() {
local file="$1" text ref key def val
text=$(<"$file") || return 1
while IFS= read -r ref; do
key=${ref#\$\{}; key=${key%\}}; def=""
if [[ $key == *:-* ]]; then def=${key#*:-}; key=${key%%:-*}; fi
if [ -n "${!key:-}" ]; then
val=${!key}
elif [[ $ref == *:-* ]]; then
val=$def
else
echo "render_manifest: $file needs \${$key}, which nothing sets" >&2
return 1
fi
text=${text//"$ref"/"$val"}
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u)
printf '%s\n' "$text"
}
_config_restore() {
local line
while IFS= read -r line; do

View File

@@ -307,7 +307,7 @@ HOUSE_PAT="$(printf '%s' 'semes' 'ter|local' '\.ar\b')"
check "✖ S2 no example environment name from the copies era" "0" \
"$(cd .. && grep -rIlE "$COPIES_PAT" . --exclude-dir=def --exclude-dir=local --exclude=STALE.md 2>/dev/null | wc -l)"
check "✖ S3 ctrl/addons makes the cluster work, nothing more" "cert-manager metallb metrics-server" \
"$(ls addons/ | sed 's/\.sh$//' | sort | xargs)"
"$(ls addons/*.sh | xargs -n1 basename | sed 's/\.sh$//' | sort | xargs)" # the scripts; their manifests sit beside them
check "✖ S3 versions.env pins no workload image" "0" \
"$(grep -cE '^(POSTGRES|REDIS|AIRFLOW)_IMAGE=' versions.env)"
check "✖ S4 no namespace named after the cluster" "0" "$(grep -c "CLUSTER + ':namespace'" Tiltfile)"
@@ -378,6 +378,16 @@ note "the examples are overlays that work as shipped"
bad=""
for f in ../examples/*/addons/*.sh; do [ -e "$f" ] && { bash -n "$f" 2>/dev/null || bad+="$f "; }; done
check "every example addon parses" "" "$bad"
# An addon's manifest may only name what the config or its own script sets, so a typo
# fails here rather than as an empty value on a cluster.
bad=""
for y in addons/*.yaml ../examples/*/addons/*.yaml; do
[ -e "$y" ] || continue
for n in $(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' "$y" | tr -d '${}' | sort -u); do
grep -qE "(^|[[:space:]])$n=" "${y%.yaml}.sh" versions.env ../examples/*/rig.env 2>/dev/null || bad+="$y:$n "
done
done
check "every addon manifest names only what is set" "" "$bad"
if command -v python3 >/dev/null; then
bad=""
for f in ../examples/*/dags/*.py; do