penguin/monok8s

k8s image for Mono Gateway Dev Kit

clitools/pkg/cmd/initcmd/init.go

raw ยท 6915 bytes

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
}