From 5fbc2846a146dc252d72db157402b285660de46ec389541ef0a0b1729e5d987f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sun, 29 Mar 2026 23:55:59 +0800 Subject: [PATCH] Can ctl init with index --- clitools/pkg/bootstrap/registry.go | 1 + clitools/pkg/bootstrap/runner.go | 176 ++++++++++++++++++++++++----- clitools/pkg/cmd/initcmd/init.go | 154 +++++++++++++++++++++++-- clitools/pkg/cmd/root/root.go | 12 +- clitools/pkg/node/kubeadm.go | 48 ++++---- 5 files changed, 334 insertions(+), 57 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,37 +35,121 @@ 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) 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 - } - } - return nil -} - func (r *Runner) RunNamedStep(ctx context.Context, name string) error { step, err := r.Registry.Get(name) if err != nil { @@ -60,3 +157,26 @@ func (r *Runner) RunNamedStep(ctx context.Context, name string) error { } return step(ctx, r.NodeCtx) } + +func (r *Runner) InitSteps() []StepInfo { + return r.initSteps +} + +func (r *Runner) Init(ctx context.Context) error { + 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) 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 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 {