272 lines
10 KiB
Go
272 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.BootstrapEndpoint = cfg.Talos.BootstrapNode
|
|
cfg.Talos.KubeconfigNode = cfg.Talos.Nodes[0].Networks[0].IP
|
|
cfg.Talos.KubeconfigEndpoint = cfg.Talos.KubeconfigNode
|
|
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
|
|
}
|