big update based on real wokr
This commit is contained in:
@@ -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
|
||||
|
||||
19
rig/ctrl/addons/metallb.yaml
Normal file
19
rig/ctrl/addons/metallb.yaml
Normal 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
|
||||
@@ -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)"
|
||||
# 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
|
||||
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
|
||||
|
||||
# ── 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,24 +131,41 @@ 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."
|
||||
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
|
||||
@@ -145,6 +176,12 @@ if [ -n "$others" ] && { [ -n "$VERBOSE" ] || [ "$headroom" -lt 512 ]; }; then
|
||||
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"
|
||||
fi
|
||||
@@ -152,47 +189,106 @@ if [ "$overcommit" = "1" ]; then
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// TODO: PLACEHOLDER — replace with the real estate topology.
|
||||
//
|
||||
// This is where the extracted platform diagrams land. The shape below is
|
||||
// illustrative only: it shows how a mocked dependency, a real service and a
|
||||
// remote system are meant to sit together, not what the system actually is.
|
||||
//
|
||||
// The intended end state is that this file stops being hand-written and is
|
||||
// generated from the running cluster, so the diagram becomes a report of what
|
||||
// exists rather than a drawing of what was once intended.
|
||||
digraph estate {
|
||||
rankdir=LR
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Estate topology — PLACEHOLDER"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#ffc107"
|
||||
|
||||
subgraph cluster_new {
|
||||
label="New"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
api [label="service under work\n(real: built and hot-reloaded)" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
||||
}
|
||||
|
||||
subgraph cluster_core {
|
||||
label="Core (mocked)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
svc_a [label="upstream service\n(mock: canned responses)" fillcolor="#121829"]
|
||||
db [label="datastore\n(mock)" fillcolor="#121829" shape=cylinder]
|
||||
}
|
||||
|
||||
subgraph cluster_legacy {
|
||||
label="Legacy estate (mocked)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
batch [label="batch drop\n(mock: writes files on a timer)" fillcolor="#121829"]
|
||||
}
|
||||
|
||||
remote [label="external system\n(remote: ExternalName,\nreachable only from a VDI)" fillcolor="#3a1a1a" fontcolor="#ffc107" shape=octagon]
|
||||
|
||||
api -> svc_a [label="HTTP"]
|
||||
api -> db [label="query"]
|
||||
api -> batch [label="file handoff" style=dashed]
|
||||
api -> remote [label="only when reachable" style=dashed color="#ffc107"]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: estate Pages: 1 -->
|
||||
<svg width="579pt" height="362pt"
|
||||
viewBox="0.00 0.00 579.00 362.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 357.62)">
|
||||
<title>estate</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-357.62 575.41,-357.62 575.41,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="285.7" y="-334.42" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ffc107">Estate topology — PLACEHOLDER</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_new</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-130.12 8,-210.12 199,-210.12 199,-130.12 8,-130.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-190.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">New</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_core</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="357.33,-172.12 357.33,-318.12 534.83,-318.12 534.83,-172.12 357.33,-172.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-298.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Core (mocked)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_legacy</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="341.83,-84.12 341.83,-164.12 551.33,-164.12 551.33,-84.12 341.83,-84.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.58" y="-144.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Legacy estate (mocked)</text>
|
||||
</g>
|
||||
<!-- api -->
|
||||
<g id="node1" class="node">
|
||||
<title>api</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="191,-174.12 16,-174.12 16,-138.12 191,-138.12 191,-174.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-159.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">service under work</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-145.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(real: built and hot-reloaded)</text>
|
||||
</g>
|
||||
<!-- svc_a -->
|
||||
<g id="node2" class="node">
|
||||
<title>svc_a</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="526.83,-282.12 365.33,-282.12 365.33,-246.12 526.83,-246.12 526.83,-282.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-267.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">upstream service</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-253.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock: canned responses)</text>
|
||||
</g>
|
||||
<!-- api->svc_a -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>api->svc_a</title>
|
||||
<path fill="none" stroke="#4a5568" d="M149.07,-174.56C167.54,-182.07 189.23,-190.7 209,-198.12 258.25,-216.6 270.12,-222.85 320.75,-237.12 331.43,-240.13 342.71,-243 353.94,-245.67"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="353.12,-249.07 363.66,-247.92 354.71,-242.25 353.12,-249.07"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-234.24" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">HTTP</text>
|
||||
</g>
|
||||
<!-- db -->
|
||||
<g id="node3" class="node">
|
||||
<title>db</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M480.7,-223.81C480.7,-226.22 465.18,-228.18 446.08,-228.18 426.97,-228.18 411.45,-226.22 411.45,-223.81 411.45,-223.81 411.45,-184.43 411.45,-184.43 411.45,-182.02 426.97,-180.06 446.08,-180.06 465.18,-180.06 480.7,-182.02 480.7,-184.43 480.7,-184.43 480.7,-223.81 480.7,-223.81"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M480.7,-223.81C480.7,-221.39 465.18,-219.43 446.08,-219.43 426.97,-219.43 411.45,-221.39 411.45,-223.81"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-207.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">datastore</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-193.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock)</text>
|
||||
</g>
|
||||
<!-- api->db -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>api->db</title>
|
||||
<path fill="none" stroke="#4a5568" d="M191.32,-168.36C257.83,-177.73 346.91,-190.28 399.91,-197.75"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="399.4,-201.22 409.79,-199.15 400.38,-194.29 399.4,-201.22"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-185.69" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">query</text>
|
||||
</g>
|
||||
<!-- batch -->
|
||||
<g id="node4" class="node">
|
||||
<title>batch</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="537.33,-128.12 354.83,-128.12 354.83,-92.12 537.33,-92.12 537.33,-128.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-113.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">batch drop</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-99.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock: writes files on a timer)</text>
|
||||
</g>
|
||||
<!-- api->batch -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>api->batch</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M191.32,-144.39C237.67,-138.13 294.98,-130.39 343.4,-123.85"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="343.61,-127.36 353.05,-122.55 342.67,-120.42 343.61,-127.36"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-143.94" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">file handoff</text>
|
||||
</g>
|
||||
<!-- remote -->
|
||||
<g id="node5" class="node">
|
||||
<title>remote</title>
|
||||
<polygon fill="#3a1a1a" stroke="#1e2a4a" points="571.41,-21.74 571.41,-52.5 497.99,-74.24 394.17,-74.24 320.75,-52.5 320.75,-21.74 394.17,0 497.99,0 571.41,-21.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-46.92" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">external system</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-33.42" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(remote: ExternalName,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-19.92" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">reachable only from a VDI)</text>
|
||||
</g>
|
||||
<!-- api->remote -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>api->remote</title>
|
||||
<path fill="none" stroke="#ffc107" stroke-dasharray="5,2" d="M149.55,-137.68C167.9,-130.34 189.37,-121.97 209,-114.87 254.45,-98.43 305.32,-81.49 348.17,-67.64"/>
|
||||
<polygon fill="#ffc107" stroke="#ffc107" points="349,-71.05 357.44,-64.65 346.85,-64.39 349,-71.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-117.57" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">only when reachable</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.9 KiB |
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>rig — local environment installer</title>
|
||||
<style>
|
||||
@@ -237,13 +238,9 @@
|
||||
<div class="nav-backdrop"></div>
|
||||
<nav>
|
||||
<a href="#start">Start here</a>
|
||||
<a href="#steps">The steps</a>
|
||||
<a href="#install">Installation</a>
|
||||
<a href="#environments">Environments</a>
|
||||
<a href="#overlays">Overlays</a>
|
||||
<a href="#profiles">Profiles</a>
|
||||
<a href="#registry">Registry</a>
|
||||
<a href="#architecture">Architecture</a>
|
||||
<a href="#troubleshooting">Troubleshooting</a>
|
||||
</nav>
|
||||
|
||||
@@ -251,114 +248,68 @@
|
||||
|
||||
<section class="section" id="start">
|
||||
<h2>Start here</h2>
|
||||
<p class="lede">A runnable local model of a large, regulated estate — legacy and new side by side.</p>
|
||||
<div class="prose">
|
||||
<p>rig builds a disposable Kubernetes environment on your machine so you can
|
||||
explore how a system fits together without needing access to any of it. Its
|
||||
job is <b>onboarding and exploration</b>, not a production replica.</p>
|
||||
<p>rig builds a disposable Kubernetes environment on your machine, so you can
|
||||
see how a system fits together without access to any of it. It is for
|
||||
<b>onboarding and exploration</b>, not a production replica.</p>
|
||||
|
||||
<p>Most services in it are deliberately <b>not real</b>. What has to be faithful
|
||||
is the topology — the names, the ports, the dependency order, who can reach whom,
|
||||
and how it fails. The workloads themselves are noise. This is what makes the
|
||||
whole estate fit on a laptop: a real 20-service platform will not fit even once
|
||||
on 14 GB, but mocks are about 30 MB each, so three faithful copies do.</p>
|
||||
<p>Most services in it are <b>not real</b>. What has to be faithful is the
|
||||
topology: names, ports, dependency order, who can reach whom, and how it fails.
|
||||
That is what makes a whole estate fit on a laptop. A real 20-service platform
|
||||
does not fit once in 14 GB; stubs of about 30 MB each fit three times.</p>
|
||||
|
||||
<h3>The only prerequisite</h3>
|
||||
<p><b>Docker.</b> No curl, no jq, no python, no apt repositories to configure.</p>
|
||||
<pre><code><span class="c"># then, in the environment directory:</span>
|
||||
make check <span class="c"># is this machine ready? reports, never fixes</span>
|
||||
make deps <span class="c"># install the pinned toolchain</span>
|
||||
<pre><code><span class="c"># in the environment directory:</span>
|
||||
make check <span class="c"># is this machine ready? reports, never changes anything</span>
|
||||
make deps <span class="c"># install the pinned toolchain; safe to re-run</span>
|
||||
make cluster up <span class="c"># cluster + registry + addons; ports derive by themselves</span>
|
||||
</code></pre>
|
||||
<p>Read <code>make check</code> before <code>make deps</code>. It never changes
|
||||
anything — it prints what it found and, at the end, the steps it cannot perform
|
||||
for you.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="steps">
|
||||
<h2>The steps</h2>
|
||||
<p class="lede">Start to finish, in order, with what each one actually does.</p>
|
||||
<div class="prose">
|
||||
|
||||
<h3>1 · make check</h3>
|
||||
<p>Asks whether this machine is ready. It <b>changes nothing</b> — it
|
||||
reports what it found and, at the end, the things only a human can do
|
||||
(anything needing <code>sudo</code>, or a Windows-side restart). Read it
|
||||
before installing anything; it is faster than discovering the same problems
|
||||
one failure at a time.</p>
|
||||
<pre><code>make check</code></pre>
|
||||
|
||||
<h3>2 · make deps</h3>
|
||||
<p>Installs the pinned toolchain — only what is missing — and tells you
|
||||
if its directory is not on PATH yet. Running it twice is safe.</p>
|
||||
<pre><code>make deps</code></pre>
|
||||
|
||||
<h3>3 · make cluster up</h3>
|
||||
<p>Builds the cluster, starts its registry and installs the profile's
|
||||
addons — there is nothing else to run first. It prints what the profile
|
||||
locks in <i>before</i> spending the time, because the kind config is
|
||||
fixed at creation and cannot be changed afterwards.</p>
|
||||
<p>Re-running is safe and, more importantly, <b>convergent</b>: if a first
|
||||
attempt was interrupted before the CNI was installed, running it again
|
||||
finishes the job rather than reporting "already exists" and leaving every
|
||||
node permanently NotReady.</p>
|
||||
<p>Read <code>make check</code> before <code>make deps</code>: it lists what is
|
||||
missing, including the steps only you can do, such as anything needing
|
||||
<code>sudo</code> or a Windows-side restart.</p>
|
||||
|
||||
<h3>make cluster up</h3>
|
||||
<p>It prints what the kind config locks in before spending the time, because
|
||||
that config is fixed when the cluster is created. Re-running is safe and
|
||||
continues where an interrupted run stopped, rather than reporting that the
|
||||
cluster already exists and leaving every node <code>NotReady</code>.</p>
|
||||
<pre><code>make cluster up <span class="c"># built-in defaults — no profile needed</span>
|
||||
make cluster up PROFILE=mirror <span class="c"># after copying env.d/mirror.env.example: cached registry</span>
|
||||
make cluster up PROFILE=mirror <span class="c"># images through an internal registry</span>
|
||||
make cluster up OVERLAY=examples/data <span class="c"># an overlay: what runs, kept outside rig</span>
|
||||
make cluster reset <span class="c"># destroy and rebuild — how an edited kind config takes effect</span>
|
||||
make cluster reset <span class="c"># destroy and rebuild: how an edited kind config takes effect</span>
|
||||
</code></pre>
|
||||
|
||||
<h3>4 · make docs</h3>
|
||||
<p>Serves this page from a throwaway container. Works with no cluster and
|
||||
no toolchain, which is deliberate: these pages are the instructions for
|
||||
building everything else, so they cannot depend on it.</p>
|
||||
<pre><code>make docs</code></pre>
|
||||
|
||||
<h3>Checking on things</h3>
|
||||
<dl>
|
||||
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. The usual reason a new one will not start is an old one you forgot about; <code>make cluster free</code> frees them without deleting.</dd>
|
||||
<dt>make check</dt><dd>Short: host, toolchain, and whether this cluster fits, its ports, registry and addons. Details only appear when something needs attention; <code>make check all</code> prints every one.</dd>
|
||||
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it really climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
|
||||
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. A new cluster usually fails to start because of an old one; <code>make cluster free</code> stops them without deleting.</dd>
|
||||
<dt>make check</dt><dd>Host, toolchain, and whether this cluster fits, its ports, registry and addons. Only problems print; <code>make check all</code> prints everything.</dd>
|
||||
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
|
||||
<dt>make docs</dt><dd>Serves these pages from a throwaway container. It needs no cluster and no toolchain, because these are the instructions for building both.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Running more than one</h3>
|
||||
<p>Name another overlay, or copy the directory and rename it. Cluster name,
|
||||
context, image tags and the port block all follow the folder name — the
|
||||
overlay's, or rig's — so the second environment collides with nothing and
|
||||
neither one's teardown can reach the other.</p>
|
||||
<pre><code>OVERLAY=local/platform-v2 make cluster up
|
||||
cp -r rig ../platform-v3 && cd ../platform-v3 && make cluster up
|
||||
</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="install">
|
||||
<h2>Installation</h2>
|
||||
<p class="lede">A container installs onto the host and then gets out of the way.</p>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="Installation flow"></a>
|
||||
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="How the installer works"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>The installer is a container, not a shell script, for a specific reason: a
|
||||
stock slim Debian has no <code>curl</code>, no <code>wget</code>, no
|
||||
<code>jq</code>, no <code>python3</code> and <b>no CA bundle</b>. A shell
|
||||
installer could not make a verified HTTPS request, let alone check one. The
|
||||
container carries its own toolchain, so the host needs nothing but Docker.</p>
|
||||
|
||||
<p>The cluster never runs inside that container. Everything it installs —
|
||||
kind, kubectl, tilt, jq — runs natively afterwards, so nothing pays a
|
||||
container tax during daily work.</p>
|
||||
|
||||
<h3>Pinned and verified</h3>
|
||||
<p>Every tool is a single binary fetched at a pinned version and checked
|
||||
against a published SHA256. Node images are pinned <b>by digest</b>, so
|
||||
upgrading kind cannot silently move your Kubernetes version.</p>
|
||||
|
||||
<h3>Not every machine should get cluster tooling</h3>
|
||||
<p>A managed or corporate-issued machine — the kind that holds the access
|
||||
you cannot get anywhere else — is not somewhere to install development
|
||||
tools by default. So the toolchain comes in two tiers:</p>
|
||||
<p>The installer is a container, not a shell script, because a stock slim Debian
|
||||
has no curl, wget, jq, python3 or CA bundle: a shell installer could not make a
|
||||
verified HTTPS request, let alone check one. The container carries its own
|
||||
toolchain, so the host needs only Docker. Nothing runs inside it afterwards —
|
||||
kind, kubectl, tilt and jq run natively.</p>
|
||||
|
||||
<p>Every tool is a single binary at a pinned version, checked against a published
|
||||
SHA256. Node images are pinned <b>by digest</b>, so upgrading kind cannot move
|
||||
your Kubernetes version.</p>
|
||||
|
||||
<h3>Two tiers</h3>
|
||||
<p>A managed or corporate-issued machine is not somewhere to install development
|
||||
tools by default, so installing cluster tooling is a decision rather than a side
|
||||
effect:</p>
|
||||
<table>
|
||||
<tr><th>tier</th><th>installs</th><th>for</th></tr>
|
||||
<tr><td><code>core</code></td><td>kubectl, jq</td><td>talk to a cluster someone else runs</td></tr>
|
||||
@@ -367,11 +318,6 @@ cp -r rig ../platform-v3 && cd ../platform-v3 && make cluster up
|
||||
<pre><code>make deps core <span class="c"># kubectl and jq only — nothing that creates a cluster</span>
|
||||
make deps <span class="c"># dev, the default</span>
|
||||
</code></pre>
|
||||
<p>Testing <i>in situ</i> on a managed machine is still possible — install
|
||||
the <code>dev</code> tier deliberately when you need it. The point is that
|
||||
it should be a decision rather than a side effect of installing.</p>
|
||||
<p>The documentation itself needs neither tier: <code>make docs</code>
|
||||
wants only Docker.</p>
|
||||
|
||||
<h3>Air-gapped</h3>
|
||||
<pre><code>make deps image full <span class="c"># bakes every binary into the image</span>
|
||||
@@ -384,185 +330,129 @@ docker load < rig.tgz && make cluster up PROFILE=offline <span clas
|
||||
|
||||
<section class="section" id="environments">
|
||||
<h2>Environments</h2>
|
||||
<p class="lede">One folder is one environment — an overlay's, or rig's own. Copy it, rename it, run it.</p>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Running several versions of a system at once means several clusters on one
|
||||
machine, not several machines. Everything that could collide is derived from
|
||||
the folder name (the overlay's when one is named):</p>
|
||||
<p>One folder is one environment. Running several versions of a system at once
|
||||
means several clusters on one machine, and everything that could collide is
|
||||
derived from the folder name (the overlay's, when one is named):</p>
|
||||
<dl>
|
||||
<dt>cluster + context</dt><dd><code>platform-v2/</code> builds <code>platform-v2</code> on <code>kind-platform-v2</code>.</dd>
|
||||
<dt>port block</dt><dd>Ten ports from a hash of the name, in the 20000+ range — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
|
||||
<dt>port block</dt><dd>Ten ports from a hash of the name, above 20000 — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
|
||||
<dt>registry + images</dt><dd>Named after the environment, so two copies never share one.</dd>
|
||||
</dl>
|
||||
<p>Two copies therefore never collide, and neither one's
|
||||
<code>make cluster down</code> can touch the other. <code>make check</code>
|
||||
shows the block; <code>bash ctrl/ports.sh persist</code> freezes it into
|
||||
<code>ctrl/.env</code> if you want it fixed rather than derived.</p>
|
||||
<p>So two copies never collide, and neither one's <code>make cluster down</code>
|
||||
can touch the other. <code>make check</code> shows the block;
|
||||
<code>bash ctrl/ports.sh persist</code> freezes it into <code>ctrl/.env</code>.</p>
|
||||
|
||||
<h3>Configuration layers</h3>
|
||||
<p>Weakest first, later wins: built-in defaults → pinned versions → a
|
||||
profile, if you name one → the overlay's <code>rig.env</code> →
|
||||
<code>ctrl/.env</code> → the environment. So
|
||||
<code>make cluster up PROFILE=<name></code> always beats every file.</p>
|
||||
<p>Weakest first, later wins: built-in defaults → pinned versions → a profile, if
|
||||
you name one → the overlay's <code>rig.env</code> → <code>ctrl/.env</code> → the
|
||||
caller's environment. So a value on the command line always beats every file.</p>
|
||||
|
||||
<h3>Profiles</h3>
|
||||
<p>A profile says how this machine reaches the world. rig needs none.</p>
|
||||
<table>
|
||||
<tr><th>example</th><th>registry</th><th>for</th></tr>
|
||||
<tr><td>none</td><td><code>local</code></td><td>the built-in defaults</td></tr>
|
||||
<tr><td><code>mirror.env.example</code></td><td><code>mirror</code></td><td>images through an internal registry</td></tr>
|
||||
<tr><td><code>offline.env.example</code></td><td><code>local</code></td><td>air-gapped</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Registry modes</h3>
|
||||
<table>
|
||||
<tr><th>mode</th><th>what it does</th></tr>
|
||||
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
|
||||
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
|
||||
<tr><td><code>mirror</code></td><td>that container as a pull-through cache of the corporate registry</td></tr>
|
||||
<tr><td><code>remote</code></td><td>no local container; pull direct with an <code>imagePullSecret</code></td></tr>
|
||||
</table>
|
||||
<p><code>mirror</code> is what a locked-down network looks like: images come from
|
||||
the corporate registry, you do not hammer it, and you keep working when the
|
||||
connection drops. It is also only reachable from where that registry is routable —
|
||||
from a laptop, use <code>local</code> or <code>offline</code>.</p>
|
||||
<p><b>Corporate CA trust is the usual failure.</b> It has to reach three places:
|
||||
the host Docker daemon, every node's containerd (nodes do not inherit host trust),
|
||||
and any in-cluster client. Set <code>REGISTRY_CA_FILE</code>;
|
||||
<code>make check</code> reports which is still missing. Without it the error is
|
||||
<code>x509: certificate signed by unknown authority</code>.</p>
|
||||
|
||||
<h3>LoadBalancer services</h3>
|
||||
<p>Real manifests use <code>type: LoadBalancer</code>. On a bare local cluster
|
||||
those Services sit at <code>EXTERNAL-IP <pending></code> forever, with no
|
||||
error: the deployment looks healthy and is unreachable. The <code>metallb</code>
|
||||
addon fixes that, so upstream manifests work unchanged. Its address pool is
|
||||
derived from the cluster's Docker network at install time, because Docker picks
|
||||
that subnet and it differs between machines.</p>
|
||||
<p>Those addresses are reachable from the Linux side, including inside WSL. A
|
||||
browser on Windows has no route to them: use the ingress host ports instead.</p>
|
||||
|
||||
<h3>Networking and memory</h3>
|
||||
<p>The cluster uses kind's built-in networking, which does enforce standard
|
||||
NetworkPolicy — verified against a no-policy control. A pluggable CNI was tried
|
||||
and removed: it added policy features nothing here needs, in exchange for a
|
||||
slower boot and one more thing to get right at creation time.</p>
|
||||
<p>Every cluster is a running container tree whether you use it or not.
|
||||
<code>make cluster list</code> shows what each costs, and
|
||||
<code>make cluster free</code> stops the others without deleting them.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="overlays">
|
||||
<h2>Overlays</h2>
|
||||
<p class="lede">What runs lives outside rig — rig reads it and never writes into it.</p>
|
||||
<div class="prose">
|
||||
<p>An overlay is one folder, kept outside rig's version control, holding a
|
||||
use case. Every piece is optional:</p>
|
||||
<p>An overlay is one folder, kept outside rig's version control, holding a use
|
||||
case. rig reads it and never writes into it. Every piece is optional:</p>
|
||||
<table>
|
||||
<tr><th>in the overlay</th><th>what rig does with it</th></tr>
|
||||
<tr><td><code>rig.env</code></td><td>a config layer: addons, namespaces, images — anything a profile could set</td></tr>
|
||||
<tr><td><code>k8s/overlays/dev/</code></td><td>the manifests the dev loop applies</td></tr>
|
||||
<tr><td><code>kind-config.yaml.tpl</code></td><td>the cluster's shape, when it needs its own (mounts, ports)</td></tr>
|
||||
<tr><td><code>addons/<name>.sh</code></td><td>addons, found before rig's own</td></tr>
|
||||
<tr><td><code>addons/<name>.sh</code></td><td>addons, found before rig's own; each applies its own <code><name>.yaml</code></td></tr>
|
||||
<tr><td><code>Tiltfile</code></td><td>the workload's half of the dev loop, included by rig's</td></tr>
|
||||
</table>
|
||||
<pre><code>cp -r examples/starter local/myenv <span class="c"># local/ is gitignored</span>
|
||||
OVERLAY=local/myenv make cluster up
|
||||
</code></pre>
|
||||
<p>With none named, rig runs its own <code>examples/starter</code>.
|
||||
<code>examples/data</code> carries postgres, redis and airflow as an
|
||||
overlay's own addons. A project can also carry rig at
|
||||
<code><project>/rig/</code> and be the overlay itself, with a
|
||||
three-line forwarding Makefile — see <code>docs/notes/overlay.md</code>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="profiles">
|
||||
<h2>Profiles</h2>
|
||||
<p class="lede">How this machine reaches the world — optional; rig needs none.</p>
|
||||
<div class="prose">
|
||||
<table>
|
||||
<tr><th>example</th><th>registry</th><th>for</th></tr>
|
||||
<tr><td><i>none</i></td><td>local</td><td>the built-in defaults; no profile needed</td></tr>
|
||||
<tr><td><code>mirror.env.example</code></td><td>mirror</td><td>images through an internal registry</td></tr>
|
||||
<tr><td><code>offline.env.example</code></td><td>local</td><td>air-gapped</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="note"><p><b>The kind config cannot be re-applied.</b> Edit
|
||||
<code>ctrl/k8s/kind-config.yaml.tpl</code> (or the overlay's own); it takes effect when the cluster is created. <code>cluster up</code> prints what it
|
||||
locks in before spending the time, and <code>make cluster reset</code> is
|
||||
the way out.</p></div>
|
||||
|
||||
<h3>LoadBalancer services</h3>
|
||||
<p>Real manifests use <code>type: LoadBalancer</code>, because a real
|
||||
cluster has one. On a bare local cluster those Services sit at
|
||||
<code>EXTERNAL-IP <pending></code> forever, with no error anywhere —
|
||||
the deployment looks healthy and simply is not reachable.</p>
|
||||
<p>The <code>metallb</code> addon fixes that, so the same manifests work
|
||||
here as upstream and nothing has to be rewritten to NodePort. Its address
|
||||
pool is derived from the cluster's Docker network at install time rather
|
||||
than hardcoded, because Docker picks that subnet and it differs between
|
||||
machines.</p>
|
||||
<div class="note"><p><b>Where those addresses are reachable from.</b> The
|
||||
pool lives on the Docker bridge, so LoadBalancer IPs work from the Linux
|
||||
side — including from inside WSL. A browser on Windows has no route to
|
||||
them. Use the ingress host ports for anything you need to open in a
|
||||
browser.</p></div>
|
||||
|
||||
<h3>Networking</h3>
|
||||
<p>The cluster uses kind's built-in networking, which <b>does</b> enforce
|
||||
standard NetworkPolicy — verified against a no-policy control, not assumed.
|
||||
The widely repeated claim that it accepts policies and silently ignores
|
||||
them is out of date.</p>
|
||||
<p>A pluggable CNI was tried and removed: it only added
|
||||
GlobalNetworkPolicy, policy tiers and egress-CIDR rules, none of which are
|
||||
needed yet, in exchange for a slower boot and one more thing that has to be
|
||||
right at creation time. Worth revisiting only when a policy the built-in
|
||||
cannot express actually comes up.</p>
|
||||
|
||||
<h3>Memory</h3>
|
||||
<p>Every cluster is a running container tree whether you are using it or not.
|
||||
<code>make cluster list</code> shows what exists and what it costs;
|
||||
<code>make cluster free</code> stops the others without deleting them.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="registry">
|
||||
<h2>Registry</h2>
|
||||
<p class="lede">Local, cached, or straight to the corporate registry.</p>
|
||||
<div class="prose">
|
||||
<table>
|
||||
<tr><th>mode</th><th>what it does</th></tr>
|
||||
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
|
||||
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
|
||||
<tr><td><code>mirror</code></td><td>that container as a <b>pull-through cache</b> of the corporate registry</td></tr>
|
||||
<tr><td><code>remote</code></td><td>no local container; pull direct with an imagePullSecret</td></tr>
|
||||
</table>
|
||||
<p><code>mirror</code> is what a locked-down network actually looks like:
|
||||
images originate from the corporate registry, you do not hammer it, and you
|
||||
keep working when the connection drops.</p>
|
||||
|
||||
<div class="note"><p><b>The corporate CA will bite you.</b> A corporate
|
||||
registry is usually behind an internal CA, and trust has to reach
|
||||
<b>three</b> places: the host Docker daemon, every cluster node's containerd
|
||||
(nodes do <i>not</i> inherit host trust), and any in-cluster client. Set
|
||||
<code>REGISTRY_CA_FILE</code> and <code>make check</code> reports which is
|
||||
still missing. The symptom otherwise is an opaque
|
||||
<code>x509: certificate signed by unknown authority</code>.</p></div>
|
||||
|
||||
<p>Reachability also depends on where you are: if the registry is only
|
||||
routable from a managed workspace, <code>mirror</code> and <code>remote</code>
|
||||
will not resolve from a laptop at all. That is what <code>local</code> and
|
||||
<code>offline</code> are for.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="architecture">
|
||||
<h2>Architecture</h2>
|
||||
<p class="lede">The estate being modelled.</p>
|
||||
<div class="note"><p><b>TODO — placeholder.</b> The diagram below is
|
||||
illustrative only: it shows how a mocked dependency, a service under active
|
||||
work, and an unreachable remote system sit together. It is not the real
|
||||
topology. Replace <code>docs/graphs/03-architecture.dot</code> with the
|
||||
extracted platform diagrams, then run <code>make docs graphs</code>.</p></div>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/03-architecture.svg"><img src="graphs/03-architecture.svg" alt="Estate topology (placeholder)"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Each component is one of three things, and switching between them should be
|
||||
a one-line change rather than a rewrite:</p>
|
||||
<code>examples/data</code> carries postgres, redis and airflow as an overlay's own
|
||||
addons. A project can also carry rig at <code><project>/rig/</code> and be
|
||||
the overlay itself, with a three-line forwarding Makefile — see
|
||||
<code>docs/notes/overlay.md</code>.</p>
|
||||
|
||||
<h3>What runs in it</h3>
|
||||
<p>Each component is one of three things, and switching between them is a
|
||||
one-line change:</p>
|
||||
<dl>
|
||||
<dt>real</dt><dd>Built from source and hot-reloaded. The thing you are actually working on — usually exactly one.</dd>
|
||||
<dt>real</dt><dd>Built from source and hot-reloaded. What you are working on — usually exactly one.</dd>
|
||||
<dt>mock</dt><dd>A generic stub with canned responses. Everything you do not care about today.</dd>
|
||||
<dt>remote</dt><dd>No pod at all: a Service of type ExternalName pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
|
||||
<dt>remote</dt><dd>No pod at all: a Service of type <code>ExternalName</code> pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
|
||||
</dl>
|
||||
<p>The intended end state is that this diagram is <b>generated from the
|
||||
running cluster</b> rather than drawn by hand — so it becomes a report of
|
||||
what exists instead of a picture of what was once intended.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="troubleshooting">
|
||||
<h2>Troubleshooting</h2>
|
||||
<p class="lede">The failures that are hard to diagnose from their symptoms.</p>
|
||||
<div class="prose">
|
||||
<h3>Tilt stops noticing file changes</h3>
|
||||
<p>Almost always <code>inotify</code> limits, and it fails <i>silently</i> —
|
||||
nothing errors, changes just stop being picked up. Defaults on WSL are far too
|
||||
low. <code>make check</code> reports it and prints the fix.</p>
|
||||
<p>Almost always inotify limits, and it fails silently: nothing errors, changes
|
||||
just stop being picked up. Defaults on WSL are far too low.
|
||||
<code>make check</code> reports it and prints the fix.</p>
|
||||
|
||||
<h3>Cluster creation dies halfway with a port error</h3>
|
||||
<p>Docker reports <code>failed to bind host port … address already in use</code>
|
||||
partway through creating the cluster. Run <code>make check</code> first — it
|
||||
checks every port in this environment's block before anything is built.</p>
|
||||
partway through creating the cluster. <code>make check</code> tests every port in
|
||||
this environment's block before anything is built.</p>
|
||||
|
||||
<h3>Every node stays NotReady</h3>
|
||||
<p>Usually a cluster created with the default CNI disabled but the real CNI
|
||||
never installed — typically an interrupted first run. Just run
|
||||
<code>make cluster up</code> again: it converges rather than exiting early, and
|
||||
will finish the missing steps.</p>
|
||||
<p>Usually a cluster created with the default CNI disabled and the real CNI never
|
||||
installed, after an interrupted first run. Run <code>make cluster up</code> again:
|
||||
it finishes the missing steps instead of stopping because the cluster exists.</p>
|
||||
|
||||
<h3>x509: certificate signed by unknown authority</h3>
|
||||
<p>Corporate CA trust has not reached one of the three places it needs to be.
|
||||
See <a href="#registry">Registry</a>.</p>
|
||||
<p>Corporate CA trust has not reached one of the three places it needs to be. See
|
||||
<a href="#environments">Environments</a>.</p>
|
||||
|
||||
<h3>kubectl says the context does not exist</h3>
|
||||
<p>The cluster can exist while its context does not — a reset or a switched
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
Each addon is its own idempotent script — adding one is adding a file, not editing a dispatcher. `ADDONS` names them, in install order; the overlay's `addons/<name>.sh` is found before rig's `ctrl/addons/<name>.sh`, and every one runs from rig's `ctrl/` with `RIG_CTRL` exported, wherever its file lives (see [overlay.md](overlay.md)).
|
||||
|
||||
## The manifest beside the script
|
||||
|
||||
An addon's Kubernetes objects are a plain `<name>.yaml` beside its `<name>.sh`, not a heredoc inside it: the YAML reads, diffs and lints as YAML, and the script keeps only what YAML cannot do (secrets made once, a database created, waiting for a rollout). The script renders it with `render_manifest "$ADDON_DIR/<name>.yaml"` (`lib/config.sh`) and pipes it to `kubectl apply`.
|
||||
|
||||
`render_manifest` fills `${NAME}` and `${NAME:-default}` from the loaded config or the script's own variables, and fails on a name nothing sets, so a typo is an error rather than an empty image. `$VAR` and `$(VAR)` are left alone: they are the container's, as in postgres' `pg_isready -U $POSTGRES_USER`. selftest checks that every name a manifest uses is set somewhere. `ADDON_DIR` is taken before the script's `cd` into rig's `ctrl/`: `ADDON_DIR="$(cd "$(dirname "$0")" && pwd)"`.
|
||||
|
||||
## What rig ships, and what it does not
|
||||
|
||||
rig's own addons make the *cluster* work, and are useless outside one: metallb, cert-manager, metrics-server. Things a workload happens to need — a database, a cache, a scheduler — are the workload's, and which workload needs which is not rig's business, so they live with the overlay. `examples/data/addons/` has postgres, redis and airflow as a worked example; an overlay that wants them copies them in.
|
||||
|
||||
@@ -34,14 +34,37 @@ A port held by THIS environment's own cluster is not a clash; it is the thing wo
|
||||
|
||||
The ports are extracted with a second grep rather than `tr -d ':->'`: in tr, ':->' is the character RANGE ':' to '>', which does not contain '-', so the trailing dash survives and nothing ever matches.
|
||||
|
||||
## Compact by default
|
||||
|
||||
`make check` prints one line per question — host, toolchain, and for this rig: cluster, memory,
|
||||
ports, registry, addons — and adds detail only where something needs attention (`!` lines, the
|
||||
"held elsewhere" list when memory is tight, the clashing port). `make check all` prints every fact,
|
||||
as the full report did before 2026-09-17. `deps.sh detect all` is the same switch for the host part,
|
||||
so the standalone `rigdeps.sh detect` is short too. Changed because the long report buried the few
|
||||
lines that mattered.
|
||||
## Budget first, configuration after (2026-09-23)
|
||||
|
||||
`make check` answers the question people run it for first, in the shape the adapter rig had
|
||||
(the user preferred it): a memory budget with one row per part, the node overhead, the total
|
||||
against what the machine has and has available; then a one-line verdict (`fits`, `! tight`,
|
||||
`! does not fit`, with the two opposite fixes told apart); then where things stand and the next
|
||||
command — not created, stopped (with the `docker start` line), up (with the Tilt and gateway
|
||||
URLs, and whether this environment's Tilt is already running).
|
||||
|
||||
Below that, the configuration as it is, raw: the host and toolchain (`deps.sh detect`), then
|
||||
one `key value` line each for cluster, context, overlay, profile, nodes, kind config, manifests,
|
||||
ingress, ports (named), registry, addons. `!` lines always print; `make check all` adds every
|
||||
fact (per-port detail, addon list, swap and overcommit notes). `deps.sh detect all` is the same
|
||||
switch for the host part, so the standalone `rigdeps.sh detect` is short too.
|
||||
|
||||
## MEMORY_BUDGET
|
||||
|
||||
rig knows what a node costs (NODE_MB), not what a workload does, so the parts come from the
|
||||
overlay's rig.env: `MEMORY_BUDGET="name=MB ..."`, in the order listed; a 0 is left out, and an
|
||||
entry that is not `name=MB` is a `!` line. rig.env is sourced, so an overlay can size a part by
|
||||
its own knob (poc_rig's airflow follows its mode). When one part outweighs the rest together
|
||||
(and is at least 1 GB), check names it as the first thing to switch off; `MEMORY_BUDGET_TIP`
|
||||
says how, in the overlay's words. No budget: one `?` row says where to add it.
|
||||
|
||||
## Tilt: ours or another
|
||||
|
||||
A Tilt still running after `make cluster down` holds the Tilt port, and the next `make tilt`
|
||||
fails with "address already in use" (2026-09-23, the Workspace). check reads the listener with
|
||||
`ss -p` (our own processes only): a `tilt` whose working directory is this ctrl/ is this
|
||||
environment's Tilt, reported as running with its URL; another `tilt` is a `!` line with its pid,
|
||||
its folder and the `kill` that frees the port. Anything else is the ordinary port clash.
|
||||
|
||||
## overlay and kind config
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ out `python -m http.server` too — a minimal Debian has no python3. What it doe
|
||||
have, by definition, is Docker: the single prerequisite rig already demands. So a
|
||||
throwaway nginx container serves a read-only bind mount.
|
||||
|
||||
It runs with a config of its own: one worker (nginx's `auto` starts one per core,
|
||||
twelve on an ordinary laptop, for a handful of static pages), no access log and a
|
||||
quiet entrypoint, so the terminal shows the URL and nothing else unless something
|
||||
fails. The pages declare an empty icon, so the browser does not ask for a
|
||||
`favicon.ico` that is not there.
|
||||
|
||||
## Committed SVGs
|
||||
|
||||
Rendered SVGs are committed alongside their `.dot` sources for the same reason:
|
||||
|
||||
@@ -60,6 +60,9 @@ air-gapped install); an overlay says what runs. `ctrl/.env` is still this
|
||||
machine's, and the caller still wins over everything.
|
||||
|
||||
`rig.env` may not set `PROFILE` or `OVERLAY`: both are chosen before it loads.
|
||||
|
||||
`MEMORY_BUDGET` (and optionally `MEMORY_BUDGET_TIP`) is how an overlay tells `make check` what its
|
||||
workload costs: rig only knows the node. See check.md.
|
||||
The paths it sets (`MANIFESTS_DIR`, `KIND_CONFIG`) are relative to the overlay.
|
||||
`MANIFESTS_DIR=none` means rig applies no manifests and the overlay's Tiltfile
|
||||
does, e.g. when kustomize needs flags.
|
||||
@@ -100,7 +103,8 @@ load_config
|
||||
```
|
||||
|
||||
`OVERLAY_DIR` is set, so an addon can find files beside it
|
||||
(`$(_from_ctrl "$OVERLAY_DIR")/...`). rig's own `ctrl/addons/` holds only what
|
||||
(`$(_from_ctrl "$OVERLAY_DIR")/...`). Its manifest is a `<name>.yaml` beside the script,
|
||||
rendered with `render_manifest` (see [addons.md](addons.md)). rig's own `ctrl/addons/` holds only what
|
||||
makes a cluster work (metallb, cert-manager, metrics-server);
|
||||
`examples/data/addons/` shows workload ones.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>Graph Viewer</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Requires the postgres addon; refuses to install without it.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
@@ -70,77 +71,7 @@ if [ -d "$dags" ]; then
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
selector:
|
||||
app: airflow
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airflow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airflow
|
||||
spec:
|
||||
containers:
|
||||
- name: airflow
|
||||
image: ${AIRFLOW_IMAGE}
|
||||
args: ["standalone"]
|
||||
env:
|
||||
- name: AIRFLOW__CORE__EXECUTOR
|
||||
value: LocalExecutor
|
||||
- name: AIRFLOW__CORE__LOAD_EXAMPLES
|
||||
value: "false"
|
||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: SQL_ALCHEMY_CONN}
|
||||
- name: AIRFLOW_CONN_APP_DB
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: AIRFLOW_CONN_APP_DB}
|
||||
- name: AIRFLOW__CORE__FERNET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||
- name: _AIRFLOW_WWW_USER_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_USER}
|
||||
- name: _AIRFLOW_WWW_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: dags
|
||||
mountPath: /opt/airflow/dags
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
# First boot runs the whole migration before it serves anything.
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
volumes:
|
||||
- name: dags
|
||||
configMap:
|
||||
name: airflow-dags
|
||||
optional: true
|
||||
YAML
|
||||
render_manifest "$ADDON_DIR/airflow.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
||||
$K rollout status deployment/airflow -n "$NS" --timeout=600s
|
||||
|
||||
69
rig/examples/data/addons/airflow.yaml
Normal file
69
rig/examples/data/addons/airflow.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
selector:
|
||||
app: airflow
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airflow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airflow
|
||||
spec:
|
||||
containers:
|
||||
- name: airflow
|
||||
image: ${AIRFLOW_IMAGE}
|
||||
args: ["standalone"]
|
||||
env:
|
||||
- name: AIRFLOW__CORE__EXECUTOR
|
||||
value: LocalExecutor
|
||||
- name: AIRFLOW__CORE__LOAD_EXAMPLES
|
||||
value: "false"
|
||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: SQL_ALCHEMY_CONN}
|
||||
- name: AIRFLOW_CONN_APP_DB
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: AIRFLOW_CONN_APP_DB}
|
||||
- name: AIRFLOW__CORE__FERNET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||
- name: _AIRFLOW_WWW_USER_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_USER}
|
||||
- name: _AIRFLOW_WWW_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: dags
|
||||
mountPath: /opt/airflow/dags
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
# First boot runs the whole migration before it serves anything.
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
volumes:
|
||||
- name: dags
|
||||
configMap:
|
||||
name: airflow-dags
|
||||
optional: true
|
||||
@@ -2,6 +2,7 @@
|
||||
# PostgreSQL for this overlay: plain manifests, one replica on a PVC, password generated once and kept.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
@@ -26,77 +27,7 @@ else
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-data
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: ${POSTGRES_STORAGE:-2Gi}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
# One volume, one writer. Rolling would start a second pod against the same
|
||||
# PVC before the first exits, and Postgres refuses to share a data directory.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: ${POSTGRES_IMAGE}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres
|
||||
env:
|
||||
# The image initialises into the volume root otherwise, and a
|
||||
# lost+found from the PVC makes it refuse to initdb.
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
YAML
|
||||
render_manifest "$ADDON_DIR/postgres.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for postgres..."
|
||||
$K rollout status deployment/postgres -n "$NS" --timeout=240s
|
||||
|
||||
69
rig/examples/data/addons/postgres.yaml
Normal file
69
rig/examples/data/addons/postgres.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-data
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: ${POSTGRES_STORAGE:-2Gi}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
# One volume, one writer. Rolling would start a second pod against the same
|
||||
# PVC before the first exits, and Postgres refuses to share a data directory.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: ${POSTGRES_IMAGE}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres
|
||||
env:
|
||||
# The image initialises into the volume root otherwise, and a
|
||||
# lost+found from the PVC makes it refuse to initdb.
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
@@ -2,6 +2,7 @@
|
||||
# Redis for this overlay: cache and broker (Celery), no persistence.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
@@ -13,43 +14,7 @@ NS="${DATA_NAMESPACE:-data}"
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: ${REDIS_IMAGE}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
YAML
|
||||
render_manifest "$ADDON_DIR/redis.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for redis..."
|
||||
$K rollout status deployment/redis -n "$NS" --timeout=180s
|
||||
|
||||
35
rig/examples/data/addons/redis.yaml
Normal file
35
rig/examples/data/addons/redis.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: ${REDIS_IMAGE}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
@@ -7,6 +7,9 @@
|
||||
# LocalExecutor and needs no broker: add redis (before airflow) only to switch to Celery.
|
||||
ADDONS="metallb postgres airflow"
|
||||
|
||||
# What each part costs in MB, for `make check`'s memory budget (rig adds the kind node).
|
||||
MEMORY_BUDGET="airflow=1800 postgres=300"
|
||||
|
||||
# Namespace for the dependency containers (k8s/base/kustomization.yaml names it too).
|
||||
DATA_NAMESPACE=data
|
||||
|
||||
|
||||
@@ -61,6 +61,32 @@ render_kind_config ()
|
||||
fi;
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" -e "s|\${OVERLAY_DIR}|${overlay_dir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
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};
|
||||
else
|
||||
if [[ $ref == *:-* ]]; then
|
||||
val=$def;
|
||||
else
|
||||
echo "render_manifest: $file needs \${$key}, which nothing sets" 1>&2;
|
||||
return 1;
|
||||
fi;
|
||||
fi;
|
||||
text=${text//"$ref"/"$val"};
|
||||
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u);
|
||||
printf '%s\n' "$text"
|
||||
}
|
||||
|
||||
# ── configuration, frozen for profile 'default' ──
|
||||
load_config() {
|
||||
|
||||
@@ -61,6 +61,32 @@ render_kind_config ()
|
||||
fi;
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" -e "s|\${OVERLAY_DIR}|${overlay_dir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
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};
|
||||
else
|
||||
if [[ $ref == *:-* ]]; then
|
||||
val=$def;
|
||||
else
|
||||
echo "render_manifest: $file needs \${$key}, which nothing sets" 1>&2;
|
||||
return 1;
|
||||
fi;
|
||||
fi;
|
||||
text=${text//"$ref"/"$val"};
|
||||
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u);
|
||||
printf '%s\n' "$text"
|
||||
}
|
||||
|
||||
# ── configuration, frozen for profile 'default' ──
|
||||
load_config() {
|
||||
|
||||
Reference in New Issue
Block a user