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}", "cwd": "${workspaceFolder}",
"console": "integratedTerminal" "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 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 ## TODO
create the file: create the file:
within the main flux repo dynamically changing the org and repo location based on the user's input 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.") fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
repoManager := github.NewRepoManager(orgName, manifestsRepoName, fluxRepoName) repoManager := github.NewRepoManager(orgName, manifestsRepoName, fluxRepoName)
repoManager.InitializeManifestsRepo() if err := repoManager.InitializeAll(); err != nil {
repoManager.InitializeFluxRepo() fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
fmt.Println("\n🎉 Onboarding complete! 🎉") fmt.Println("\n🎉 Onboarding complete! 🎉")
fmt.Printf("✅ Manifests repository: https://github.com/%s/%s\n", orgName, manifestsRepoName) 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{ var createGhcrSecretCmd = &cobra.Command{
Use: "create-ghcr-secret", 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, 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) 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)) { func (rm *RepoManager) setupRepository(fullRepoName string, structureFunc func(string)) {
tempDir, err := os.MkdirTemp("", "repo-setup-*") tempDir, err := os.MkdirTemp("", "repo-setup-*")
if err != nil { 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
}