penguin/monok8s

k8s image for Mono Gateway Dev Kit

alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh

raw ยท 12084 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}"

    BOOTSTRAP_MODE="${BOOTSTRAP_MODE:-init}"
    JOIN_KIND="${JOIN_KIND:-worker}"
    API_SERVER_ENDPOINT="${API_SERVER_ENDPOINT:-}"
    BOOTSTRAP_TOKEN="${BOOTSTRAP_TOKEN:-}"
    DISCOVERY_TOKEN_CA_CERT_HASH="${DISCOVERY_TOKEN_CA_CERT_HASH:-}"
    CONTROL_PLANE_CERT_KEY="${CONTROL_PLANE_CERT_KEY:-}"
    CNI_PLUGIN="${CNI_PLUGIN:-none}"
}

validate_config() {
    case "$BOOTSTRAP_MODE" in
        init)
            ;;
        join)
            : "${API_SERVER_ENDPOINT:?API_SERVER_ENDPOINT is required for join mode}"
            : "${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN is required for join mode}"
            : "${DISCOVERY_TOKEN_CA_CERT_HASH:?DISCOVERY_TOKEN_CA_CERT_HASH is required for join mode}"

            case "$JOIN_KIND" in
                worker|control-plane)
                    ;;
                *)
                    fail "JOIN_KIND must be 'worker' or 'control-plane'"
                    ;;
            esac

            if [ "$JOIN_KIND" = "control-plane" ]; then
                : "${CONTROL_PLANE_CERT_KEY:?CONTROL_PLANE_CERT_KEY is required for JOIN_KIND=control-plane}"
            fi
            ;;
        *)
            fail "BOOTSTRAP_MODE must be 'init' or 'join'"
            ;;
    esac
}

check_prereqs() {
    need_cmd kubeadm
    need_cmd kubelet
    need_cmd kubectl
    need_cmd crictl
    need_cmd rc-service
    need_cmd awk
	need_cmd ip
    need_cmd grep
    need_cmd sed
    need_cmd hostname
}

check_apiserver_reachable() {
    host="${API_SERVER_ENDPOINT%:*}"
    port="${API_SERVER_ENDPOINT##*:}"

    need_cmd nc

    log "checking API server reachability: ${host}:${port}"

    for _ in $(seq 1 20); do
        if nc -z "$host" "$port" >/dev/null 2>&1; then
            log "API server is reachable"
            return 0
        fi
        sleep 1
    done

    fail "cannot reach API server at ${host}:${port}"
}

start_crio() {
	rc-service crio start
}

check_crio_running() {
    log "waiting for CRI-O to become ready..."

    last_status="unknown"

    for _ in $(seq 1 30); do
        if rc-service crio status >/dev/null 2>&1; then
            last_status="service-running"
            if crictl --runtime-endpoint "$CONTAINER_RUNTIME_ENDPOINT" info >/dev/null 2>&1; then
                log "CRI-O is up"
                return 0
            fi
            last_status="service-running-but-runtime-not-ready"
        else
            last_status="service-not-running"
        fi
        sleep 1
    done

    fail "CRI-O did not become ready in time (${last_status})"
}

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_not_already_bootstrapped() {
    case "$BOOTSTRAP_MODE" in
        init)
            if [ -f /etc/kubernetes/admin.conf ]; then
                fail "cluster already appears initialized (/etc/kubernetes/admin.conf exists)"
            fi
            ;;
        join)
            if [ -f /etc/kubernetes/kubelet.conf ]; then
                fail "node already appears joined (/etc/kubernetes/kubelet.conf exists)"
            fi
            ;;
    esac
}

run_kubeadm_join() {
    log "running kubeadm join..."

    case "$JOIN_KIND" in
        worker)
            kubeadm join "${API_SERVER_ENDPOINT}" \
                --token "${BOOTSTRAP_TOKEN}" \
                --discovery-token-ca-cert-hash "${DISCOVERY_TOKEN_CA_CERT_HASH}" \
                --node-name "${NODE_NAME}" \
                --cri-socket "${CONTAINER_RUNTIME_ENDPOINT}"
            ;;
        control-plane)
            kubeadm join "${API_SERVER_ENDPOINT}" \
                --token "${BOOTSTRAP_TOKEN}" \
                --discovery-token-ca-cert-hash "${DISCOVERY_TOKEN_CA_CERT_HASH}" \
                --control-plane \
                --certificate-key "${CONTROL_PLANE_CERT_KEY}" \
                --apiserver-advertise-address "${APISERVER_ADVERTISE_ADDRESS}" \
                --node-name "${NODE_NAME}" \
                --cri-socket "${CONTAINER_RUNTIME_ENDPOINT}"
            ;;
    esac
}

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"
}

require_local_ip() {
    wanted_ip="$1"

    ip -o addr show | awk '{print $4}' | cut -d/ -f1 | grep -Fx "$wanted_ip" >/dev/null 2>&1 \
        || fail "required local IP is not present on any interface: $wanted_ip"
}

validate_network_requirements() {
    case "$BOOTSTRAP_MODE" in
        init)
            require_local_ip "$APISERVER_ADVERTISE_ADDRESS"
            ;;
        join)
            require_local_ip "$APISERVER_ADVERTISE_ADDRESS"
            check_apiserver_reachable
            ;;
        *)
            fail "unsupported BOOTSTRAP_MODE: $BOOTSTRAP_MODE"
            ;;
    esac
}

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
}

wait_for_node() {
    log "waiting for node registration: $NODE_NAME"

    for _ in $(seq 1 60); do
        if kubectl --kubeconfig /etc/kubernetes/admin.conf get node "$NODE_NAME" >/dev/null 2>&1; then
            return 0
        fi
        sleep 1
    done

    fail "node $NODE_NAME did not register in time"
}

apply_local_node_metadata_if_possible() {
    if [ "$BOOTSTRAP_MODE" != "init" ]; then
        log "skipping node labels/annotations from this node (not control-plane init mode)"
        return 0
    fi

    wait_for_node

    if [ -n "${NODE_ANNOTATIONS:-}" ]; then
        kubectl --kubeconfig /etc/kubernetes/admin.conf annotate node "$NODE_NAME" $(printf '%s' "$NODE_ANNOTATIONS" | tr ',' ' ') --overwrite
    fi
    if [ -n "${NODE_LABELS:-}" ]; then
        kubectl --kubeconfig /etc/kubernetes/admin.conf label node "$NODE_NAME" $(printf '%s' "$NODE_LABELS" | tr ',' ' ') --overwrite
    fi
}

install_cni_if_requested() {
    case "${CNI_PLUGIN}" in
        none)
            if [ -f /etc/cni/net.d/10-crio-bridge.conflist ]; then
                mv /etc/cni/net.d/10-crio-bridge.conflist \
                   /etc/cni/net.d/10-crio-bridge.conflist.disabled
            fi
            log "bootstrap bridge CNI disabled; install a cluster CNI (e.g., flannel) for pod networking"
            ;;
        bridge)
            if [ -f /etc/cni/net.d/10-crio-bridge.conflist.disabled ]; then
                mv /etc/cni/net.d/10-crio-bridge.conflist.disabled \
                   /etc/cni/net.d/10-crio-bridge.conflist
            fi
            log "bootstrap bridge CNI enabled"
            ;;
        *)
            fail "unsupported CNI_PLUGIN: ${CNI_PLUGIN}"
            ;;
    esac
}

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() {
    echo
    echo "[monok8s] bootstrap complete"
    echo

    case "$BOOTSTRAP_MODE" in
        init)
            cat <<EOF
Try these now:

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

Notes:
- On a fresh cluster without a CNI, nodes may stay NotReady.
- If you want pods to run on this same node, keep ALLOW_SCHEDULING_ON_CONTROL_PLANE=yes.
EOF
            ;;
        join)
            cat <<EOF
This node has attempted to join the cluster.

Check from the control-plane node:

  kubectl get nodes -o wide
  kubectl describe node ${NODE_NAME}

Notes:
- This node can join without a CNI.
- Without a cluster CNI, the node may remain NotReady.
EOF
            ;;
    esac
}

main() {
    load_config
    validate_config

    check_prereqs
    validate_network_requirements
    check_not_already_bootstrapped

    install_cni_if_requested
    start_crio
    check_crio_running

    case "$BOOTSTRAP_MODE" in
        init)
            check_required_images
            generate_kubeadm_config
            run_kubeadm_init
            rc-service kubelet restart
            setup_local_kubectl
            apply_local_node_metadata_if_possible
            allow_single_node_scheduling
            ;;
        join)
            run_kubeadm_join
            ;;
    esac

    print_next_steps
}

main "$@"