package initcmd import ( "bufio" "fmt" "os" "sort" "strconv" "strings" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/klog/v2" "undecided.project/monok8s/pkg/bootstrap" "undecided.project/monok8s/pkg/config" types "undecided.project/monok8s/pkg/apis/monok8s/v1alpha1" "undecided.project/monok8s/pkg/templates" ) func NewCmdInit(_ *genericclioptions.ConfigFlags) *cobra.Command { var configPath string var envFile string cmd := &cobra.Command{ Use: "init [list|STEPSEL] [--config path | --env-file path]", Short: "Bootstrap this node (from config file or env file)", Long: `Run the node bootstrap process. You can provide configuration in two ways: --config PATH Load MonoKSConfig YAML --env-file PATH Load MKS_* variables from env file and render config STEPSEL allows running specific steps instead of the full sequence. Supported formats: 3 Run step 3 1-3 Run steps 1 through 3 -3 Run steps from start through 3 3- Run steps from 3 to the end 1,3,5 Run specific steps 9-10,15 Combine ranges and individual steps `, Example: ` # Run full bootstrap using config file ctl init --config /etc/monok8s/config.yaml # Run full bootstrap using env file ctl init --env-file /opt/monok8s/config/cluster.env # List steps ctl init list # Run selected steps ctl init 1-3 --env-file /opt/monok8s/config/cluster.env ctl init 3- --config /etc/monok8s/config.yaml `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if strings.TrimSpace(configPath) != "" && strings.TrimSpace(envFile) != "" { return fmt.Errorf("--config and --env-file are mutually exclusive") } if strings.TrimSpace(envFile) != "" { if err := loadEnvFile(envFile); err != nil { return fmt.Errorf("load env file %q: %w", envFile, err) } } var cfg *types.MonoKSConfig // or value, depending on your API switch { case strings.TrimSpace(envFile) != "": if err := loadEnvFile(envFile); err != nil { return fmt.Errorf("load env file %q: %w", envFile, err) } vals := templates.LoadTemplateValuesFromEnv() rendered := templates.DefaultMonoKSConfig(vals) cfg = &rendered default: path, err := (config.Loader{}).ResolvePath(configPath) if err != nil { return err } loaded, err := (config.Loader{}).Load(path) if err != nil { return err } cfg = loaded klog.InfoS("starting init", "config", path, "node", cfg.Spec.NodeName, "envFile", envFile) } 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 := 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 nil } if len(args) == 0 { return runner.Init(cmd.Context()) } steps := runner.InitSteps() sel, err := parseStepSelection(args[0], len(steps)) if err != nil { return err } klog.InfoS("Running selected init steps", "steps", sel.Indices) return runner.InitSelected(cmd.Context(), sel) }, } cmd.Flags().StringVarP(&configPath, "config", "c", "", "path to MonoKSConfig yaml") cmd.Flags().StringVar(&envFile, "env-file", "", "path to env file containing MKS_* variables") return cmd } func loadEnvFile(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, "=") if !ok { return fmt.Errorf("line %d: expected KEY=VALUE", lineNum) } key = strings.TrimSpace(key) val = strings.TrimSpace(val) if key == "" { return fmt.Errorf("line %d: empty variable name", lineNum) } // Remove matching single or double quotes around the whole value. if len(val) >= 2 { if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') { val = val[1 : len(val)-1] } } if err := os.Setenv(key, val); err != nil { return fmt.Errorf("line %d: set %q: %w", lineNum, key, err) } } if err := scanner.Err(); err != nil { return err } return nil } 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{}{} for _, item := range strings.Split(raw, ",") { item = strings.TrimSpace(item) if item == "" { return bootstrap.StepSelection{}, fmt.Errorf("invalid empty selector in %q", raw) } // Range or open-ended range if strings.Contains(item, "-") { if strings.Count(item, "-") != 1 { return bootstrap.StepSelection{}, fmt.Errorf("invalid range %q", item) } parts := strings.SplitN(item, "-", 2) left := strings.TrimSpace(parts[0]) right := strings.TrimSpace(parts[1]) var start, end int switch { case left == "" && right == "": return bootstrap.StepSelection{}, fmt.Errorf("invalid range %q", item) 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 } if a > b { return bootstrap.StepSelection{}, fmt.Errorf("invalid descending range %q", item) } start, end = a, b } for i := start; i <= end; i++ { selected[i] = struct{}{} } continue } // Single step n, err := parseStepNumber(item, max) if err != nil { return bootstrap.StepSelection{}, err } selected[n] = struct{}{} } indices := make([]int, 0, len(selected)) for n := range selected { indices = append(indices, n) } sort.Ints(indices) return bootstrap.StepSelection{Indices: indices}, 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 }