maidn-cli/internal/ui/wizard.go

230 lines
8.3 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() (config.Config, error) {
reader := bufio.NewReader(os.Stdin)
cfg := config.Config{}
cfg.WorkspaceDir = prompt(reader, "Workspace directory", ".\\maidn-workspace")
cfg.Git.Provider = "forgejo"
cfg.Git.BaseURL = prompt(reader, "Git server URL", "https://git.pingu.pw")
cfg.Git.Username = prompt(reader, "Git username", "eding")
cfg.Git.Owner = prompt(reader, "Repo owner/user", "Maidn")
cfg.Git.Token = prompt(reader, "Forgejo access token", "")
cfg.Git.CloneParent = prompt(reader, "Clone directory", cfg.WorkspaceDir)
cfg.Flux.RepoName = prompt(reader, "Flux repo name", "test-maidn-cd-cluster")
cfg.Flux.ManifestsRepo = prompt(reader, "Manifests repo name", "cicd-deployment-manifests")
cfg.Flux.Branch = "main"
cfg.Flux.ClusterPath = "./clusters/maidn-cd-0"
cfg.Templates.TalosRepoURL = prompt(reader, "Talos template repo URL", "https://git.pingu.pw/Maidn/maidn-talos-proxmox.git")
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", true)
cfg.Talos.AutoBootstrap = promptBool(reader, "Run talos bootstrap automatically", true)
cfg.Talos.AutoBootstrapFlux = promptBool(reader, "Run flux bootstrap automatically", true)
cfg.Talos.Proxmox.APIURL = prompt(reader, "Proxmox API URL", "https://192.168.0.15:8006")
cfg.Talos.Proxmox.APITokenID = prompt(reader, "Proxmox API token ID", "root@pam!iac")
cfg.Talos.Proxmox.APITokenSecret = prompt(reader, "Proxmox API token secret", "")
cfg.Talos.Proxmox.Insecure = true
cfg.Talos.Proxmox.Pool = prompt(reader, "Proxmox pool", "")
discovered, err := proxmox.New(cfg.Talos.Proxmox.APIURL, cfg.Talos.Proxmox.APITokenID, cfg.Talos.Proxmox.APITokenSecret, cfg.Talos.Proxmox.Insecure).Discover()
if err != nil {
return cfg, err
}
printNodes(discovered)
cfg.Talos.Proxmox.DefaultNode = prompt(reader, "Default Proxmox node", discovered[0].Name)
cfg.Talos.Proxmox.NodeInterfaces = map[string]string{}
for _, node := range discovered {
fallback := first(node.Networks, "vmbr0")
cfg.Talos.Proxmox.NodeInterfaces[node.Name] = prompt(reader, fmt.Sprintf("Primary interface for %s", node.Name), fallback)
}
cfg.Talos.Cluster.Name = prompt(reader, "Talos cluster name", "proxmox-talos-dev02-cluster")
cfg.Talos.Cluster.Domain = prompt(reader, "Talos cluster domain", "dev02.nid3.com")
cfg.Flux.ClusterDomain = cfg.Talos.Cluster.Domain
cfg.Talos.Cluster.ControlPlaneVIP = prompt(reader, "Control plane VIP", "192.168.45.2")
cfg.Talos.Cluster.DNSServers = promptList(reader, "DNS servers", []string{"192.168.0.222", "1.1.1.1"})
cfg.Talos.Cluster.DiskStorage = prompt(reader, "Primary disk storage", firstStorage(discovered, true, "local-lvm"))
cfg.Talos.Cluster.AdditionalStorage = prompt(reader, "Additional disk storage", cfg.Talos.Cluster.DiskStorage)
cfg.Talos.Cluster.CreateVLANInterface = promptBool(reader, "Create VLAN interfaces", true)
latestTalos := talos.LatestVersion()
cfg.Talos.Image.TalosVersion = prompt(reader, "Talos version", latestTalos)
fmt.Printf("Talos factory: %s\n", talos.FactoryHint(cfg.Talos.Image.TalosVersion))
cfg.Talos.Image.FactorySchematicID = prompt(reader, "Talos factory schematic ID", "")
cfg.Talos.Image.UpdateMode = "download"
cfg.Talos.Image.Storage = prompt(reader, "ISO storage", firstStorage(discovered, false, "local"))
cfg.Talos.Image.Architecture = chooseArchitecture(reader)
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) string {
fmt.Println("Architecture options: 1) amd64 2) arm64")
choice := prompt(reader, "Architecture", "1")
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 first(values []string, fallback string) string {
if len(values) == 0 {
return fallback
}
return values[0]
}
func firstStorage(nodes []proxmox.NodeInfo, iso bool, fallback string) string {
for _, node := range nodes {
for _, storage := range node.Storages {
if iso && storage.SupportsISO {
return storage.Name
}
if !iso {
return storage.Name
}
}
}
return fallback
}
func min(a, b int) int {
if a < b {
return a
}
return b
}