#!/bin/bash set -e LOG() { echo -e "\033[36m[k8s-lab]\033[0m $*"; } OK() { echo -e "\033[32m[k8s-lab]\033[0m ✓ $*"; } ERR() { echo -e "\033[31m[k8s-lab]\033[0m ✗ $*" >&2; } LOG "Starting KubeKosh..." # ── 0. Fix cgroupv2 hierarchy (Docker Desktop / Mac) ───────────────────────── # cgroupv2 enforces the "no-internal-process constraint": a cgroup with domain # controllers (cpu, memory, etc.) cannot have processes AND child cgroups at the # same level. Docker Desktop places our container's processes in the root cgroup, # making it impossible for containerd/runc to create pod sub-cgroups (k8s.io). # # Fix (same as k3d): move all current processes to a leaf cgroup first, then # enable all available controllers in the root's subtree_control. if [ -f /sys/fs/cgroup/cgroup.controllers ]; then LOG "Configuring cgroupv2 delegation..." mkdir -p /sys/fs/cgroup/init # Move every process currently in the root cgroup into the leaf xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs 2>/dev/null || true # Enable all available controllers for child cgroups (e.g. k8s.io, kubepods) sed -e 's/ / +/g' -e 's/^/+/' \ < /sys/fs/cgroup/cgroup.controllers \ > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || true OK "cgroupv2 delegation configured" fi # ── 1. Start k3s server ────────────────────────────────────────────────────── LOG "Starting k3s (Kubernetes)..." # k3s needs cgroupv2 or cgroupv1 mounted; --disable flags slim it down for lab use k3s server \ --disable=traefik \ --disable=servicelb \ --write-kubeconfig-mode=644 \ --node-name=k8s-lab \ --snapshotter=native \ --kubelet-arg=cgroups-per-qos=false \ --kubelet-arg=enforce-node-allocatable="" \ &>/var/log/k3s.log & K3S_PID=$! # Wait for k3s API server to be ready KUBECONFIG_PATH=/etc/rancher/k3s/k3s.yaml for i in $(seq 1 60); do if [ -f "$KUBECONFIG_PATH" ] && \ kubectl --kubeconfig="$KUBECONFIG_PATH" get nodes &>/dev/null 2>&1; then break fi sleep 2 if [ $i -eq 60 ]; then ERR "k3s failed to start. Last log lines:" tail -20 /var/log/k3s.log >&2 exit 1 fi done OK "k3s API server is up" # Symlink kubeconfig to the standard location for convenience mkdir -p /root/.kube cp "$KUBECONFIG_PATH" /root/.kube/config export KUBECONFIG=/root/.kube/config # ── 2. Wait for node to be Ready ──────────────────────────────────────────── LOG "Waiting for cluster node to become Ready..." # Phase 1: wait until at least one node is registered # (kubectl wait --all exits immediately with error if no resources exist yet) # Stream k3s logs to stdout in background so failures are visible tail -f /var/log/k3s.log & TAIL_PID=$! for i in $(seq 1 90); do NODE_COUNT=$(kubectl get nodes --no-headers 2>/dev/null | wc -l) if [ "$NODE_COUNT" -gt 0 ]; then kill $TAIL_PID 2>/dev/null || true break fi sleep 3 if [ $i -eq 90 ]; then kill $TAIL_PID 2>/dev/null || true ERR "Timed out waiting for a node to register (270s)" ERR "k3s node status:" kubectl get nodes 2>&1 >&2 || true exit 1 fi done # Phase 2: wait for the node to reach Ready condition kubectl wait --for=condition=Ready nodes --all --timeout=120s OK "Cluster node is Ready" # Phase 3: wait for flannel CNI to write its subnet config. # Pods scheduled before flannel is ready get FailedCreatePodSandBox warnings # (missing /run/flannel/subnet.env). Waiting here avoids that noise. for i in $(seq 1 30); do [ -f /run/flannel/subnet.env ] && break sleep 1 done # ── 3. Install metrics-server ──────────────────────────────────────────────── # LOG "Installing metrics-server..." # kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml &>/dev/null || true # kubectl patch deployment metrics-server -n kube-system \ # --type='json' \ # -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' \ # &>/dev/null 2>&1 || true # OK "Metrics-server applied" # ── 4. Shell environment ───────────────────────────────────────────────────── LOG "Configuring shell environment..." cat >> /root/.bashrc << 'BASHRC' # KubeKosh aliases export KUBECONFIG=/root/.kube/config alias k='kubectl' alias kgp='kubectl get pods' alias kga='kubectl get pods --all-namespaces' alias kgd='kubectl get deployments' alias kgs='kubectl get services' alias kgn='kubectl get nodes' alias kgns='kubectl get namespaces' alias kdp='kubectl describe pod' alias kaf='kubectl apply -f' alias kdf='kubectl delete -f' alias kg='kubectl get' alias kd='kubectl describe' alias krm='kubectl delete' alias kex='kubectl exec -it' alias klogs='kubectl logs' # Useful functions kns() { kubectl config set-context --current --namespace="$1"; } kctx() { kubectl config use-context "$1"; } source <(kubectl completion bash) 2>/dev/null || true complete -F __start_kubectl k 2>/dev/null || true PS1='\[\033[01;32m\]\u@k8s-lab\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' echo "" echo " ⎈ KubeKosh - Node: k8s-lab" KUBECTL_VER=$(kubectl version --client 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[^ ]*' | head -1) echo " kubectl ${KUBECTL_VER}" echo " Aliases: k=kubectl, kgp=get pods, kaf=apply -f, kns=set-namespace, kgns=get namespaces, kex=kubectl exec -it" echo " kgd=get deployments, kgn=get nodes, kgs=get services, kdp=describe pod, krm=kubectl delete, klogs=kubectl logs" echo "" BASHRC OK "Shell configured" # ── 5. Start Node.js API server ────────────────────────────────────────────── LOG "Starting API server..." cd /app/backend && node server.js &>/var/log/api.log & OK "API server started (port 4000)" # ── 6. Browser terminal ────────────────────────────────────────────────────── # Terminal is served via WebSocket at /shell-ws by the Node.js API server # using node-pty — no external ttyd binary needed. # ── 7. Start nginx reverse proxy ──────────────────────────────────────────── LOG "Starting nginx proxy..." nginx -g 'daemon off;' &>/var/log/nginx.log & OK "nginx started (port 80)" # ── 8. Keep Alive & Graceful Shutdown ──────────────────────────────────────── cleanup() { LOG "Caught signal, shutting down KubeKosh..." kill -TERM "$K3S_PID" 2>/dev/null || true kill $(jobs -p) 2>/dev/null || true exit 0 } trap cleanup SIGINT SIGTERM LOG "══════════════════════════════════════════════════" LOG " KubeKosh is ready! → http://localhost:7554 " LOG "══════════════════════════════════════════════════" # Wait for background jobs. When a signal is caught, wait returns instantly and triggers cleanup. wait