224 lines
8.3 KiB
Bash
Executable File
224 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The rig bundle: generate it, deploy it, tear it down, find it.
|
|
#
|
|
# Usage: bundle.sh manifest | up | down | status | url | list | dev
|
|
#
|
|
# What `up` proves, in order: kind installed and a cluster exists, MetalLB can
|
|
# hand out an address, a Service of type LoadBalancer actually resolves, and a
|
|
# pod serves the bundle listing. If all of that works the installation is sound,
|
|
# and the only thing missing is the real architecture.
|
|
#
|
|
# ONE ARTIFACT
|
|
# `up` applies generated/<slug>.yaml — the same self-contained file you would
|
|
# hand to an external cluster. There is no separate local path, so what works
|
|
# here cannot quietly differ from the master deployment applied elsewhere.
|
|
#
|
|
# ONE CLUSTER, SEVERAL RIGS
|
|
# Identity follows the FOLDER NAME, exactly as rig's cluster identity does. This
|
|
# directory deploys into a namespace named after itself, so copying it to
|
|
# corporate-rig/ yields a second rig in the SAME local cluster with no edits and
|
|
# no collisions — different namespace, its own MetalLB address. `list` shows all
|
|
# of them. The cluster itself is rig's business; this only ever owns a namespace.
|
|
#
|
|
# MetalLB is installed by calling rig's own addon script rather than
|
|
# reimplementing it — deriving the pool from the kind Docker network is the
|
|
# fiddly part and there should be exactly one copy of it.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
BUNDLE_ROOT="$(pwd)"
|
|
RIG_CTRL="$(cd .. && pwd)/ctrl"
|
|
|
|
# The containing folder's name, reduced to a DNS label (same rule as rig's
|
|
# default_cluster_name and ctrl/manifest.py, so all three agree on the slug).
|
|
slug() {
|
|
local n
|
|
n=$(basename "$BUNDLE_ROOT")
|
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
|
echo "${n:-rig-bundle}"
|
|
}
|
|
NS="$(slug)"
|
|
ARTIFACT="generated/${NS}.yaml"
|
|
|
|
# Resolved lazily, not at load time: `manifest` and `dev` deliberately work
|
|
# with no cluster and no kubectl at all, and a top-level check would break that.
|
|
#
|
|
# Follows whatever context rig's cluster.sh selected, so this bundle works in a
|
|
# copied-and-renamed environment without being told which cluster it is in.
|
|
init_kube() {
|
|
KUBECONTEXT="${KUBECONTEXT:-$(kubectl config current-context 2>/dev/null || true)}"
|
|
if [ -z "$KUBECONTEXT" ]; then
|
|
echo "no kubectl context — bring a cluster up first: (cd .. && make cluster up)" >&2
|
|
exit 1
|
|
fi
|
|
KCTX="kubectl --context ${KUBECONTEXT}"
|
|
K="kubectl --context ${KUBECONTEXT} --namespace ${NS}"
|
|
}
|
|
|
|
require_cluster() {
|
|
if ! $KCTX cluster-info >/dev/null 2>&1; then
|
|
echo "context '$KUBECONTEXT' does not reach a cluster" >&2
|
|
echo "bring one up: (cd .. && make cluster up)" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
ensure_metallb() {
|
|
if $KCTX get deployment -n metallb-system controller >/dev/null 2>&1; then
|
|
echo "metallb: present"
|
|
return 0
|
|
fi
|
|
|
|
# Only kind needs it. On a real cluster the cloud load balancer answers a
|
|
# `type: LoadBalancer` Service, and installing MetalLB there would be wrong.
|
|
case "$KUBECONTEXT" in
|
|
kind-*) ;;
|
|
*)
|
|
echo "metallb: skipped — '$KUBECONTEXT' is not a kind context"
|
|
echo " (a cloud load balancer answers LoadBalancer services there)"
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
if [ ! -f "$RIG_CTRL/addons/metallb.sh" ]; then
|
|
echo "metallb is not installed and rig's addon script was not found at" >&2
|
|
echo " $RIG_CTRL/addons/metallb.sh" >&2
|
|
echo "a Service of type LoadBalancer will sit at <pending> without it." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# rig's addons derive their target cluster from RIG'S OWN folder name via
|
|
# load_config, so left alone this bundle would install into `kind-rig` —
|
|
# a cluster that need not exist — while deploying everything else into the
|
|
# context actually selected. CLUSTER is in load_config's overridable set,
|
|
# so passing it here points the addon at the same cluster we are using.
|
|
local target="${KUBECONTEXT#kind-}"
|
|
echo "metallb: installing via rig's addon into '$target'"
|
|
CLUSTER="$target" bash "$RIG_CTRL/addons/metallb.sh"
|
|
}
|
|
|
|
# Regenerate the artifact. No cluster and no kubectl required — this is the step
|
|
# a staging workstation runs before anything is installed.
|
|
manifest() {
|
|
mkdir -p generated
|
|
python3 ctrl/manifest.py "$NS" > "$ARTIFACT"
|
|
echo "wrote $ARTIFACT ($(wc -l < "$ARTIFACT") lines)"
|
|
echo " applies as-is anywhere: kubectl apply -f ${BUNDLE_ROOT}/${ARTIFACT}"
|
|
}
|
|
|
|
up() {
|
|
manifest
|
|
init_kube
|
|
require_cluster
|
|
ensure_metallb
|
|
|
|
echo
|
|
echo "applying '${NS}' to context '${KUBECONTEXT}'"
|
|
$KCTX apply -f "$ARTIFACT"
|
|
|
|
# `rollout status` does not work on a bare Pod — it only understands
|
|
# Deployments, StatefulSets and DaemonSets. Wait on the condition instead.
|
|
# This is the slow step: the container npm-installs before vite serves.
|
|
echo "waiting for the pod to be ready (npm install runs first)..."
|
|
$K wait --for=condition=Ready pod/rig-ui --timeout=300s
|
|
echo
|
|
url
|
|
}
|
|
|
|
down() {
|
|
init_kube
|
|
# Delete the namespace and everything in it goes with it. Scoped to THIS
|
|
# rig — a sibling rig in the same cluster is untouched.
|
|
$KCTX delete namespace "$NS" --ignore-not-found
|
|
echo "'${NS}' removed (cluster, metallb and any sibling rig are left alone)"
|
|
}
|
|
|
|
status() {
|
|
init_kube
|
|
require_cluster
|
|
if ! $KCTX get namespace "$NS" >/dev/null 2>&1; then
|
|
echo "'${NS}' is not deployed — run: make up"
|
|
return 0
|
|
fi
|
|
$K get pod,svc,configmap -o wide
|
|
}
|
|
|
|
# Every rig in this cluster, not just this one — the point of the namespace
|
|
# split is that several coexist, so there has to be a way to see them together.
|
|
list() {
|
|
init_kube
|
|
require_cluster
|
|
local names
|
|
names=$($KCTX get namespace -l rig.bundle/name \
|
|
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true)
|
|
if [ -z "$names" ]; then
|
|
echo "no rigs deployed in context '${KUBECONTEXT}'"
|
|
return 0
|
|
fi
|
|
printf "%-20s %-16s %s\n" RIG ADDRESS ""
|
|
local n ip
|
|
for n in $names; do
|
|
ip=$($KCTX -n "$n" get svc rig-ui \
|
|
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
|
|
printf "%-20s %-16s %s\n" "$n" "${ip:-<pending>}" \
|
|
"$([ "$n" = "$NS" ] && echo '<- this one')"
|
|
done
|
|
}
|
|
|
|
# The address MetalLB (or a cloud load balancer) assigned. <pending> here is the
|
|
# classic silent failure: everything reports healthy and nothing is reachable.
|
|
url() {
|
|
init_kube
|
|
local ip
|
|
ip=$($K get svc rig-ui \
|
|
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
|
|
if [ -z "$ip" ]; then
|
|
ip=$($K get svc rig-ui \
|
|
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$ip" ]; then
|
|
echo "no external address yet — nothing has assigned one."
|
|
echo "on kind: kubectl --context $KUBECONTEXT -n metallb-system get pods"
|
|
return 1
|
|
fi
|
|
echo "IT WORKS -> http://${ip}/"
|
|
echo " bundle http://${ip}/bundle.json"
|
|
}
|
|
|
|
# Run the UI locally with no cluster at all — the fast way to iterate on
|
|
# bundle.json. Same vite command the pod runs, so what you see here is what
|
|
# gets served there.
|
|
dev() {
|
|
if ! command -v npm >/dev/null 2>&1; then
|
|
echo "npm not found — the UI needs node locally for this." >&2
|
|
echo "(in-cluster it runs on the node:22-alpine image instead)" >&2
|
|
exit 1
|
|
fi
|
|
# bundle.json lives one level up so it stays the rig's data rather than the
|
|
# app's; vite serves public/ at the root, which is where the app fetches it.
|
|
mkdir -p rig-ui/public
|
|
cp bundle.json rig-ui/public/bundle.json
|
|
|
|
# The mocked cluster is a DEMO asset and is deliberately not embedded in the
|
|
# deployed artifact — on a real rig the UI would then show canned values
|
|
# beside a live cluster, which is precisely the lie its banner warns about.
|
|
# It is served here, and in the static build for the public UI-only page.
|
|
cp cluster.mock.json rig-ui/public/cluster.mock.json
|
|
|
|
cd rig-ui
|
|
[ -d node_modules ] || npm install --no-audit --no-fund
|
|
VITE_RIG_NAME="$NS" npm run dev
|
|
}
|
|
|
|
case "${1:-status}" in
|
|
manifest) manifest ;;
|
|
up) up ;;
|
|
down) down ;;
|
|
status) status ;;
|
|
url) url ;;
|
|
list) list ;;
|
|
dev) dev ;;
|
|
*) echo "usage: $0 [manifest|up|down|status|url|list|dev]" >&2; exit 1 ;;
|
|
esac
|