cka-sim/setup.sh

230 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bootstraps the kind cluster(s) for the exam tracks on podman and preloads images.
# Each exam runs on one Kubernetes minor, and each minor gets its own cluster (1 control-plane +
# 3 workers), so you practise on the version you will sit:
# ./setup.sh [cka|ckad|cks|all] [--recreate]
# cka, cks -> cka-sim-135 (Kubernetes v1.35) ckad -> cka-sim-137 (Kubernetes v1.37)
# With no track it sets up the one ./exam defaults to ($CKA_TRACK, else cka). Idempotent: an
# existing cluster is reused (its nodes restarted if stopped); --recreate rebuilds one whose
# version is wrong. `./exam cluster-env` is the single source of the track -> version -> image map.
# Override with CONTAINER_RUNTIME=docker if you ever want docker instead.
set -euo pipefail
cd "$(dirname "$0")"
RT="${CONTAINER_RUNTIME:-podman}"
need() {
command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' is not installed. $2"; exit 1; }
}
need "$RT" "https://podman.io/docs/installation"
need kind "https://kind.sigs.k8s.io/docs/user/quick-start/#installation"
need kubectl "https://kubernetes.io/docs/tasks/tools/"
need python3 ""
tracks=() recreate=0
while [ $# -gt 0 ]; do
case "$1" in
cka|ckad|cks) tracks+=("$1") ;;
all) tracks+=(cka ckad cks) ;;
--track) tracks+=("${2:?--track needs a value}"); shift ;;
--recreate) recreate=1 ;;
-h|--help) sed -n '2,10p' "$(basename "$0")" | sed 's/^# \{0,1\}//'; exit 0 ;; # cwd is the script dir
*) echo "unknown argument '$1'. usage: ./setup.sh [cka|ckad|cks|all] [--recreate]"; exit 1 ;;
esac
shift
done
[ ${#tracks[@]} -gt 0 ] || tracks=("${CKA_TRACK:-cka}")
if [ "$RT" = podman ]; then
export KIND_EXPERIMENTAL_PROVIDER=podman
podman info >/dev/null 2>&1 || { echo "ERROR: podman is not working (try: podman info)."; exit 1; }
# kind needs cgroup v2 with controller delegation when podman is rootless.
if [ "$(id -u)" != 0 ] && [ "$(podman info --format '{{.Host.CgroupsVersion}}' 2>/dev/null)" != "v2" ]; then
echo "ERROR: rootless podman needs cgroup v2 for kind. Either enable cgroup v2 or run setup as root (sudo -E ./setup.sh)."
exit 1
fi
# inotify limits are the #1 cause of pods stuck in ContainerCreating on multi-node kind
for k in fs.inotify.max_user_watches:524288 fs.inotify.max_user_instances:512; do
key=${k%%:*}; want=${k##*:}; have=$(sysctl -n "$key" 2>/dev/null || echo 0)
[ "$have" -ge "$want" ] || echo "warn: $key=$have (recommend >= $want: sudo sysctl $key=$want)"
done
else
docker info >/dev/null 2>&1 || { echo "ERROR: docker daemon is not running/reachable."; exit 1; }
fi
# a session in progress keeps its kubectl context and kubectl version (kind create switches the context)
prev_ctx=$(kubectl config current-context 2>/dev/null || true)
open_session=$(python3 -c 'import glob, json
for f in glob.glob(".state/state-*.json"):
try:
if not json.load(open(f)).get("ended_at"): print(f[13:-5])
except Exception: pass' | head -1)
# track -> cluster, deduplicated: CKA and CKS share the v1.35 cluster
clusters=() first_ctx=""
for t in "${tracks[@]}"; do
env_lines=$(./exam --track "$t" cluster-env) || exit 1
eval "$env_lines" # TRACK K8S CLUSTER NODE_IMAGE
[ -n "$NODE_IMAGE" ] || { echo "ERROR: no kind node image is pinned for Kubernetes $K8S (KIND_IMAGES in ./exam)."; exit 1; }
[ -n "$first_ctx" ] || first_ctx="kind-$CLUSTER"
case " ${clusters[*]-} " in *" $CLUSTER|"*) ;; *) clusters+=("$CLUSTER|$K8S|$NODE_IMAGE") ;; esac
done
# kubectl at the cluster's own version, as the real exam gives you: .state/kubectl/<minor>/kubectl.
# ./exam points .state/kubectl/current at the right one per session and examrc puts it on PATH.
# On Linux it comes out of the control-plane node, which ships exactly that kubectl (offline, no
# checksum needed); elsewhere the node's Linux binary will not run, so it is downloaded.
fetch_kubectl() {
local ver=$1 dir=.state/kubectl/$2 cp=$3 os arch url tmp want got
if [ -x "$dir/kubectl" ] && "$dir/kubectl" version --client 2>/dev/null | grep -q "Client Version: $ver\$"; then
echo " kubectl $ver already in $dir"; return 0
fi
os=$(uname -s | tr '[:upper:]' '[:lower:]'); arch=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
mkdir -p "$dir"; tmp=$(mktemp "$dir/.kubectl.XXXXXX")
if [ "$os" = linux ] && "$RT" cp "$cp:/usr/bin/kubectl" "$tmp" >/dev/null 2>&1 && chmod +x "$tmp" \
&& "$tmp" version --client 2>/dev/null | grep -q "Client Version: $ver\$"; then
mv -f "$tmp" "$dir/kubectl"; echo " kubectl $ver (from $cp) -> $dir/kubectl"; return 0
fi
url="https://dl.k8s.io/release/$ver/bin/$os/$arch/kubectl"
want=$(curl -sfL --max-time 30 "$url.sha256" || true)
if [ -n "$want" ] && curl -sfL --max-time 300 "$url" -o "$tmp"; then
got=$( (sha256sum "$tmp" 2>/dev/null || shasum -a 256 "$tmp") | awk '{print $1}')
if [ "$got" = "$want" ]; then
chmod +x "$tmp"; mv -f "$tmp" "$dir/kubectl"; echo " kubectl $ver -> $dir/kubectl"; return 0
fi
echo " warn: kubectl $ver checksum mismatch, discarded"
fi
rm -f "$tmp"
echo " warn: could not download kubectl $ver - your own kubectl ($(kubectl version --client 2>/dev/null | head -1)) will be used"
}
create() {
kind create cluster --name "$1" --config kind-config.yaml --image "${KIND_NODE_IMAGE:-$2}" --wait 180s
}
setup_cluster() {
local cluster=$1 k8s=$2 image=$3 ctx="kind-$1" cp="$1-control-plane" ver
echo
if kind get clusters 2>/dev/null | grep -qx "$cluster"; then
# rootless podman does not bring node containers back after a reboot or logout
for n in $(kind get nodes --name "$cluster"); do
[ "$("$RT" inspect -f '{{.State.Running}}' "$n" 2>/dev/null)" = true ] || { echo "==> starting stopped node $n"; "$RT" start "$n" >/dev/null; }
done
ver=$("$RT" exec "$cp" cat /kind/version 2>/dev/null || echo unknown)
case "$ver" in
"$k8s".*) echo "==> kind cluster '$cluster' already exists (Kubernetes $ver), reusing it." ;;
*)
if [ "$recreate" != 1 ]; then
echo "ERROR: kind cluster '$cluster' runs Kubernetes $ver, but its tracks need $k8s."
echo " Rebuild it (this deletes everything on it): ./setup.sh ${tracks[*]} --recreate"
exit 1
fi
echo "==> '$cluster' runs $ver, not $k8s - recreating it"
kind delete cluster --name "$cluster"
create "$cluster" "$image"
;;
esac
else
echo "==> Creating kind cluster '$cluster' (Kubernetes $k8s) on $RT (1 control-plane + 3 workers)..."
create "$cluster" "$image"
fi
echo "==> Waiting for nodes..."
# `kubectl wait` gives up on a refused connection, and restarted nodes need 15-40 s before the
# API server answers again - wait for it first
local deadline=$((SECONDS + 180))
until kubectl --context "$ctx" get --raw=/readyz >/dev/null 2>&1; do
[ $SECONDS -lt $deadline ] || { echo "ERROR: the API server of '$cluster' did not come back within 180 s."; exit 1; }
sleep 3
done
kubectl --context "$ctx" wait --for=condition=Ready nodes --all --timeout=180s
echo "==> Preloading images into the cluster (non-fatal if a pull fails)..."
# `kind load docker-image` shells out to the docker CLI internally no matter which
# provider the cluster uses, so it can't see images that only exist in podman's
# store. Save to a tar and use `kind load image-archive` instead, which is
# runtime-agnostic.
for img in nginx:1.25 nginx:1.27 busybox:1.36 redis:7-alpine; do
if "$RT" pull -q "docker.io/library/$img" >/dev/null 2>&1; then
tarfile="$(mktemp)"
if "$RT" save -o "$tarfile" "docker.io/library/$img" >/dev/null 2>&1 \
&& kind load image-archive --name "$cluster" "$tarfile" >/dev/null 2>&1; then
echo " loaded $img"
else
echo " warn: could not load $img"
fi
rm -f "$tarfile"
else
echo " warn: could not pull $img (tasks will pull at runtime)"
fi
done
echo "==> Making sure the exam workdir exists on the control-plane node..."
"$RT" exec "$cp" mkdir -p /opt/cka
# The real exam gives you vim (and less/jq) on every node; the kind node image is minimal, so
# install them once here. Skip with CKA_SKIP_NODE_TOOLS=1.
if [ -z "${CKA_SKIP_NODE_TOOLS:-}" ]; then
echo "==> Installing editors and tools on the nodes (vim, nano, less, jq)..."
for n in $(kind get nodes --name "$cluster"); do
if "$RT" exec "$n" sh -c 'command -v vim >/dev/null'; then
echo " $n already has vim"
continue
fi
if "$RT" exec "$n" sh -c 'DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq vim nano less jq >/dev/null 2>&1'; then
"$RT" exec "$n" sh -c 'printf "set tabstop=2\nset expandtab\nset shiftwidth=2\n" > /root/.vimrc'
echo " $n ok"
else
echo " warn: could not install tools on $n (no outbound network?). Fall back to:"
echo " $RT cp $n:/path/file /tmp/f && \$EDITOR /tmp/f && $RT cp /tmp/f $n:/path/file"
fi
done
fi
# The exam expects etcdctl/etcdutl on the control-plane node, like the real one. They cannot be
# copied out of the running etcd pod (that image is distroless), so install them properly.
echo "==> Installing etcdctl/etcdutl on the control-plane node..."
printf ' '
CONTAINER_RUNTIME="$RT" CKA_CP="$cp" bash ./etcd-tools.sh || echo " warn: no etcd client on $cp; Q02/Q45 will say so when they seed"
echo "==> Fetching kubectl to match the cluster..."
fetch_kubectl "$("$RT" exec "$cp" cat /kind/version)" "$k8s" "$cp"
echo
echo "Cluster '$cluster' ready (context $ctx). Nodes:"
kubectl --context "$ctx" get nodes -o wide
}
for c in "${clusters[@]}"; do
IFS='|' read -r cluster k8s image <<<"$c"
setup_cluster "$cluster" "$k8s" "$image"
done
# remember the runtime so the CLI uses the matching exec command
mkdir -p .state && echo "$RT" > .state/runtime
# `kind create cluster` switches the current context to each new cluster; land on the first track's,
# with its kubectl (./exam re-points both at every session start) - unless a session is open, which
# keeps what it had
first_k8s=$(./exam --track "${tracks[0]}" cluster-env | sed -n 's/^K8S=//p')
mkdir -p .state/kubectl
if [ -n "$open_session" ]; then
[ -n "$prev_ctx" ] && kubectl config use-context "$prev_ctx" >/dev/null
echo "note: a $open_session session is still open, so your kubectl context stays $prev_ctx."
elif kubectl config use-context "$first_ctx" >/dev/null && [ -x ".state/kubectl/$first_k8s/kubectl" ]; then
ln -sfn "$first_k8s" .state/kubectl/current
else # no download: current/ -> your own kubectl, so the path examrc puts on PATH always exists
own=$(PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -v '/\.state/kubectl' | paste -sd: -) command -v kubectl)
mkdir -p .state/kubectl/host && ln -sfn "$(realpath "$own")" .state/kubectl/host/kubectl && ln -sfn host .state/kubectl/current
fi
if kind get clusters 2>/dev/null | grep -qx cka-sim; then
echo
echo "note: the old single cluster 'cka-sim' (from before each exam got its own version) still exists."
echo " Nothing uses it now; free its ~4 GB with: ./teardown.sh legacy"
fi
chmod +x exam
echo
echo "Next: ./exam --track ${tracks[0]} web (browser UI) or ./exam --track ${tracks[0]} drill (5 tasks from your least-drilled domain)"
echo " ./exam clusters which cluster and Kubernetes version each track uses"