commit 01ec867c2f43892fc82e9843eed3dec1de6d593b1e43415fb426b9d1a4d8b7f8
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-03-25T10:05:42Z |
| subject | Can now create a node worker |
commit 01ec867c2f43892fc82e9843eed3dec1de6d593b1e43415fb426b9d1a4d8b7f8
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-03-25T10:05:42Z
Can now create a node worker
---
alpine/build-rootfs.sh | 2 +-
alpine/configure-system.sh | 1 -
alpine/install-packages.sh | 3 -
alpine/rootfs-extra/etc/fancontrol | 2 +-
.../rootfs-extra/opt/scripts/apply-node-config.sh | 149 +++++++
.../rootfs-extra/opt/scripts/bootstrap-cluster.sh | 436 +++++++++++++++++++++
.../opt/scripts/bootstrap-control-plane.sh | 377 ------------------
configs/cluster.env.default | 18 +
configs/node.env.default | 7 +
initramfs/rootfs-extra/bin/flash-emmc.sh.tmpl | 140 +++++--
kernel-build/dts/mono-gateway-dk.dts | 3 +
makefile | 17 +-
12 files changed, 747 insertions(+), 408 deletions(-)
diff --git a/alpine/build-rootfs.sh b/alpine/build-rootfs.sh
index 99933b0..8031868 100755
--- a/alpine/build-rootfs.sh
+++ b/alpine/build-rootfs.sh
@@ -51,7 +51,7 @@ rm -r "$ROOTFS/build"
echo "##################################################### Packaging RootFS "$( du -sh "$ROOTFS/" )
IMG=output.img
-SIZE=2048MB
+SIZE=3072MB
dd if=/dev/zero of="$IMG" bs=1 count=0 seek=$SIZE
diff --git a/alpine/configure-system.sh b/alpine/configure-system.sh
index f7e8636..9d931d1 100755
--- a/alpine/configure-system.sh
+++ b/alpine/configure-system.sh
@@ -11,4 +11,3 @@ rc-update add fancontrol boot
rc-update add loopback boot
rc-update add hostname boot
rc-update add localmount boot
-rc-update add crio default
diff --git a/alpine/install-packages.sh b/alpine/install-packages.sh
index 11855cb..08c8630 100755
--- a/alpine/install-packages.sh
+++ b/alpine/install-packages.sh
@@ -31,9 +31,6 @@ if [ $? -ne 0 ]; then
exit $?
fi
-mv /etc/cni/net.d/10-crio-bridge.conflist.disabled \
- /etc/cni/net.d/10-crio-bridge.conflist
-
sed -i "s/default_runtime = \"crun\"/\0\ncgroup_manager = \"cgroupfs\"/g" /etc/crio/crio.conf.d/10-crio.conf
grep cgroup_manager /etc/crio/crio.conf.d/10-crio.conf || exit 1
diff --git a/alpine/rootfs-extra/etc/fancontrol b/alpine/rootfs-extra/etc/fancontrol
index b378156..143d6eb 100644
--- a/alpine/rootfs-extra/etc/fancontrol
+++ b/alpine/rootfs-extra/etc/fancontrol
@@ -3,7 +3,7 @@ DEVPATH=hwmon0=devices/platform/soc/2180000.i2c/i2c-0/i2c-7/7-002e hwmon1=device
DEVNAME=hwmon0=emc2305 hwmon1=ddr_thermal
FCTEMPS=hwmon0/pwm2=hwmon1/temp1_input
FCFANS= hwmon0/pwm2=hwmon0/fan1_input
-MINTEMP=hwmon0/pwm2=40
+MINTEMP=hwmon0/pwm2=35
MAXTEMP=hwmon0/pwm2=60
MINSTART=hwmon0/pwm2=60
MINSTOP=hwmon0/pwm2=45
diff --git a/alpine/rootfs-extra/opt/scripts/apply-node-config.sh b/alpine/rootfs-extra/opt/scripts/apply-node-config.sh
new file mode 100644
index 0000000..74391e0
--- /dev/null
+++ b/alpine/rootfs-extra/opt/scripts/apply-node-config.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+set -euo pipefail
+
+CONFIG_DIR="${CONFIG_DIR:-/opt/monok8s/config}"
+NODE_ENV="${NODE_ENV:-$CONFIG_DIR/node.env}"
+
+log() {
+ echo "[monok8s-node] $*"
+}
+
+fail() {
+ echo "[monok8s-node] 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 "$NODE_ENV"
+
+ # shellcheck disable=SC1090
+ . "$NODE_ENV"
+
+ HOSTNAME="${HOSTNAME:-}"
+ MGMT_IFACE="${MGMT_IFACE:-}"
+ MGMT_ADDRESS="${MGMT_ADDRESS:-}"
+ MGMT_GATEWAY="${MGMT_GATEWAY:-}"
+}
+
+validate_config() {
+ [ -n "$HOSTNAME" ] || fail "HOSTNAME is required"
+ [ -n "$MGMT_IFACE" ] || fail "MGMT_IFACE is required"
+ [ -n "$MGMT_ADDRESS" ] || fail "MGMT_ADDRESS is required"
+
+ case "$MGMT_ADDRESS" in
+ */*)
+ ;;
+ *)
+ fail "MGMT_ADDRESS must include a CIDR prefix, example: 10.0.0.13/24"
+ ;;
+ esac
+}
+
+check_prereqs() {
+ need_cmd ip
+ need_cmd hostname
+ need_cmd grep
+ need_cmd awk
+ need_cmd cut
+ need_cmd mkdir
+ need_cmd printf
+ need_cmd cat
+}
+
+configure_mgmt_interface() {
+ local addr_ip
+
+ ip link show "$MGMT_IFACE" >/dev/null 2>&1 || fail "interface not found: $MGMT_IFACE"
+
+ log "bringing up interface: $MGMT_IFACE"
+ ip link set "$MGMT_IFACE" up
+
+ addr_ip="${MGMT_ADDRESS%/*}"
+
+ if ip -o addr show dev "$MGMT_IFACE" | awk '{print $4}' | cut -d/ -f1 | grep -Fx "$addr_ip" >/dev/null 2>&1; then
+ log "address already present on $MGMT_IFACE: $MGMT_ADDRESS"
+ else
+ log "assigning $MGMT_ADDRESS to $MGMT_IFACE"
+ ip addr add "$MGMT_ADDRESS" dev "$MGMT_IFACE"
+ fi
+
+ if [ -n "${MGMT_GATEWAY:-}" ]; then
+ log "ensuring default route via $MGMT_GATEWAY"
+ ip route replace default via "$MGMT_GATEWAY" dev "$MGMT_IFACE"
+ fi
+}
+
+set_hostname_if_needed() {
+ local current_hostname
+
+ current_hostname="$(hostname 2>/dev/null || true)"
+
+ if [ "$current_hostname" != "$HOSTNAME" ]; then
+ log "setting hostname to $HOSTNAME"
+
+ hostname "$HOSTNAME"
+
+ mkdir -p /etc
+ printf '%s\n' "$HOSTNAME" > /etc/hostname
+
+ if [ -f /etc/hosts ]; then
+ if ! grep -Eq "[[:space:]]$HOSTNAME([[:space:]]|$)" /etc/hosts; then
+ printf '127.0.0.1\tlocalhost %s\n' "$HOSTNAME" >> /etc/hosts
+ fi
+ else
+ cat > /etc/hosts <<EOF
+127.0.0.1 localhost $HOSTNAME
+EOF
+ fi
+ else
+ log "hostname already set: $HOSTNAME"
+ fi
+}
+
+ensure_ip_forward() {
+ local current
+
+ 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
+}
+
+print_summary() {
+ log "node configuration applied"
+ log "hostname: $HOSTNAME"
+ log "interface: $MGMT_IFACE"
+ log "address: $MGMT_ADDRESS"
+ if [ -n "${MGMT_GATEWAY:-}" ]; then
+ log "gateway: $MGMT_GATEWAY"
+ else
+ log "gateway: <not set>"
+ fi
+}
+
+main() {
+ load_config
+ validate_config
+ check_prereqs
+
+ ensure_ip_forward
+ configure_mgmt_interface
+ set_hostname_if_needed
+ print_summary
+}
+
+main "$@"
\ No newline at end of file
diff --git a/alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh b/alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh
new file mode 100755
index 0000000..9752bd2
--- /dev/null
+++ b/alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh
@@ -0,0 +1,436 @@
+#!/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 "$@"
diff --git a/alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh b/alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh
deleted file mode 100755
index eccddb7..0000000
--- a/alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh
+++ /dev/null
@@ -1,377 +0,0 @@
-#!/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 "$@"
diff --git a/configs/cluster.env.default b/configs/cluster.env.default
index c326e84..efc09b5 100644
--- a/configs/cluster.env.default
+++ b/configs/cluster.env.default
@@ -9,6 +9,24 @@ POD_SUBNET=10.244.0.0/16
SERVICE_SUBNET=10.96.0.0/12
CLUSTER_DOMAIN=cluster.local
+# Bootstrap mode: init, join
+BOOTSTRAP_MODE=init
+
+# For join mode: worker, control-plane
+JOIN_KIND=worker
+API_SERVER_ENDPOINT=
+BOOTSTRAP_TOKEN=
+DISCOVERY_TOKEN_CA_CERT_HASH=
+CONTROL_PLANE_CERT_KEY=
+
+# none: Install manually
+# bridge: CRIO's default bridge CNI
+CNI_PLUGIN=none
+
+# Node registration metadata
+NODE_LABELS=topology.kubernetes.io/zone=lab,node.kubernetes.io/instance-type=mono-gateway
+NODE_ANNOTATIONS=mono.si/board=ls1046a,mono.si/image-version=dev
+
# Optional
# Extra API server SANs, comma-separated
SANS=127.0.0.1,localhost,monok8s-master
diff --git a/configs/node.env.default b/configs/node.env.default
new file mode 100644
index 0000000..c7ab0f8
--- /dev/null
+++ b/configs/node.env.default
@@ -0,0 +1,7 @@
+HOSTNAME=monok8s-master
+
+# RJ45 Ports from left to right (interface name inside the OS)
+# - eth1, eth2, eth0
+MGMT_IFACE=eth0
+MGMT_ADDRESS=10.0.0.13/24
+MGMT_GATEWAY=10.0.0.1
diff --git a/initramfs/rootfs-extra/bin/flash-emmc.sh.tmpl b/initramfs/rootfs-extra/bin/flash-emmc.sh.tmpl
index af75fcf..b3ed5e1 100755
--- a/initramfs/rootfs-extra/bin/flash-emmc.sh.tmpl
+++ b/initramfs/rootfs-extra/bin/flash-emmc.sh.tmpl
@@ -2,13 +2,69 @@
set -eu
MNT=/mnt
+ROOT_MNT=/mnt-root
USB_DEV=/dev/sda
EMMC_DEV=/dev/mmcblk0
-DEFAULT_GLOB="${MNT}/$${BUILD_TAG}_RELEASE_IMAGE"
+EMMC_ROOT_PART=/dev/mmcblk0p1
+CONFIG_DIR=/opt/monok8s/config
-mkdir -p "$MNT"
+DEFAULT_GLOB="${MNT}/${BUILD_TAG}_RELEASE_IMAGE"
-echo "[*] Mounting USB..."
+cleanup() {
+ umount "$ROOT_MNT" 2>/dev/null || true
+ umount "$MNT" 2>/dev/null || true
+}
+
+log() {
+ echo "[*] $*"
+}
+
+warn() {
+ echo "[!] $*" >&2
+}
+
+die() {
+ echo "[!] ERROR: $*" >&2
+ exit 1
+}
+
+reread_partitions() {
+ log "Re-reading partition table..."
+ blockdev --rereadpt "$EMMC_DEV" 2>/dev/null || true
+ partprobe "$EMMC_DEV" 2>/dev/null || true
+ partx -u "$EMMC_DEV" 2>/dev/null || true
+ mdev -s 2>/dev/null || true
+}
+
+get_sectors() {
+ devname="${1##*/}"
+ cat "/sys/class/block/$devname/size" 2>/dev/null || echo 0
+}
+
+wait_for_partition_ready() {
+ part="$1"
+ timeout="${2:-15}"
+
+ i=0
+ while [ "$i" -lt "$timeout" ]; do
+ if [ -b "$part" ]; then
+ sectors="$(get_sectors "$part")"
+ if [ "${sectors:-0}" -gt 0 ]; then
+ log "Partition $part is present with $sectors sectors"
+ return 0
+ fi
+ fi
+ i=$((i + 1))
+ sleep 1
+ done
+
+ return 1
+}
+
+mkdir -p "$MNT" "$ROOT_MNT"
+trap cleanup EXIT
+
+log "Mounting USB..."
mount "$USB_DEV" "$MNT"
# Find a default image (first match)
@@ -23,45 +79,83 @@ else
fi
echo
-# Prompt user
printf "Image Location (%s): " "${DEFAULT_IMG:-/mnt/...}"
read -r IMG
-# Use default if empty
if [ -z "$IMG" ]; then
IMG="$DEFAULT_IMG"
fi
-# Validate
-if [ -z "$IMG" ] || [ ! -f "$IMG" ]; then
- echo "Invalid image: $IMG"
- umount "$MNT" || true
- exit 1
-fi
+[ -n "$IMG" ] || die "No image selected"
+[ -f "$IMG" ] || die "Invalid image: $IMG"
echo
+echo "Looking for env files on USB..."
+ENV_FILES="$(find "$MNT" -maxdepth 1 -type f -name '*.env' | sort || true)"
+
+if [ -n "$ENV_FILES" ]; then
+ echo "Found env files:"
+ echo "$ENV_FILES" | sed 's/^/ /'
+else
+ echo "No *.env files found in $MNT"
+fi
+echo
+
echo "About to write:"
-echo " Image: $IMG"
+echo " Image: $IMG"
echo " Target: $EMMC_DEV"
echo
printf "Type 'YES' to continue: "
read -r CONFIRM
-if [ "$CONFIRM" != "YES" ]; then
- echo "Aborted."
- umount "$MNT" || true
- exit 1
-fi
+[ "$CONFIRM" = "YES" ] || die "Aborted."
-echo "[*] Flashing..."
+log "Flashing image to eMMC..."
gunzip -c "$IMG" > "$EMMC_DEV"
-echo "[*] Syncing..."
+log "Syncing flash write..."
sync
-echo "[*] Done."
-umount "$MNT" || true
+reread_partitions
+
+log "Waiting for partition nodes..."
+if ! wait_for_partition_ready "$EMMC_ROOT_PART" 15; then
+ warn "Initial partition scan did not settle; retrying after another reread..."
+ reread_partitions
+ sleep 2
+ wait_for_partition_ready "$EMMC_ROOT_PART" 15 || die "root partition not ready: $EMMC_ROOT_PART"
+fi
+
+log "Partition sizes:"
+echo " $(basename "$EMMC_DEV"): $(get_sectors "$EMMC_DEV") sectors"
+echo " $(basename "$EMMC_ROOT_PART"): $(get_sectors "$EMMC_ROOT_PART") sectors"
+
+if [ -n "$ENV_FILES" ]; then
+ log "Mounting flashed root partition..."
+ mount "$EMMC_ROOT_PART" "$ROOT_MNT" || {
+ warn "Mount failed; retrying once after partition reread..."
+ reread_partitions
+ sleep 2
+ mount "$EMMC_ROOT_PART" "$ROOT_MNT"
+ }
+
+ log "Creating config dir: $CONFIG_DIR"
+ mkdir -p "$ROOT_MNT$CONFIG_DIR"
+
+ log "Copying env files..."
+ for f in $ENV_FILES; do
+ base="$(basename "$f")"
+ cp "$f" "$ROOT_MNT$CONFIG_DIR/$base"
+ echo " copied: $base"
+ done
+
+ log "Syncing copied config..."
+ sync
+
+ umount "$ROOT_MNT"
+fi
-echo "[*] Rebooting..."
-reboot -f
+log "Done."
+log "Rebooting..."
+reboot -f
\ No newline at end of file
diff --git a/kernel-build/dts/mono-gateway-dk.dts b/kernel-build/dts/mono-gateway-dk.dts
index eaafe8e..d2d7aea 100644
--- a/kernel-build/dts/mono-gateway-dk.dts
+++ b/kernel-build/dts/mono-gateway-dk.dts
@@ -482,18 +482,21 @@
ethernet@e8000 {
phy-handle = <&sgmii_phy0>;
phy-connection-type = "sgmii";
+ managed = "in-band-status";
status = "okay";
};
ethernet@ea000 {
phy-handle = <&sgmii_phy1>;
phy-connection-type = "sgmii";
+ managed = "in-band-status";
status = "okay";
};
ethernet@e2000 {
phy-handle = <&sgmii_phy2>;
phy-connection-type = "sgmii";
+ managed = "in-band-status";
status = "okay";
};
diff --git a/makefile b/makefile
index 01285b6..2fa068c 100644
--- a/makefile
+++ b/makefile
@@ -20,6 +20,8 @@ CONFIGS_DIR := configs
SCRIPTS_DIR := scripts
CLUSTER_ENV_DEFAULT := $(CONFIGS_DIR)/cluster.env.default
CLUSTER_ENV := $(OUT_DIR)/cluster.env
+NODE_ENV_DEFAULT := configs/node.env.default
+NODE_ENV := $(OUT_DIR)/node.env
BOARD_ITB := $(OUT_DIR)/board.itb
INITRAMFS := $(OUT_DIR)/initramfs.cpio.gz
@@ -211,14 +213,25 @@ check-functions:
in_main && /^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*$$/ { \
gsub(/^[ \t]+/, "", $$0); \
print $$0 \
- }' ./alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh \
+ }' ./alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh \
| sort -u > /tmp/called.txt
- @grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' ./alpine/rootfs-extra/opt/scripts/bootstrap-control-plane.sh \
+ @grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' ./alpine/rootfs-extra/opt/scripts/bootstrap-cluster.sh \
| sed 's/().*//' \
| sort -u > /tmp/defined.txt
@echo "Missing functions:"
@comm -23 /tmp/called.txt /tmp/defined.txt || true
+# ---- node targets ------------------------------------------------------------
+
+node-config: $(NODE_ENV_DEFAULT) $(SCRIPTS_DIR)/merge-env.sh | $(OUT_DIR)
+ sh $(SCRIPTS_DIR)/merge-env.sh $(NODE_ENV_DEFAULT) $(NODE_ENV)
+
+node-defconfig: $(NODE_ENV_DEFAULT) | $(OUT_DIR)
+ cp $(NODE_ENV_DEFAULT) $(NODE_ENV)
+
+node-print:
+ @cat $(NODE_ENV)
+
# ---- cluster targets ------------------------------------------------------------
cluster-config: $(CLUSTER_ENV_DEFAULT) $(SCRIPTS_DIR)/merge-env.sh | $(OUT_DIR)