penguin/monok8s

k8s image for Mono Gateway Dev Kit

commit 5fbc2846a146dc252d72db157402b285660de46ec389541ef0a0b1729e5d987f

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-03-29T15:55:59Z
subjectCan ctl init with index
commit 5fbc2846a146dc252d72db157402b285660de46ec389541ef0a0b1729e5d987f
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-03-29T15:55:59Z

    Can ctl init with index
---
 clitools/pkg/bootstrap/registry.go |   1 +
 clitools/pkg/bootstrap/runner.go   | 174 +++++++++++++++++++++++++++++++------
 clitools/pkg/cmd/initcmd/init.go   | 154 ++++++++++++++++++++++++++++++--
 clitools/pkg/cmd/root/root.go      |  12 ++-
 clitools/pkg/node/kubeadm.go       |  48 +++++-----
 5 files changed, 333 insertions(+), 56 deletions(-)

diff --git a/clitools/pkg/bootstrap/registry.go b/clitools/pkg/bootstrap/registry.go
index 777c425..412dfd7 100644
--- a/clitools/pkg/bootstrap/registry.go
+++ b/clitools/pkg/bootstrap/registry.go
@@ -24,6 +24,7 @@ func NewRegistry(ctx *node.NodeContext) *Registry {
 		steps: map[string]node.Step{
 			"validate_network_requirements":         node.ValidateNetworkRequirements,
 			"detect_local_cluster_state":            node.DetectLocalClusterState,
+			"classify_bootstrap_action":             node.ClassifyBootstrapAction,
 			"configure_default_cni":                 node.ConfigureDefaultCNI,
 			"start_crio":                            node.StartCRIO,
 			"wait_for_existing_cluster_if_needed":   node.WaitForExistingClusterIfNeeded,
diff --git a/clitools/pkg/bootstrap/runner.go b/clitools/pkg/bootstrap/runner.go
index 7a6c86a..15e27d1 100644
--- a/clitools/pkg/bootstrap/runner.go
+++ b/clitools/pkg/bootstrap/runner.go
@@ -2,6 +2,7 @@ package bootstrap
 
 import (
 	"context"
+	"fmt"
 
 	monov1alpha1 "undecided.project/monok8s/pkg/apis/monok8s/v1alpha1"
 	"undecided.project/monok8s/pkg/node"
@@ -11,6 +12,18 @@ import (
 type Runner struct {
 	NodeCtx  *node.NodeContext
 	Registry *Registry
+
+	initSteps []StepInfo
+}
+
+type StepInfo struct {
+	RegKey string
+	Name   string
+	Desc   string
+}
+
+type StepSelection struct {
+	Indices []int // 1-based
 }
 
 func NewRunner(cfg *monov1alpha1.MonoKSConfig) *Runner {
@@ -22,41 +35,148 @@ func NewRunner(cfg *monov1alpha1.MonoKSConfig) *Runner {
 	return &Runner{
 		NodeCtx:  nctx,
 		Registry: NewRegistry(nctx),
+		initSteps: []StepInfo{
+			{
+				RegKey: "configure_hostname",
+				Name:   "Configure hostname",
+				Desc:   "Set system hostname according to cluster configuration",
+			},
+			{
+				RegKey: "configure_mgmt_interface",
+				Name:   "Configure management interface",
+				Desc:   "Configure management network interface, IP address, and gateway",
+			},
+			{
+				RegKey: "configure_dns",
+				Name:   "Configure DNS",
+				Desc:   "Set system DNS resolver configuration for cluster and external access",
+			},
+			{
+				RegKey: "ensure_ip_forward",
+				Name:   "Ensure IP forwarding",
+				Desc:   "Enable kernel IP forwarding required for pod networking",
+			},
+			{
+				RegKey: "configure_default_cni",
+				Name:   "Configure default CNI",
+				Desc:   "Install or configure default container networking (CNI bridge, IPAM, etc.)",
+			},
+			{
+				RegKey: "start_crio",
+				Name:   "Start CRI-O runtime",
+				Desc:   "Start container runtime and verify it is ready for Kubernetes workloads",
+			},
+			{
+				RegKey: "validate_required_images",
+				Name:   "Validate required images",
+				Desc:   "Ensure all required Kubernetes images are present or available locally",
+			},
+			{
+				RegKey: "validate_network_requirements",
+				Name:   "Validate network requirements",
+				Desc:   "Ensure required kernel networking features (iptables/nftables, bridge, forwarding) are available",
+			},
+			{
+				RegKey: "detect_local_cluster_state",
+				Name:   "Detect local cluster state",
+				Desc:   "Inspect local node to determine existing Kubernetes membership and configuration",
+			},
+			{
+				RegKey: "classify_bootstrap_action",
+				Name:   "Classify bootstrap action",
+				Desc:   "Decide whether to init, join, upgrade, or reconcile based on local state and desired version",
+			},
+			{
+				RegKey: "wait_for_existing_cluster_if_needed",
+				Name:   "Wait for existing cluster",
+				Desc:   "Block until control plane is reachable when joining or reconciling an existing cluster",
+			},
+			{
+				RegKey: "generate_kubeadm_config",
+				Name:   "Generate kubeadm config",
+				Desc:   "Render kubeadm configuration for init, join, or upgrade operations",
+			},
+			{
+				RegKey: "apply_local_node_metadata_if_possible",
+				Name:   "Apply node metadata",
+				Desc:   "Apply labels/annotations to the local node if API server is reachable",
+			},
+			{
+				RegKey: "allow_single_node_scheduling",
+				Name:   "Allow single-node scheduling",
+				Desc:   "Remove control-plane taints to allow workloads on single-node clusters",
+			},
+			{
+				RegKey: "reconcile_control_plane",
+				Name:   "Reconcile control plane",
+				Desc:   "Ensure control plane components match desired state without full reinitialization",
+			},
+			{
+				RegKey: "check_upgrade_prereqs",
+				Name:   "Check upgrade prerequisites",
+				Desc:   "Validate cluster state and version compatibility before upgrade",
+			},
+			{
+				RegKey: "run_kubeadm_upgrade_apply",
+				Name:   "Run kubeadm upgrade apply",
+				Desc:   "Upgrade control plane components using kubeadm",
+			},
+			{
+				RegKey: "run_kubeadm_init",
+				Name:   "Run kubeadm init",
+				Desc:   "Initialize a new Kubernetes control plane using kubeadm",
+			},
+			{
+				RegKey: "run_kubeadm_join",
+				Name:   "Run kubeadm join",
+				Desc:   "Join node to existing cluster as worker or control-plane",
+			},
+			{
+				RegKey: "reconcile_node",
+				Name:   "Reconcile node state",
+				Desc:   "Ensure node configuration matches desired state after join or upgrade",
+			},
+			{
+				RegKey: "run_kubeadm_upgrade_node",
+				Name:   "Run kubeadm upgrade node",
+				Desc:   "Upgrade node components (kubelet, config) to match control plane",
+			},
+			{
+				RegKey: "print_summary",
+				Name:   "Print summary",
+				Desc:   "Output final bootstrap summary and detected state",
+			},
+		},
+	}
+}
+
+func (r *Runner) RunNamedStep(ctx context.Context, name string) error {
+	step, err := r.Registry.Get(name)
+	if err != nil {
+		return err
 	}
+	return step(ctx, r.NodeCtx)
+}
+
+func (r *Runner) InitSteps() []StepInfo {
+	return r.initSteps
 }
 
 func (r *Runner) Init(ctx context.Context) error {
-	for _, name := range []string{
-		"configure_hostname",
-		"configure_dns",
-		"configure_mgmt_interface",
-		"detect_local_cluster_state",
-		"validate_network_requirements",
-		"configure_default_cni",
-		"start_crio",
-		"check_required_images",
-		"chcek_for_version_skew",
-		"wait_for_existing_cluster_if_needed",
-		"decide_bootstrap_action",
-		"check_required_images",
-		"generate_kubeadm_config",
-		"run_kubeadm_init",
-		"restart_kubelet",
-		"apply_local_node_metadata_if_possible",
-		"allow_single_node_scheduling",
-		"print_summary",
-	} {
-		if err := r.RunNamedStep(ctx, name); err != nil {
-			return err
+	for i, step := range r.initSteps {
+		if err := r.RunNamedStep(ctx, step.RegKey); err != nil {
+			return fmt.Errorf("step %d (%s): %w", i+1, step.Name, err)
 		}
 	}
 	return nil
 }
 
-func (r *Runner) RunNamedStep(ctx context.Context, name string) error {
-	step, err := r.Registry.Get(name)
-	if err != nil {
-		return err
+func (r *Runner) InitSelected(ctx context.Context, sel StepSelection) error {
+	for _, idx := range sel.Indices {
+		step := r.initSteps[idx-1]
+		if err := r.RunNamedStep(ctx, step.RegKey); err != nil {
+			return fmt.Errorf("step %d (%s): %w", idx, step.Name, err)
+		}
 	}
-	return step(ctx, r.NodeCtx)
+	return nil
 }
diff --git a/clitools/pkg/cmd/initcmd/init.go b/clitools/pkg/cmd/initcmd/init.go
index 4950ea8..44539de 100644
--- a/clitools/pkg/cmd/initcmd/init.go
+++ b/clitools/pkg/cmd/initcmd/init.go
@@ -1,21 +1,35 @@
 package initcmd
 
 import (
-	"context"
+	"fmt"
+	"sort"
+	"strconv"
+	"strings"
 
-	"undecided.project/monok8s/pkg/bootstrap"
-	"undecided.project/monok8s/pkg/config"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/klog/v2"
+
+	"undecided.project/monok8s/pkg/bootstrap"
+	"undecided.project/monok8s/pkg/config"
 )
 
 func NewCmdInit(_ *genericclioptions.ConfigFlags) *cobra.Command {
 	var configPath string
+
 	cmd := &cobra.Command{
-		Use:   "init",
-		Short: "Equivalent of apply-node-config + bootstrap-cluster",
-		RunE: func(cmd *cobra.Command, _ []string) error {
+		Use:   "init [list|STEPSEL]",
+		Short: "Start the bootstrap process for this node",
+		Example: strings.TrimSpace(`
+  ctl init
+  ctl init list
+  ctl init 1-3
+  ctl init -3
+  ctl init 3-
+  ctl init 1,3,5
+`),
+		Args: cobra.MaximumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
 			path, err := (config.Loader{}).ResolvePath(configPath)
 			if err != nil {
 				return err
@@ -24,11 +38,135 @@ func NewCmdInit(_ *genericclioptions.ConfigFlags) *cobra.Command {
 			if err != nil {
 				return err
 			}
+
+			runner := bootstrap.NewRunner(cfg)
+
+			if len(args) == 1 && strings.EqualFold(strings.TrimSpace(args[0]), "list") {
+				steps := runner.InitSteps()
+
+				fmt.Fprintln(cmd.OutOrStdout(), "Showing current bootstrap sequence")
+
+				// width = number of digits of max step number
+				width := len(fmt.Sprintf("%d", len(steps)))
+
+				for i, s := range steps {
+					fmt.Fprintf(cmd.OutOrStdout(), "\n  %*d. %s\n", width, i+1, s.Name)
+					fmt.Fprintf(cmd.OutOrStdout(), "        %s\n", s.Desc)
+				}
+				return err
+			}
+
 			klog.InfoS("starting init", "config", path, "node", cfg.Spec.NodeName)
-			return bootstrap.NewRunner(cfg).Init(cmd.Context())
+
+			if len(args) == 0 {
+				return runner.Init(cmd.Context())
+			}
+
+			steps := runner.InitSteps()
+			sel, err := parseStepSelection(args[0], len(steps))
+			if err != nil {
+				return err
+			}
+
+			return runner.InitSelected(cmd.Context(), sel)
 		},
 	}
+
 	cmd.Flags().StringVarP(&configPath, "config", "c", "", "path to MonoKSConfig yaml")
-	_ = context.Background()
 	return cmd
 }
+
+func parseStepSelection(raw string, max int) (bootstrap.StepSelection, error) {
+	raw = strings.TrimSpace(raw)
+	if raw == "" {
+		return bootstrap.StepSelection{}, fmt.Errorf("empty step selection")
+	}
+	if max <= 0 {
+		return bootstrap.StepSelection{}, fmt.Errorf("no steps available")
+	}
+
+	selected := map[int]struct{}{}
+
+	parts := strings.Split(raw, ",")
+	for _, part := range parts {
+		part = strings.TrimSpace(part)
+		if part == "" {
+			return bootstrap.StepSelection{}, fmt.Errorf("invalid empty selector in %q", raw)
+		}
+
+		if strings.Contains(part, "-") {
+			if strings.Count(part, "-") != 1 {
+				return bootstrap.StepSelection{}, fmt.Errorf("invalid range %q", part)
+			}
+
+			bounds := strings.SplitN(part, "-", 2)
+			left := strings.TrimSpace(bounds[0])
+			right := strings.TrimSpace(bounds[1])
+
+			var start, end int
+			switch {
+			case left == "" && right == "":
+				return bootstrap.StepSelection{}, fmt.Errorf("invalid range %q", part)
+
+			case left == "":
+				n, err := parseStepNumber(right, max)
+				if err != nil {
+					return bootstrap.StepSelection{}, err
+				}
+				start, end = 1, n
+
+			case right == "":
+				n, err := parseStepNumber(left, max)
+				if err != nil {
+					return bootstrap.StepSelection{}, err
+				}
+				start, end = n, max
+
+			default:
+				a, err := parseStepNumber(left, max)
+				if err != nil {
+					return bootstrap.StepSelection{}, err
+				}
+				b, err := parseStepNumber(right, max)
+				if err != nil {
+					return bootstrap.StepSelection{}, err
+				}
+				start, end = a, b
+			}
+
+			if start > end {
+				return bootstrap.StepSelection{}, fmt.Errorf("invalid descending range %q", part)
+			}
+
+			for i := start; i <= end; i++ {
+				selected[i] = struct{}{}
+			}
+			continue
+		}
+
+		n, err := parseStepNumber(part, max)
+		if err != nil {
+			return bootstrap.StepSelection{}, err
+		}
+		selected[n] = struct{}{}
+	}
+
+	out := make([]int, 0, len(selected))
+	for n := range selected {
+		out = append(out, n)
+	}
+	sort.Ints(out)
+
+	return bootstrap.StepSelection{Indices: out}, nil
+}
+
+func parseStepNumber(raw string, max int) (int, error) {
+	n, err := strconv.Atoi(strings.TrimSpace(raw))
+	if err != nil {
+		return 0, fmt.Errorf("invalid step number %q", raw)
+	}
+	if n < 1 || n > max {
+		return 0, fmt.Errorf("step number %d out of range 1-%d", n, max)
+	}
+	return n, nil
+}
diff --git a/clitools/pkg/cmd/root/root.go b/clitools/pkg/cmd/root/root.go
index 0055d9d..d2ed572 100644
--- a/clitools/pkg/cmd/root/root.go
+++ b/clitools/pkg/cmd/root/root.go
@@ -2,6 +2,7 @@ package root
 
 import (
 	"flag"
+	"os"
 
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -15,6 +16,16 @@ import (
 	versioncmd "undecided.project/monok8s/pkg/cmd/version"
 )
 
+func init() {
+	klog.InitFlags(nil)
+
+	if os.Getenv("DEBUG") != "" {
+		_ = flag.Set("v", "4") // debug level
+	} else {
+		_ = flag.Set("v", "0")
+	}
+}
+
 func NewRootCmd() *cobra.Command {
 	flags := genericclioptions.NewConfigFlags(true)
 
@@ -24,7 +35,6 @@ func NewRootCmd() *cobra.Command {
 		SilenceUsage:  true,
 		SilenceErrors: true,
 		PersistentPreRun: func(*cobra.Command, []string) {
-			klog.InitFlags(nil)
 			_ = flag.Set("logtostderr", "true")
 		},
 	}
diff --git a/clitools/pkg/node/kubeadm.go b/clitools/pkg/node/kubeadm.go
index ccbd268..b3fda8b 100644
--- a/clitools/pkg/node/kubeadm.go
+++ b/clitools/pkg/node/kubeadm.go
@@ -58,6 +58,7 @@ func DetectLocalClusterState(ctx context.Context, nctx *NodeContext) error {
 		return fmt.Errorf("unreachable local cluster state")
 	}
 
+	klog.V(4).Infof("Detected local state: %+v", state)
 	nctx.LocalClusterState = &state
 	return nil
 }
@@ -164,6 +165,10 @@ func CheckForVersionSkew(ctx context.Context, nctx *NodeContext) error {
 func ClassifyBootstrapAction(ctx context.Context, nctx *NodeContext) error {
 	_ = ctx
 
+	if nctx.LocalClusterState == nil {
+		return errors.New("LocalClusterState is nil, call detect_local_cluster_state()")
+	}
+
 	role := strings.TrimSpace(nctx.Config.Spec.ClusterRole)
 	initControlPlane := nctx.Config.Spec.InitControlPlane
 	wantVersion := normalizeKubeVersion(strings.TrimSpace(nctx.Config.Spec.KubernetesVersion))
@@ -171,26 +176,27 @@ func ClassifyBootstrapAction(ctx context.Context, nctx *NodeContext) error {
 		return errors.New("spec.kubernetesVersion is required")
 	}
 
-	nctx.BootstrapState = &BootstrapState{}
+	state := &BootstrapState{}
+
+	// Preserve already-detected info if earlier steps populated it.
+	if nctx.BootstrapState != nil {
+		state.DetectedClusterVersion = nctx.BootstrapState.DetectedClusterVersion
+	}
+
 	switch role {
 	case "worker":
 		switch nctx.LocalClusterState.MembershipKind {
 		case LocalMembershipFresh:
-			nctx.BootstrapState.Action = BootstrapActionJoinWorker
-			return nil
+			state.Action = BootstrapActionJoinWorker
 
 		case LocalMembershipExistingWorker:
-			if nctx.BootstrapState.DetectedClusterVersion == "" {
-				nctx.BootstrapState.Action = BootstrapActionReconcileWorker
-				return nil
-			}
-
-			if versionEq(nctx.BootstrapState.DetectedClusterVersion, wantVersion) {
-				nctx.BootstrapState.Action = BootstrapActionReconcileWorker
+			if state.DetectedClusterVersion == "" {
+				state.Action = BootstrapActionReconcileWorker
+			} else if versionEq(state.DetectedClusterVersion, wantVersion) {
+				state.Action = BootstrapActionReconcileWorker
 			} else {
-				nctx.BootstrapState.Action = BootstrapActionUpgradeWorker
+				state.Action = BootstrapActionUpgradeWorker
 			}
-			return nil
 
 		case LocalMembershipExistingControlPlane, LocalMembershipPartial:
 			return fmt.Errorf("local state %q is invalid for worker role", nctx.LocalClusterState.MembershipKind)
@@ -203,23 +209,21 @@ func ClassifyBootstrapAction(ctx context.Context, nctx *NodeContext) error {
 		switch nctx.LocalClusterState.MembershipKind {
 		case LocalMembershipFresh:
 			if initControlPlane {
-				nctx.BootstrapState.Action = BootstrapActionInitControlPlane
+				state.Action = BootstrapActionInitControlPlane
 			} else {
-				nctx.BootstrapState.Action = BootstrapActionJoinControlPlane
+				state.Action = BootstrapActionJoinControlPlane
 			}
-			return nil
 
 		case LocalMembershipExistingControlPlane:
-			if nctx.BootstrapState.DetectedClusterVersion == "" {
+			if state.DetectedClusterVersion == "" {
 				return errors.New("existing control-plane state found, but detected cluster version is empty")
 			}
 
-			if versionEq(nctx.BootstrapState.DetectedClusterVersion, wantVersion) {
-				nctx.BootstrapState.Action = BootstrapActionReconcileControlPlane
+			if versionEq(state.DetectedClusterVersion, wantVersion) {
+				state.Action = BootstrapActionReconcileControlPlane
 			} else {
-				nctx.BootstrapState.Action = BootstrapActionUpgradeControlPlane
+				state.Action = BootstrapActionUpgradeControlPlane
 			}
-			return nil
 
 		case LocalMembershipExistingWorker:
 			return fmt.Errorf("local state %q is invalid for control-plane role", nctx.LocalClusterState.MembershipKind)
@@ -234,6 +238,10 @@ func ClassifyBootstrapAction(ctx context.Context, nctx *NodeContext) error {
 	default:
 		return fmt.Errorf("unsupported cluster role %q", role)
 	}
+
+	nctx.BootstrapState = state
+	klog.V(4).Infof("Bootstrap action: %+v", *state)
+	return nil
 }
 
 func InitControlPlane(ctx context.Context, nctx *NodeContext) error {