#!/bin/sh set -eu CONFIG_DIR="${CONFIG_DIR:-/opt/monok8s/config}" CLUSTER_ENV="${CONFIG_DIR}/cluster.env" KUBEADM_CONFIG_OUT="${KUBEADM_CONFIG_OUT:-/tmp/kubeadm-init.yaml}" log() { echo "[monok8s] $*" } fail() { echo "[monok8s] ERROR: $*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" } require_file() { [ -f "$1" ] || fail "required file not found: $1" } load_config() { require_file "$CLUSTER_ENV" # shellcheck disable=SC1090 . "$CLUSTER_ENV" : "${KUBERNETES_VERSION:?KUBERNETES_VERSION is required}" : "${NODE_NAME:?NODE_NAME is required}" : "${APISERVER_ADVERTISE_ADDRESS:?APISERVER_ADVERTISE_ADDRESS is required}" POD_SUBNET="${POD_SUBNET:-10.244.0.0/16}" SERVICE_SUBNET="${SERVICE_SUBNET:-10.96.0.0/12}" CLUSTER_NAME="${CLUSTER_NAME:-monok8s}" CLUSTER_DOMAIN="${CLUSTER_DOMAIN:-cluster.local}" CONTAINER_RUNTIME_ENDPOINT="${CONTAINER_RUNTIME_ENDPOINT:-unix:///var/run/crio/crio.sock}" SANS="${SANS:-}" ALLOW_SCHEDULING_ON_CONTROL_PLANE="${ALLOW_SCHEDULING_ON_CONTROL_PLANE:-yes}" SKIP_IMAGE_CHECK="${SKIP_IMAGE_CHECK:-no}" KUBECONFIG_USER_HOME="${KUBECONFIG_USER_HOME:-/root}" } check_prereqs() { need_cmd kubeadm need_cmd kubelet need_cmd kubectl need_cmd crictl need_cmd rc-service need_cmd awk need_cmd grep need_cmd sed need_cmd hostname [ -S /var/run/crio/crio.sock ] || fail "CRI-O socket not found at /var/run/crio/crio.sock" } set_hostname_if_needed() { current_hostname="$(hostname 2>/dev/null || true)" if [ "$current_hostname" != "$NODE_NAME" ]; then log "setting hostname to $NODE_NAME" hostname "$NODE_NAME" mkdir -p /etc printf '%s\n' "$NODE_NAME" > /etc/hostname # Keep localhost mapping sane even without DNS. if [ -f /etc/hosts ]; then if ! grep -Eq "[[:space:]]$NODE_NAME([[:space:]]|\$)" /etc/hosts; then printf '127.0.0.1\tlocalhost %s\n' "$NODE_NAME" >> /etc/hosts fi else cat > /etc/hosts < /proc/sys/net/ipv4/ip_forward fi mkdir -p /etc/sysctl.d cat > /etc/sysctl.d/99-monok8s.conf <<'EOF' net.ipv4.ip_forward = 1 EOF } ensure_kubelet_binary() { if command -v kubelet >/dev/null 2>&1; then return 0 fi if [ -x /usr/local/bin/kubelet ]; then export PATH="/usr/local/bin:$PATH" return 0 fi if [ -x /opt/cni/bin/kubelet ]; then export PATH="/opt/cni/bin:$PATH" return 0 fi fail "kubelet binary not found; install kubelet into /usr/local/bin or make sure PATH includes it" } ensure_kubelet_service() { if ! rc-service kubelet status >/dev/null 2>&1; then if [ ! -f /etc/init.d/kubelet ]; then fail "kubelet OpenRC service does not exist at /etc/init.d/kubelet" fi log "starting kubelet service" rc-service kubelet start || fail "failed to start kubelet" fi } check_crio_running() { log "checking CRI-O status..." if ! rc-service crio status >/dev/null 2>&1; then fail "crio service is not running" fi if ! crictl --runtime-endpoint "$CONTAINER_RUNTIME_ENDPOINT" info >/dev/null 2>&1; then fail "crictl cannot talk to CRI-O via $CONTAINER_RUNTIME_ENDPOINT" fi log "CRI-O is up" } normalize_image_ref() { # Add docker.io/library/ for bare images like pause:3.10 if needed. # kubeadm usually emits full refs already, so this is just defensive. ref="$1" case "$ref" in */* ) echo "$ref" ;; * ) echo "docker.io/library/$ref" ;; esac } image_present() { wanted="$1" repo="${wanted%:*}" tag="${wanted##*:}" crictl --runtime-endpoint "$CONTAINER_RUNTIME_ENDPOINT" images \ | awk 'NR>1 { print $1 ":" $2 }' \ | grep -Fx "$repo:$tag" >/dev/null 2>&1 } check_required_images() { [ "$SKIP_IMAGE_CHECK" = "yes" ] && { log "skipping image check (SKIP_IMAGE_CHECK=yes)" return 0 } log "checking required Kubernetes images for $KUBERNETES_VERSION..." missing_any=0 for img in $(kubeadm config images list --kubernetes-version "$KUBERNETES_VERSION"); do if image_present "$img"; then log "found image: $img" else echo "[monok8s] MISSING image: $img" >&2 missing_any=1 fi done [ "$missing_any" -eq 0 ] || fail "preload the Kubernetes images before bootstrapping" log "all required images are present" } check_node_name() { current_hostname="$(hostname)" if [ "$current_hostname" != "$NODE_NAME" ]; then log "warning: hostname is '$current_hostname' but NODE_NAME is '$NODE_NAME'" log "kubeadm will use NODE_NAME from config" fi } check_already_initialized() { if [ -f /etc/kubernetes/admin.conf ]; then fail "cluster already appears initialized (/etc/kubernetes/admin.conf exists)" fi } generate_kubeadm_config() { log "generating kubeadm config at $KUBEADM_CONFIG_OUT..." SAN_LINES="" if [ -n "${SANS:-}" ]; then old_ifs="$IFS" IFS=',' for san in $SANS; do san_trimmed="$(echo "$san" | sed 's/^ *//;s/ *$//')" [ -n "$san_trimmed" ] && SAN_LINES="${SAN_LINES} - \"${san_trimmed}\" " done IFS="$old_ifs" fi cat > "$KUBEADM_CONFIG_OUT" </dev/null 2>&1; then log "API server advertise address already present: $APISERVER_ADVERTISE_ADDRESS" return 0 fi warn_dummy_interface if ! command -v modprobe >/dev/null 2>&1; then fail "modprobe not found; cannot create dummy interface" fi log "loading dummy kernel module..." modprobe dummy || fail "failed to load dummy kernel module" if ! ip link show "$DUMMY_IFACE" >/dev/null 2>&1; then log "creating dummy interface: $DUMMY_IFACE" ip link add "$DUMMY_IFACE" type dummy || fail "failed to create dummy interface $DUMMY_IFACE" else log "dummy interface already exists: $DUMMY_IFACE" fi log "assigning ${APISERVER_ADVERTISE_ADDRESS}/${DUMMY_PREFIX_LEN} to $DUMMY_IFACE" ip addr add "${APISERVER_ADVERTISE_ADDRESS}/${DUMMY_PREFIX_LEN}" dev "$DUMMY_IFACE" 2>/dev/null || true log "bringing up $DUMMY_IFACE" ip link set "$DUMMY_IFACE" up || fail "failed to bring up $DUMMY_IFACE" if ! ip -o addr show dev "$DUMMY_IFACE" | awk '{print $4}' | cut -d/ -f1 | grep -Fx "$APISERVER_ADVERTISE_ADDRESS" >/dev/null 2>&1; then fail "dummy interface came up, but ${APISERVER_ADVERTISE_ADDRESS} is still not present" fi log "dummy interface ready: $DUMMY_IFACE -> ${APISERVER_ADVERTISE_ADDRESS}/${DUMMY_PREFIX_LEN}" } setup_local_kubectl() { kube_dir="${KUBECONFIG_USER_HOME}/.kube" log "setting up local kubectl config in ${kube_dir}/config..." mkdir -p "$kube_dir" cp /etc/kubernetes/admin.conf "${kube_dir}/config" chmod 600 "${kube_dir}/config" if [ "$KUBECONFIG_USER_HOME" = "/root" ]; then mkdir -p /etc/profile.d cat > /etc/profile.d/kubeconfig.sh <<'EOF' export KUBECONFIG=/root/.kube/config EOF chmod 644 /etc/profile.d/kubeconfig.sh fi } allow_single_node_scheduling() { if [ "$ALLOW_SCHEDULING_ON_CONTROL_PLANE" != "yes" ]; then log "leaving control-plane taint in place" return 0 fi log "removing control-plane taint so this single node can schedule workloads..." kubectl --kubeconfig /etc/kubernetes/admin.conf taint nodes "$NODE_NAME" node-role.kubernetes.io/control-plane- >/dev/null 2>&1 || true } print_next_steps() { cat <