penguin/monok8s

k8s image for Mono Gateway Dev Kit

alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh

raw ยท 10675 bytes

#!/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 <<EOF
127.0.0.1 localhost $NODE_NAME
EOF
        fi
    fi
}

ensure_ip_forward() {
    current="$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)"
    if [ "$current" != "1" ]; then
        log "enabling IPv4 forwarding"
        echo 1 > /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" <<EOF
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: ${APISERVER_ADVERTISE_ADDRESS}
  bindPort: 6443
nodeRegistration:
  name: ${NODE_NAME}
  criSocket: ${CONTAINER_RUNTIME_ENDPOINT}
  imagePullPolicy: IfNotPresent
  kubeletExtraArgs:
    - name: hostname-override
      value: "${NODE_NAME}"
    - name: node-ip
      value: "${APISERVER_ADVERTISE_ADDRESS}"
    - name: pod-manifest-path
      value: "/etc/kubernetes/manifests"
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
clusterName: ${CLUSTER_NAME}
kubernetesVersion: ${KUBERNETES_VERSION}
networking:
  podSubnet: ${POD_SUBNET}
  serviceSubnet: ${SERVICE_SUBNET}
  dnsDomain: ${CLUSTER_DOMAIN}
apiServer:
  certSANs:
    - "${APISERVER_ADVERTISE_ADDRESS}"
${SAN_LINES}---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: cgroupfs
containerRuntimeEndpoint: ${CONTAINER_RUNTIME_ENDPOINT}
EOF
}

run_kubeadm_init() {
    log "running kubeadm init..."
    kubeadm init --config "$KUBEADM_CONFIG_OUT"
}

warn_dummy_interface() {
    YELLOW="$(printf '\033[1;33m')"
    RESET="$(printf '\033[0m')"

    echo
    printf '%s' "$YELLOW"
    echo "[monok8s] WARNING: No suitable network interface found for API server address."
    echo "[monok8s]          Creating a temporary dummy interface for bootstrap."
    echo "[monok8s]          This configuration is intended for local/testing use only."
    echo "[monok8s]          Please configure a real network interface for production."
    printf '%s' "$RESET"
    echo
}

ensure_apiserver_address_present() {
    : "${APISERVER_ADVERTISE_ADDRESS:?APISERVER_ADVERTISE_ADDRESS is required}"

    DUMMY_IFACE="${DUMMY_IFACE:-dummy0}"
    DUMMY_PREFIX_LEN="${DUMMY_PREFIX_LEN:-32}"

    # If the address already exists on an interface, we're done.
    if ip -o addr show | awk '{print $4}' | cut -d/ -f1 | grep -Fx "$APISERVER_ADVERTISE_ADDRESS" >/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 <<EOF

[monok8s] bootstrap complete

Try these now:

  export KUBECONFIG=/root/.kube/config
  kubectl get nodes -o wide
  kubectl describe nodes

Notes:
- On a fresh single-node control plane, the node may stay NotReady until you install a CNI.
- 'kubectl describe nodes' will still work as soon as the API server is up and your kubeconfig is installed.
- If you want pods to run on this same node, keep ALLOW_SCHEDULING_ON_CONTROL_PLANE=yes.
EOF
}

main() {
	load_config
	set_hostname_if_needed
	ensure_ip_forward
	ensure_apiserver_address_present

	check_prereqs
	check_already_initialized
	check_crio_running
	check_required_images

	generate_kubeadm_config
	run_kubeadm_init

	rc-service kubelet restart

	setup_local_kubectl
	allow_single_node_scheduling
	print_next_steps
}

main "$@"