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 }