230 lines
11 KiB
Bash
Executable File
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"
|