maidn-cli/internal/ui/wizard.go

270 lines
10 KiB
Go

package ui
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/proxmox"
"github.com/Pingu-Studio/MaidnCLI/internal/talos"
)
func RunBootstrapWizard(initial config.Config) (config.Config, error) {
reader := bufio.NewReader(os.Stdin)
cfg := initial
cwd, _ := os.Getwd()
if cfg.WorkspaceDir == "" {
cfg.WorkspaceDir = cwd
}
cfg.Git.Provider = "forgejo"
cfg.Git.BaseURL = prompt(reader, "Git server URL", fallback(cfg.Git.BaseURL, "https://git.pingu.pw"))
cfg.Git.Username = prompt(reader, "Git username", fallback(cfg.Git.Username, "eding"))
cfg.Git.Owner = prompt(reader, "Repo owner/user", fallback(cfg.Git.Owner, "Maidn"))
cfg.Git.Token = promptSecret(reader, "Forgejo PAT", cfg.Git.Token)
cfg.WorkspaceDir = prompt(reader, "Working directory", cfg.WorkspaceDir)
cfg.Git.CloneParent = prompt(reader, "Where should repos be cloned locally", fallback(cfg.Git.CloneParent, cfg.WorkspaceDir))
cfg.Flux.RepoName = prompt(reader, "Flux repo name", fallback(cfg.Flux.RepoName, "test-maidn-cd-cluster"))
cfg.Flux.ManifestsRepo = prompt(reader, "Manifests repo name", fallback(cfg.Flux.ManifestsRepo, "cicd-deployment-manifests"))
cfg.Flux.Branch = "main"
cfg.Flux.ClusterPath = "./clusters/maidn-cd-0"
cfg.Templates.TalosRepoURL = prompt(reader, "Talos template repo URL", fallback(cfg.Templates.TalosRepoURL, "https://git.pingu.pw/Maidn/maidn-talos-proxmox.git"))
fmt.Printf("Template repos are cloned under: %s\n", cfg.Git.CloneParent)
cfg.Templates.TalosRepoRef = "main"
cfg.Talos.RepoDirName = "maidn-talos-proxmox"
cfg.Talos.TerraformDir = "terraform"
cfg.Talos.GeneratedDir = "generated"
cfg.Talos.ConfigFileName = "terraform.tfvars"
cfg.Talos.AutoRunTerraform = promptBool(reader, "Run terraform automatically", cfg.Talos.AutoRunTerraform || !hasInitial(initial))
cfg.Talos.AutoBootstrap = promptBool(reader, "Run talos bootstrap automatically", cfg.Talos.AutoBootstrap || !hasInitial(initial))
cfg.Talos.AutoBootstrapFlux = promptBool(reader, "Run flux bootstrap automatically", cfg.Talos.AutoBootstrapFlux || !hasInitial(initial))
cfg.Talos.Proxmox.APIURL = prompt(reader, "Proxmox API URL", fallback(cfg.Talos.Proxmox.APIURL, "https://192.168.0.15:8006"))
cfg.Talos.Proxmox.APITokenID = prompt(reader, "Proxmox API token ID", fallback(cfg.Talos.Proxmox.APITokenID, "root@pam!iac"))
cfg.Talos.Proxmox.APITokenSecret = promptSecret(reader, "Proxmox API token secret", cfg.Talos.Proxmox.APITokenSecret)
cfg.Talos.Proxmox.Insecure = true
cfg.Talos.Proxmox.Pool = prompt(reader, "Proxmox pool", cfg.Talos.Proxmox.Pool)
discovered, err := discoverProxmox(reader, cfg)
if err != nil {
return cfg, err
}
printNodes(discovered)
cfg.Talos.Proxmox.DefaultNode = prompt(reader, "Default Proxmox node", fallback(cfg.Talos.Proxmox.DefaultNode, discovered[0].Name))
cfg.Talos.Proxmox.NodeInterfaces = map[string]string{}
cfg.Talos.Proxmox.NodeAddresses = map[string]string{}
for _, node := range discovered {
fallbackIface := first(node.Networks, "vmbr0")
cfg.Talos.Proxmox.NodeInterfaces[node.Name] = prompt(reader, fmt.Sprintf("Primary interface for %s", node.Name), fallbackIface)
cfg.Talos.Proxmox.NodeAddresses[node.Name] = prompt(reader, fmt.Sprintf("SSH/API hostname or IP for %s", node.Name), fallback(cfg.Talos.Proxmox.NodeAddresses[node.Name], cfg.Talos.Proxmox.APIURLHost()))
}
cfg.Talos.Cluster.Name = prompt(reader, "Talos cluster name", fallback(cfg.Talos.Cluster.Name, "proxmox-talos-dev02-cluster"))
cfg.Talos.Cluster.Domain = prompt(reader, "Talos cluster domain", fallback(cfg.Talos.Cluster.Domain, "dev02.nid3.com"))
cfg.Flux.ClusterDomain = cfg.Talos.Cluster.Domain
cfg.Talos.Cluster.ControlPlaneVIP = prompt(reader, "Control plane VIP", fallback(cfg.Talos.Cluster.ControlPlaneVIP, "192.168.45.2"))
cfg.Talos.Cluster.DNSServers = promptList(reader, "DNS servers", fallbackList(cfg.Talos.Cluster.DNSServers, []string{"192.168.0.222", "1.1.1.1"}))
cfg.Talos.Cluster.DiskStorage = prompt(reader, "Primary disk storage", fallback(cfg.Talos.Cluster.DiskStorage, firstStorageByCapability(discovered, storageSupportsDisk, "local-lvm")))
cfg.Talos.Cluster.AdditionalStorage = prompt(reader, "Additional disk storage", fallback(cfg.Talos.Cluster.AdditionalStorage, cfg.Talos.Cluster.DiskStorage))
cfg.Talos.Cluster.CreateVLANInterface = promptBool(reader, "Create VLAN interfaces", true)
latestTalos := talos.LatestVersion()
cfg.Talos.Image.TalosVersion = talos.NormalizeVersion(prompt(reader, "Talos version", fallback(cfg.Talos.Image.TalosVersion, latestTalos)))
cfg.Talos.Image.Architecture = chooseArchitecture(reader, cfg.Talos.Image.Architecture)
fmt.Printf("Talos factory: %s\n", talos.FactoryURL(cfg.Talos.Image.TalosVersion, fallback(cfg.Talos.Image.Architecture, "amd64")))
schematicID, err := talos.ResolveSchematicID()
if err != nil {
return cfg, err
}
cfg.Talos.Image.SchematicID = schematicID
fmt.Printf("Talos schematic: %s\n", cfg.Talos.Image.SchematicID)
cfg.Talos.Image.UpdateMode = "download"
cfg.Talos.Image.Storage = prompt(reader, "ISO storage", fallback(cfg.Talos.Image.Storage, firstStorageByCapability(discovered, storageSupportsISO, "local")))
nodePreset := prompt(reader, "Cluster size preset (single/ha/custom)", "single")
cfg.Talos.Nodes = buildNodes(reader, cfg, discovered, nodePreset)
cfg.Talos.BootstrapNode = cfg.Talos.Nodes[0].Networks[0].IP
cfg.Talos.KubeconfigNode = cfg.Talos.Nodes[0].Networks[0].IP
return cfg, nil
}
func printNodes(nodes []proxmox.NodeInfo) {
fmt.Println("Available Proxmox nodes:")
for _, node := range nodes {
fmt.Printf("- %s: free %dMB RAM, total %dMB RAM, free root %dGB, networks=%s\n", node.Name, node.FreeMemoryMB, node.TotalMemoryMB, node.FreeDiskGB, strings.Join(node.Networks, ","))
}
}
func chooseArchitecture(reader *bufio.Reader, current string) string {
fmt.Println("Architecture options: 1) amd64 2) arm64")
defaultChoice := "1"
if current == "arm64" {
defaultChoice = "2"
}
choice := prompt(reader, "Architecture", defaultChoice)
if choice == "2" {
return "arm64"
}
return "amd64"
}
func buildNodes(reader *bufio.Reader, cfg config.Config, discovered []proxmox.NodeInfo, preset string) []config.TalosNode {
switch preset {
case "ha":
return buildPresetNodes(reader, cfg, discovered, 3, 2)
case "custom":
count, _ := strconv.Atoi(prompt(reader, "Number of nodes", "3"))
if count < 1 {
count = 1
}
workers := count - 1
if workers < 0 {
workers = 0
}
return buildPresetNodes(reader, cfg, discovered, count, workers)
default:
return buildPresetNodes(reader, cfg, discovered, 1, 0)
}
}
func buildPresetNodes(reader *bufio.Reader, cfg config.Config, discovered []proxmox.NodeInfo, count, workers int) []config.TalosNode {
result := make([]config.TalosNode, 0, count)
for i := 0; i < count; i++ {
role := "controlplane"
if i > 0 && i <= workers {
role = "worker"
}
name := fmt.Sprintf("talos-%s-%02d", map[bool]string{role == "controlplane": "cp", role != "controlplane": "wk"}[true], i+1)
defaultNode := discovered[min(i, len(discovered)-1)].Name
proxmoxNode := prompt(reader, fmt.Sprintf("Node %d Proxmox node", i+1), defaultNode)
memoryDefault := "4096"
coresDefault := "2"
diskDefault := "48G"
if role == "worker" {
memoryDefault = "2048"
coresDefault = "2"
diskDefault = "32G"
}
ip := prompt(reader, fmt.Sprintf("Node %d IP", i+1), "")
gateway := prompt(reader, fmt.Sprintf("Node %d gateway", i+1), "192.168.45.1")
cidr := prompt(reader, fmt.Sprintf("Node %d CIDR", i+1), "192.168.45.0/24")
vlanID, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d VLAN ID", i+1), "45"))
vmid, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d VMID", i+1), fmt.Sprintf("12%02d", i)))
memory, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d memory MB", i+1), memoryDefault))
cores, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d cores", i+1), coresDefault))
result = append(result, config.TalosNode{
Name: name,
VMID: vmid,
Role: role,
Cores: cores,
Memory: memory,
DiskSize: prompt(reader, fmt.Sprintf("Node %d disk size", i+1), diskDefault),
Tags: []string{"talos", role},
ProxmoxNode: proxmoxNode,
Networks: []config.TalosNetwork{{
MACAddress: prompt(reader, fmt.Sprintf("Node %d MAC", i+1), ""),
CIDR: cidr,
IP: ip,
Gateway: gateway,
VLANID: vlanID,
}},
})
}
return result
}
func prompt(reader *bufio.Reader, label, fallback string) string {
if fallback != "" {
fmt.Printf("%s [%s]: ", label, fallback)
} else {
fmt.Printf("%s: ", label)
}
value, _ := reader.ReadString('\n')
fmt.Println()
value = strings.TrimSpace(value)
if value == "" {
return fallback
}
return value
}
func promptBool(reader *bufio.Reader, label string, fallback bool) bool {
defaultValue := "y"
if !fallback {
defaultValue = "n"
}
value := prompt(reader, label+" [y/n, default "+defaultValue+"]", "")
if value == "" {
return fallback
}
value = strings.ToLower(value)
return value == "y" || value == "yes"
}
func promptList(reader *bufio.Reader, label string, fallback []string) []string {
value := prompt(reader, label, strings.Join(fallback, ","))
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func fallback(value, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}
func fallbackList(value, defaultValue []string) []string {
if len(value) == 0 {
return defaultValue
}
return value
}
func hasInitial(cfg config.Config) bool {
return cfg.Talos.Proxmox.APIURL != "" || cfg.Git.Token != ""
}
func first(values []string, fallback string) string {
if len(values) == 0 {
return fallback
}
return values[0]
}
func firstStorageByCapability(nodes []proxmox.NodeInfo, predicate func(proxmox.StorageInfo) bool, fallback string) string {
for _, node := range nodes {
for _, storage := range node.Storages {
if predicate(storage) {
return storage.Name
}
}
}
return fallback
}
func storageSupportsISO(storage proxmox.StorageInfo) bool {
return storage.SupportsISO
}
func storageSupportsDisk(storage proxmox.StorageInfo) bool {
return storage.SupportsDisk
}
func min(a, b int) int {
if a < b {
return a
}
return b
}