#!/usr/bin/env bash # Cluster lifecycle, plus what else is running on this machine. # # `list` and `free` live here rather than in a separate script because a # near-identical second name (cluster / clusters) is a trap — you reach for one # and get the other. One target, one file, unambiguous subcommands. # # "Idempotent" here means CONVERGENT, not "exits early if the cluster exists". # That distinction matters: an interrupted first run can leave a cluster created # but not finished, and returning early on the re-run would strand it there. # The create step is conditional; every step after it always runs, and each one # is individually idempotent. # # Usage: cluster.sh up | down | reset | list | free set -euo pipefail cd "$(dirname "$0")" source ./lib/config.sh load_config up() { if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then echo "cluster '$CLUSTER' exists — converging" else # Say what this profile locks in BEFORE spending minutes building it: # the kind config is fixed at creation and cannot be changed later. echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'" echo " kind config ${KIND_CONFIG}" echo " nodes $NODES" echo " image $NODE_IMAGE" echo " ingress $INGRESS_MODE" echo " registry $REGISTRY_MODE" echo " (fixed at creation — edit the kind config, then 'make cluster reset')" echo render_kind_config | kind create cluster --config - fi # The cluster can exist while its context does not — a reset or a switched # KUBECONFIG loses it, and then nothing works despite a healthy cluster. if ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$KUBECONTEXT"; then echo "context '$KUBECONTEXT' missing from kubeconfig — re-exporting" kind export kubeconfig --name "$CLUSTER" fi kubectl config use-context "$KUBECONTEXT" >/dev/null bash registry.sh up if [ -n "${ADDONS// /}" ]; then bash addons.sh install fi echo echo "cluster '$CLUSTER' ready (context $KUBECONTEXT)" } down() { # The registry is a standalone container outside the cluster; take it down # first so a reset doesn't leave it orphaned and holding a port. bash registry.sh down || true if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then echo "deleting cluster '$CLUSTER'..." kind delete cluster --name "$CLUSTER" else echo "no cluster '$CLUSTER' to delete" fi } # The escape hatch for a wedged cluster, and the only way to change a # creation-time setting such as the node count or port mappings. reset() { down echo up } # ── the whole machine ────────────────────────────────────────────────────── # Every cluster is a running container tree whether or not you are using it, and # an idle one is the usual reason a new one will not fit. list() { local total avail total=$(awk '/^MemTotal:/{printf "%.1f", $2/1024/1024}' /proc/meminfo) avail=$(awk '/^MemAvailable:/{printf "%.1f", $2/1024/1024}' /proc/meminfo) echo "memory: ${avail} GB available of ${total} GB" echo local names; names=$(kind get clusters 2>/dev/null || true) if [ -z "$names" ]; then echo "no clusters" return fi printf "%-16s %-10s %8s %6s %-13s %s\n" CLUSTER STATE MEM NODES PORTS "" local c nodes state mem base for c in $names; do nodes=$(docker ps -a --filter "label=io.x-k8s.kind.cluster=$c" --format '{{.Names}}' | wc -l) state=$(docker inspect -f '{{.State.Status}}' "${c}-control-plane" 2>/dev/null || echo unknown) if [ "$state" = "running" ]; then mem=$(docker stats --no-stream --format '{{.MemUsage}}' \ $(docker ps --filter "label=io.x-k8s.kind.cluster=$c" -q) 2>/dev/null \ | awk '{gsub(/GiB/,"");gsub(/MiB/,"e-3");s+=$1} END {printf "%.1fG", s}') else mem="-" fi # A cluster's name is its directory slug, so its port block is derivable # here without reading that directory's config. base=$(derive_port_base "$c") printf "%-16s %-10s %8s %6s %-13s %s\n" "$c" "$state" "$mem" "$nodes" \ "${base}-$((base + 3))" \ "$([ "$c" = "$CLUSTER" ] && echo "<- this one")" done } # Stop the OTHER clusters to free memory. Stops, never deletes — a stopped # cluster restarts with `docker start`, so nothing is lost. free() { local targets=("$@") if [ ${#targets[@]} -eq 0 ]; then mapfile -t targets < <(kind get clusters 2>/dev/null | grep -vx "$CLUSTER" || true) fi if [ ${#targets[@]} -eq 0 ]; then echo "nothing to stop" return fi local c ids for c in "${targets[@]}"; do ids=$(docker ps --filter "label=io.x-k8s.kind.cluster=$c" -q) if [ -z "$ids" ]; then echo "cluster '$c' is not running" continue fi echo "stopping '$c' (restart with: docker start \$(docker ps -aq -f label=io.x-k8s.kind.cluster=$c))" # shellcheck disable=SC2086 docker stop $ids >/dev/null done } case "${1:-up}" in up) up ;; down) down ;; reset) reset ;; list) list ;; free) shift; free "$@" ;; *) echo "usage: $0 [up|down|reset|list|free]" >&2; exit 1 ;; esac