feat: add bootstrap workflow

This commit is contained in:
eding 2026-07-15 20:39:39 +02:00
parent cc8fab6832
commit 85e87f1334
13 changed files with 583 additions and 3 deletions

13
.vscode/launch.json vendored
View file

@ -44,6 +44,19 @@
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "Run and Debug CICD Tool ghcr auth creation",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": [
"vault",
"create-ghcr-secret"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}

2
How2Auth.md Normal file
View file

@ -0,0 +1,2 @@
edingrech
dckr_pat_bfKKDH4g3qUxchs9UMLxFx2oTiU

1
MaidnCLI Submodule

@ -0,0 +1 @@
Subproject commit cc8fab6832a3a51bd072155f90f91354e440077a

View file

@ -11,6 +11,12 @@ go install github.com/go-delve/delve/cmd/dlv@latest
dlv version
```
## Commands
- `cicd-tool repo init --org <org> --flux-repo <repo>` creates the manifests and Flux repos
- `cicd-tool bootstrap` runs the user-facing wizard, saves a config YAML, clones the Talos IaC repo, writes `terraform.tfvars`, stages Talos images on Proxmox when enabled, and can execute Terraform, Talos bootstrap, and Flux bootstrap
- `cicd-tool bootstrap --config maidn-bootstrap.yaml` skips the wizard and uses the saved config
## TODO
create the file:
within the main flux repo dynamically changing the org and repo location based on the user's input

53
cmd/bootstrap.go Normal file
View file

@ -0,0 +1,53 @@
package cmd
import (
"fmt"
"os"
"github.com/Pingu-Studio/MaidnCLI/internal/bootstrap"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/ui"
"github.com/spf13/cobra"
)
var bootstrapConfigPath string
var bootstrapOutputPath string
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Bootstrap Talos and Flux from config or an interactive wizard.",
Run: runBootstrap,
}
func init() {
rootCmd.AddCommand(bootstrapCmd)
bootstrapCmd.Flags().StringVar(&bootstrapConfigPath, "config", "", "Path to bootstrap config YAML")
bootstrapCmd.Flags().StringVar(&bootstrapOutputPath, "out", "maidn-bootstrap.yaml", "Path to save generated config")
}
func runBootstrap(cmd *cobra.Command, args []string) {
var cfg config.Config
var err error
if bootstrapConfigPath != "" {
cfg, err = config.Load(bootstrapConfigPath)
} else {
cfg, err = ui.RunBootstrapWizard()
if err == nil {
err = config.Save(bootstrapOutputPath, cfg)
if err == nil {
fmt.Printf("[INFO] Saved config to %s\n", bootstrapOutputPath)
}
}
}
if err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
runner := bootstrap.Runner{Config: cfg}
if err = runner.Run(); err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
}

View file

@ -48,8 +48,10 @@ func runInitRepo(cmd *cobra.Command, args []string) {
fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
repoManager := github.NewRepoManager(orgName, manifestsRepoName, fluxRepoName)
repoManager.InitializeManifestsRepo()
repoManager.InitializeFluxRepo()
if err := repoManager.InitializeAll(); err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
fmt.Println("\n🎉 Onboarding complete! 🎉")
fmt.Printf("✅ Manifests repository: https://github.com/%s/%s\n", orgName, manifestsRepoName)

View file

@ -33,7 +33,7 @@ var createDockerSecretCmd = &cobra.Command{
var createGhcrSecretCmd = &cobra.Command{
Use: "create-ghcr-secret",
Short: "Creates secrets in Vault for GHCR.",
Short: "Creates secrets in Vault for GHCR and applies it to Kubernetes.",
Run: runCreateGhcrSecret,
}

22
docs/maintenance.md Normal file
View file

@ -0,0 +1,22 @@
# Cluster maintenance
## What the CLI handles now
- Bootstraps the Talos IaC repo
- Writes cluster-specific Terraform config
- Can trigger Terraform, Talos bootstrap, kubeconfig generation, and Flux bootstrap
- Can request Proxmox-side Talos ISO staging through Terraform inputs
## Manual operations still required
- Talos upgrades remain operator-driven
- Certificate rotation remains operator-driven
- etcd snapshots and restore drills remain operator-driven
## Candidate automation
- Add a `cicd-tool talos upgrade` workflow that updates version pins, stages images, and rolls nodes one by one
- Add a `cicd-tool talos certs check` workflow that reports expiry windows
- Add a scheduled GitHub Action in the Talos repo that opens PRs for new factory-compatible image pins
Use the standalone Talos repo maintenance guide for the actual runbook: `maidn/maidn-talos-proxmox/docs/maintenance.md`.

View file

@ -0,0 +1,159 @@
package bootstrap
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
ghrepo "github.com/Pingu-Studio/MaidnCLI/internal/github"
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
)
type Runner struct {
Config config.Config
}
func (r Runner) Run() error {
workspace := r.Config.WorkspaceDir
if err := os.MkdirAll(workspace, 0755); err != nil {
return err
}
if err := ghrepo.NewRepoManager(r.Config.GitHub.Org, "cicd-deployment-manifests", r.Config.Flux.RepoName).InitializeAll(); err != nil {
return err
}
repoDir := filepath.Join(workspace, r.Config.Talos.RepoDirName)
if err := ensureRepo(repoDir, r.Config.Talos.RepoURL, r.Config.Talos.RepoRef); err != nil {
return err
}
terraformDir := filepath.Join(repoDir, r.Config.Talos.TerraformDir)
generatedDir := filepath.Join(repoDir, r.Config.Talos.GeneratedDir)
if err := os.MkdirAll(generatedDir, 0755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(terraformDir, r.Config.Talos.ConfigFileName), []byte(renderTerraformTFVars(r.Config)), 0644); err != nil {
return err
}
if r.Config.Talos.AutoRunTerraform {
if err := utils.RunCommandInDir(terraformDir, "terraform", "init"); err != nil {
return err
}
if err := utils.RunCommandInDir(terraformDir, "terraform", "apply", "-auto-approve"); err != nil {
return err
}
}
if r.Config.Talos.AutoBootstrap {
if err := utils.RunCommandInDir(generatedDir, "talosctl", "bootstrap", "--talosconfig=./clusterconfig/talosconfig", "--nodes="+r.Config.Talos.BootstrapNode); err != nil {
return err
}
if err := utils.RunCommandInDir(generatedDir, "talosctl", "kubeconfig", "--talosconfig=./clusterconfig/talosconfig", "--nodes="+r.Config.Talos.KubeconfigNode, "."); err != nil {
return err
}
}
if r.Config.Talos.AutoBootstrapFlux {
if err := utils.RunCommand("flux", "bootstrap", "github", "--owner="+r.Config.Flux.Owner, "--repository="+r.Config.Flux.RepoName, "--branch="+r.Config.Flux.Branch, "--path="+r.Config.Flux.ClusterPath, "--cluster-domain="+r.Config.Flux.ClusterDomain); err != nil {
return err
}
}
return nil
}
func ensureRepo(dir, repoURL, ref string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return utils.RunCommand("git", "clone", "--branch", ref, repoURL, dir)
}
if err := utils.RunCommandInDir(dir, "git", "fetch", "origin"); err != nil {
return err
}
return utils.RunCommandInDir(dir, "git", "checkout", ref)
}
func renderTerraformTFVars(cfg config.Config) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("proxmox_api_url = %q\n", cfg.Talos.Proxmox.APIURL))
builder.WriteString(fmt.Sprintf("proxmox_node = %q\n", cfg.Talos.Proxmox.DefaultNode))
builder.WriteString(fmt.Sprintf("proxmox_api_token = %q\n", cfg.Talos.Proxmox.APIToken))
builder.WriteString(fmt.Sprintf("proxmox_pool = %q\n", cfg.Talos.Proxmox.Pool))
builder.WriteString(fmt.Sprintf("cluster_name = %q\n", cfg.Talos.Cluster.Name))
builder.WriteString(fmt.Sprintf("cluster_domain = %q\n", cfg.Talos.Cluster.Domain))
builder.WriteString(fmt.Sprintf("talos_factory_schematic_id = %q\n", cfg.Talos.Image.FactorySchematicID))
builder.WriteString(fmt.Sprintf("talos_version = %q\n", cfg.Talos.Image.TalosVersion))
builder.WriteString(fmt.Sprintf("cni_name = %q\n", cfg.Talos.Cluster.CNI))
builder.WriteString(fmt.Sprintf("talos_iso_file = %q\n", cfg.Talos.Image.TalosISOFile))
updateMode := cfg.Talos.Image.UpdateMode
if updateMode == "" {
updateMode = "manual"
}
storage := cfg.Talos.Image.Storage
if storage == "" {
storage = "local"
}
architecture := cfg.Talos.Image.Architecture
if architecture == "" {
architecture = "amd64"
}
builder.WriteString(fmt.Sprintf("talos_image_update_mode = %q\n", updateMode))
builder.WriteString(fmt.Sprintf("talos_image_storage = %q\n", storage))
builder.WriteString(fmt.Sprintf("talos_image_architecture = %q\n", architecture))
builder.WriteString(fmt.Sprintf("disk_storage = %q\n", cfg.Talos.Cluster.DiskStorage))
builder.WriteString(fmt.Sprintf("additional_disk_storage = %q\n", cfg.Talos.Cluster.AdditionalStorage))
builder.WriteString(fmt.Sprintf("dns_servers = [%s]\n", quoteList(cfg.Talos.Cluster.DNSServers)))
builder.WriteString(fmt.Sprintf("control_plane_vip = %q\n", cfg.Talos.Cluster.ControlPlaneVIP))
builder.WriteString("node_interfaces = {\n")
for node, iface := range cfg.Talos.Proxmox.NodeInterfaces {
builder.WriteString(fmt.Sprintf(" %q = %q\n", node, iface))
}
builder.WriteString("}\n")
builder.WriteString(fmt.Sprintf("create_vlan_interface = %t\n", cfg.Talos.Cluster.CreateVLANInterface))
builder.WriteString("image_cache_proxy = { enabled = false, ip = \"\", port = 3128 }\n")
builder.WriteString("nodes = [\n")
for _, node := range cfg.Talos.Nodes {
builder.WriteString(" {\n")
builder.WriteString(fmt.Sprintf(" name = %q\n", node.Name))
builder.WriteString(fmt.Sprintf(" vmid = %d\n", node.VMID))
builder.WriteString(fmt.Sprintf(" role = %q\n", node.Role))
builder.WriteString(fmt.Sprintf(" cores = %d\n", node.Cores))
builder.WriteString(fmt.Sprintf(" memory = %d\n", node.Memory))
builder.WriteString(fmt.Sprintf(" disk_size = %q\n", node.DiskSize))
if node.AdditionalDiskSize != "" {
builder.WriteString(fmt.Sprintf(" additional_disk_size = %q\n", node.AdditionalDiskSize))
}
builder.WriteString(fmt.Sprintf(" tags = [%s]\n", quoteList(node.Tags)))
builder.WriteString(fmt.Sprintf(" proxmox_node = %q\n", node.ProxmoxNode))
builder.WriteString(" networks = [\n")
for _, net := range node.Networks {
builder.WriteString(" {\n")
builder.WriteString(fmt.Sprintf(" mac_address = %q\n", net.MACAddress))
builder.WriteString(fmt.Sprintf(" cidr = %q\n", net.CIDR))
if net.IP != "" {
builder.WriteString(fmt.Sprintf(" ip = %q\n", net.IP))
}
if net.Gateway != "" {
builder.WriteString(fmt.Sprintf(" gateway = %q\n", net.Gateway))
}
builder.WriteString(fmt.Sprintf(" vlan_id = %d\n", net.VLANID))
builder.WriteString(" },\n")
}
builder.WriteString(" ]\n")
builder.WriteString(" },\n")
}
builder.WriteString("]\n")
return builder.String()
}
func quoteList(values []string) string {
quoted := make([]string, 0, len(values))
for _, value := range values {
quoted = append(quoted, fmt.Sprintf("%q", value))
}
return strings.Join(quoted, ", ")
}

65
internal/config/config.go Normal file
View file

@ -0,0 +1,65 @@
package config
import (
"errors"
"os"
"gopkg.in/yaml.v3"
)
func Load(path string) (Config, error) {
var cfg Config
data, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, Validate(cfg)
}
func Save(path string, cfg Config) error {
if err := Validate(cfg); err != nil {
return err
}
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func Validate(cfg Config) error {
if cfg.WorkspaceDir == "" {
return errors.New("workspaceDir is required")
}
if cfg.GitHub.Org == "" {
return errors.New("github.org is required")
}
if cfg.Flux.RepoName == "" || cfg.Flux.ClusterPath == "" {
return errors.New("flux repoName and clusterPath are required")
}
if cfg.Talos.RepoURL == "" || cfg.Talos.RepoDirName == "" {
return errors.New("talos repoUrl and repoDirName are required")
}
if cfg.Talos.TerraformDir == "" || cfg.Talos.GeneratedDir == "" || cfg.Talos.ConfigFileName == "" {
return errors.New("talos terraformDir, generatedDir, and configFileName are required")
}
if cfg.Talos.Proxmox.APIURL == "" || cfg.Talos.Proxmox.APIToken == "" {
return errors.New("talos proxmox apiUrl and apiToken are required")
}
if cfg.Talos.Cluster.Name == "" || cfg.Talos.Cluster.Domain == "" {
return errors.New("talos cluster name and domain are required")
}
if cfg.Talos.Image.FactorySchematicID == "" || cfg.Talos.Image.TalosVersion == "" || cfg.Talos.Image.TalosISOFile == "" {
return errors.New("talos image pins are required")
}
if cfg.Talos.Image.UpdateMode != "" && cfg.Talos.Image.UpdateMode != "manual" && cfg.Talos.Image.UpdateMode != "download" {
return errors.New("talos image updateMode must be manual or download")
}
if len(cfg.Talos.Nodes) == 0 {
return errors.New("at least one talos node is required")
}
return nil
}

87
internal/config/types.go Normal file
View file

@ -0,0 +1,87 @@
package config
type Config struct {
WorkspaceDir string `yaml:"workspaceDir"`
GitHub GitHubConfig `yaml:"github"`
Flux FluxConfig `yaml:"flux"`
Talos TalosConfig `yaml:"talos"`
}
type GitHubConfig struct {
Org string `yaml:"org"`
}
type FluxConfig struct {
RepoName string `yaml:"repoName"`
Branch string `yaml:"branch"`
ClusterPath string `yaml:"clusterPath"`
ClusterDomain string `yaml:"clusterDomain"`
Owner string `yaml:"owner"`
}
type TalosConfig struct {
RepoURL string `yaml:"repoUrl"`
RepoRef string `yaml:"repoRef"`
RepoDirName string `yaml:"repoDirName"`
TerraformDir string `yaml:"terraformDir"`
GeneratedDir string `yaml:"generatedDir"`
ConfigFileName string `yaml:"configFileName"`
AutoRunTerraform bool `yaml:"autoRunTerraform"`
AutoBootstrap bool `yaml:"autoBootstrap"`
AutoBootstrapFlux bool `yaml:"autoBootstrapFlux"`
BootstrapNode string `yaml:"bootstrapNode"`
KubeconfigNode string `yaml:"kubeconfigNode"`
Proxmox TalosProxmoxConfig `yaml:"proxmox"`
Cluster TalosClusterConfig `yaml:"cluster"`
Image TalosImageConfig `yaml:"image"`
Nodes []TalosNode `yaml:"nodes"`
}
type TalosProxmoxConfig struct {
APIURL string `yaml:"apiUrl"`
APIToken string `yaml:"apiToken"`
DefaultNode string `yaml:"defaultNode"`
Pool string `yaml:"pool"`
NodeInterfaces map[string]string `yaml:"nodeInterfaces"`
}
type TalosClusterConfig struct {
Name string `yaml:"name"`
Domain string `yaml:"domain"`
ControlPlaneVIP string `yaml:"controlPlaneVip"`
CNI string `yaml:"cni"`
DNSServers []string `yaml:"dnsServers"`
DiskStorage string `yaml:"diskStorage"`
AdditionalStorage string `yaml:"additionalStorage"`
CreateVLANInterface bool `yaml:"createVlanInterface"`
}
type TalosImageConfig struct {
FactorySchematicID string `yaml:"factorySchematicId"`
TalosVersion string `yaml:"talosVersion"`
TalosISOFile string `yaml:"talosIsoFile"`
UpdateMode string `yaml:"updateMode"`
Storage string `yaml:"storage"`
Architecture string `yaml:"architecture"`
}
type TalosNode struct {
Name string `yaml:"name"`
VMID int `yaml:"vmid"`
Role string `yaml:"role"`
Cores int `yaml:"cores"`
Memory int `yaml:"memory"`
DiskSize string `yaml:"diskSize"`
AdditionalDiskSize string `yaml:"additionalDiskSize,omitempty"`
Tags []string `yaml:"tags"`
ProxmoxNode string `yaml:"proxmoxNode"`
Networks []TalosNetwork `yaml:"networks"`
}
type TalosNetwork struct {
MACAddress string `yaml:"macAddress"`
CIDR string `yaml:"cidr"`
IP string `yaml:"ip,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
VLANID int `yaml:"vlanId"`
}

View file

@ -154,6 +154,12 @@ func (rm *RepoManager) InitializeFluxRepo() {
rm.setupRepository(fullRepoName, rm.createFluxStructure)
}
func (rm *RepoManager) InitializeAll() error {
rm.InitializeManifestsRepo()
rm.InitializeFluxRepo()
return nil
}
func (rm *RepoManager) setupRepository(fullRepoName string, structureFunc func(string)) {
tempDir, err := os.MkdirTemp("", "repo-setup-*")
if err != nil {

164
internal/ui/wizard.go Normal file
View file

@ -0,0 +1,164 @@
package ui
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
)
func RunBootstrapWizard() (config.Config, error) {
reader := bufio.NewReader(os.Stdin)
cfg := config.Config{}
cfg.WorkspaceDir = prompt(reader, "Workspace directory", ".\\maidn-workspace")
cfg.GitHub.Org = prompt(reader, "GitHub organization", "Pingu-Studio")
cfg.Flux.Owner = cfg.GitHub.Org
cfg.Flux.RepoName = prompt(reader, "Flux repo name", "mcast-maidn-cd-cluster")
cfg.Flux.Branch = prompt(reader, "Flux branch", "main")
cfg.Flux.ClusterPath = prompt(reader, "Flux cluster path", "./clusters/maidn-cd-0")
cfg.Flux.ClusterDomain = prompt(reader, "Flux cluster domain", "dev01.nid3.com")
cfg.Talos.RepoURL = prompt(reader, "Talos IaC repo URL", "https://github.com/Pingu-Studio/maidn-talos-proxmox.git")
cfg.Talos.RepoRef = prompt(reader, "Talos IaC repo ref", "main")
cfg.Talos.RepoDirName = prompt(reader, "Talos IaC checkout directory", "maidn-talos-proxmox")
cfg.Talos.TerraformDir = prompt(reader, "Terraform subdirectory", "terraform")
cfg.Talos.GeneratedDir = prompt(reader, "Generated subdirectory", "generated")
cfg.Talos.ConfigFileName = prompt(reader, "Terraform config filename", "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.9:8006/api2/json")
cfg.Talos.Proxmox.APIToken = prompt(reader, "Proxmox API token", "")
cfg.Talos.Proxmox.DefaultNode = prompt(reader, "Default Proxmox node", "pingu4")
cfg.Talos.Proxmox.Pool = prompt(reader, "Proxmox pool", "")
cfg.Talos.Proxmox.NodeInterfaces = promptMap(reader, "Node interfaces map (node=iface,comma separated)", map[string]string{"pingu4": "enp6s0"})
cfg.Talos.Cluster.Name = prompt(reader, "Talos cluster name", "proxmox-talos-dev01-cluster")
cfg.Talos.Cluster.Domain = prompt(reader, "Talos cluster domain", "dev01.nid3.com")
cfg.Talos.Cluster.ControlPlaneVIP = prompt(reader, "Control plane VIP", "192.168.43.7")
cfg.Talos.Cluster.CNI = prompt(reader, "CNI", "flannel")
cfg.Talos.Cluster.DNSServers = promptList(reader, "DNS servers (comma separated)", []string{"192.168.0.222", "1.1.1.1"})
cfg.Talos.Cluster.DiskStorage = prompt(reader, "Primary disk storage", "local-lvm")
cfg.Talos.Cluster.AdditionalStorage = prompt(reader, "Additional disk storage", "local-lvm")
cfg.Talos.Cluster.CreateVLANInterface = promptBool(reader, "Create VLAN interfaces", true)
cfg.Talos.Image.FactorySchematicID = prompt(reader, "Talos factory schematic ID", "ddf38e55e30aa9b2bb0b765054ed63444dc00244ab8bb4ad4ad92486602285f8")
cfg.Talos.Image.TalosVersion = prompt(reader, "Talos version", "v1.11.1")
cfg.Talos.Image.TalosISOFile = prompt(reader, "Talos ISO file", "local:iso/talos-1.11.1.iso")
cfg.Talos.Image.UpdateMode = prompt(reader, "Talos image update mode (manual/download)", "manual")
cfg.Talos.Image.Storage = prompt(reader, "Talos image storage", "local")
cfg.Talos.Image.Architecture = prompt(reader, "Talos image architecture", "amd64")
cfg.Talos.Nodes = promptNodes(reader, cfg.Talos.Proxmox.DefaultNode)
if len(cfg.Talos.Nodes) > 0 {
cfg.Talos.BootstrapNode = prompt(reader, "Talos bootstrap node IP", cfg.Talos.Nodes[0].Networks[0].IP)
cfg.Talos.KubeconfigNode = prompt(reader, "Talos kubeconfig node IP", cfg.Talos.Nodes[0].Networks[0].IP)
}
return cfg, nil
}
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')
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"
}
fmt.Printf("%s [y/n, default %s]: ", label, defaultValue)
value, _ := reader.ReadString('\n')
value = strings.TrimSpace(strings.ToLower(value))
if value == "" {
return fallback
}
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 promptMap(reader *bufio.Reader, label string, fallback map[string]string) map[string]string {
pairs := make([]string, 0, len(fallback))
for key, value := range fallback {
pairs = append(pairs, key+"="+value)
}
value := prompt(reader, label, strings.Join(pairs, ","))
result := map[string]string{}
for _, pair := range strings.Split(value, ",") {
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
result[parts[0]] = parts[1]
}
}
return result
}
func promptNodes(reader *bufio.Reader, defaultNode string) []config.TalosNode {
countValue := prompt(reader, "Number of Talos nodes", "3")
count, err := strconv.Atoi(countValue)
if err != nil || count < 1 {
count = 3
}
result := make([]config.TalosNode, 0, count)
for i := 0; i < count; i++ {
name := prompt(reader, fmt.Sprintf("Node %d name", i+1), fmt.Sprintf("talos-node-%02d", i+1))
role := prompt(reader, fmt.Sprintf("Node %d role", i+1), map[bool]string{i == 0: "controlplane", i != 0: "worker"}[true])
vmid, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d VMID", i+1), fmt.Sprintf("10%d", 50+i)))
cores, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d cores", i+1), "4"))
memory, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d memory MB", i+1), "8192"))
diskSize := prompt(reader, fmt.Sprintf("Node %d disk size", i+1), "64G")
proxmoxNode := prompt(reader, fmt.Sprintf("Node %d Proxmox node", i+1), defaultNode)
tags := promptList(reader, fmt.Sprintf("Node %d tags", i+1), []string{"talos", role})
mac := prompt(reader, fmt.Sprintf("Node %d primary MAC", i+1), "")
cidr := prompt(reader, fmt.Sprintf("Node %d primary CIDR", i+1), "192.168.43.0/28")
ip := prompt(reader, fmt.Sprintf("Node %d primary IP", i+1), "")
gateway := prompt(reader, fmt.Sprintf("Node %d gateway", i+1), "192.168.43.1")
vlanID, _ := strconv.Atoi(prompt(reader, fmt.Sprintf("Node %d VLAN ID", i+1), "43"))
result = append(result, config.TalosNode{
Name: name,
VMID: vmid,
Role: role,
Cores: cores,
Memory: memory,
DiskSize: diskSize,
Tags: tags,
ProxmoxNode: proxmoxNode,
Networks: []config.TalosNetwork{{
MACAddress: mac,
CIDR: cidr,
IP: ip,
Gateway: gateway,
VLANID: vlanID,
}},
})
}
return result
}