diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 7b0f252..4d03b17 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -51,9 +51,6 @@ func (r Runner) Run() error { if err := os.WriteFile(filepath.Join(terraformDir, r.Config.Talos.ConfigFileName), []byte(renderTerraformTFVars(r.Config)), 0644); err != nil { return err } - if err := ensureTalosConfig(generatedDir); err != nil { - return err - } if r.Config.Talos.AutoRunTerraform { if err := utils.RunCommandInDir(terraformDir, "terraform", "init"); err != nil { @@ -63,11 +60,18 @@ func (r Runner) Run() error { return err } } + if err := ensureTalosConfig(generatedDir, r.Config); err != nil { + return err + } if r.Config.Talos.AutoBootstrap { + configFile := filepath.Join("clusterconfig", fmt.Sprintf("%s-%s.yaml", r.Config.Talos.Cluster.Name, r.Config.Talos.Nodes[0].Name)) + if err := utils.RunCommandInDir(generatedDir, "talosctl", "apply-config", "--insecure", "--nodes="+r.Config.Talos.BootstrapNode, "--endpoints="+r.Config.Talos.BootstrapEndpoint, "--file="+configFile); err != nil { + return err + } if err := utils.RunCommandInDir(generatedDir, "talosctl", "bootstrap", "--talosconfig=./clusterconfig/talosconfig", "--endpoints="+r.Config.Talos.BootstrapEndpoint, "--nodes="+r.Config.Talos.BootstrapNode); err != nil { return err } - if err := utils.RunCommandInDir(generatedDir, "talosctl", "kubeconfig", "--talosconfig=./clusterconfig/talosconfig", "--endpoints="+r.Config.Talos.KubeconfigEndpoint, "--nodes="+r.Config.Talos.KubeconfigNode, "."); err != nil { + if err := utils.RunCommandInDir(generatedDir, "talosctl", "kubeconfig", "--talosconfig=./clusterconfig/talosconfig", "--nodes="+r.Config.Talos.KubeconfigNode, "."); err != nil { return err } } @@ -89,10 +93,11 @@ func ensureRepo(dir, repoURL, ref string) error { return utils.RunCommandInDir(dir, "git", "checkout", ref) } -func ensureTalosConfig(generatedDir string) error { +func ensureTalosConfig(generatedDir string, cfg config.Config) error { + talhelperPath := "talhelper" secretPath := filepath.Join(generatedDir, "talsecret.sops.yaml") if _, err := os.Stat(secretPath); os.IsNotExist(err) { - secrets, err := utils.RunCommandOutputInDir(generatedDir, "talhelper", "gensecret", "--from-configfile", "talconfig.yaml") + secrets, err := utils.RunCommandOutputInDir(generatedDir, talhelperPath, "gensecret") if err != nil { return err } @@ -100,7 +105,32 @@ func ensureTalosConfig(generatedDir string) error { return err } } - return utils.RunCommandInDir(generatedDir, "talhelper", "genconfig") + kubernetesVersion := cfg.Talos.Image.TalosVersion + talconfigPath := writeTalconfigWithKubernetesVersion(generatedDir, kubernetesVersion) + return utils.RunCommandInDir(generatedDir, talhelperPath, "genconfig", "--config-file", filepath.Base(talconfigPath), "--secret-file", filepath.Base(secretPath)) +} + +func writeTalconfigWithKubernetesVersion(generatedDir, kubernetesVersion string) string { + path := filepath.Join(generatedDir, "talconfig.yaml") + content, _ := os.ReadFile(path) + if strings.HasPrefix(kubernetesVersion, "v1.13.") { + kubernetesVersion = "v1.33.4" + } + text := string(content) + if strings.Contains(text, "\nkubernetesVersion:") { + start := strings.Index(text, "\nkubernetesVersion:") + 1 + end := strings.Index(text[start:], "\n") + if end == -1 { + text = text[:start] + "kubernetesVersion: " + kubernetesVersion + } else { + text = text[:start] + "kubernetesVersion: " + kubernetesVersion + text[start+end:] + } + _ = os.WriteFile(path, []byte(text), 0644) + return path + } + updated := strings.Replace(text, "\nendpoint:", "\nkubernetesVersion: "+kubernetesVersion+"\nendpoint:", 1) + _ = os.WriteFile(path, []byte(updated), 0644) + return path } func renderTerraformTFVars(cfg config.Config) string { diff --git a/internal/config/config.go b/internal/config/config.go index d53eb08..7c41e5f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "errors" "os" + "github.com/Pingu-Studio/MaidnCLI/internal/talos" "gopkg.in/yaml.v3" ) @@ -111,6 +112,9 @@ func Validate(cfg Config) error { if cfg.Talos.Image.TalosVersion == "" || cfg.Talos.Image.SchematicID == "" { return errors.New("talos version and schematicId are required") } + if err := talos.RequireCLICompatibility(cfg.Talos.Image.TalosVersion); err != nil { + return err + } if cfg.Talos.Image.UpdateMode != "manual" && cfg.Talos.Image.UpdateMode != "download" { return errors.New("talos image updateMode must be manual or download") } diff --git a/internal/talos/releases.go b/internal/talos/releases.go index 44e306b..987637d 100644 --- a/internal/talos/releases.go +++ b/internal/talos/releases.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "net/http" + "os/exec" + "strconv" "strings" "time" ) @@ -66,6 +68,64 @@ func NormalizeVersion(version string) string { return "v" + version } +func RequireCLICompatibility(version string) error { + version = NormalizeVersion(version) + if err := requireTalhelper(version); err != nil { + return err + } + return requireTalosctl(version) +} + +func requireTalhelper(version string) error { + output, err := exec.Command("talhelper", "--version").Output() + if err != nil { + return fmt.Errorf("talhelper is required for Talos %s; install a compatible version and try again", version) + } + major, minor, ok := parseToolVersion(string(output)) + if !ok || major < 3 || (major == 3 && minor < 1) { + return fmt.Errorf("talhelper %s may be incompatible with Talos %s; check that you have the right talhelper version", strings.TrimSpace(string(output)), version) + } + return nil +} + +func requireTalosctl(version string) error { + output, err := exec.Command("talosctl", "version", "--client", "--short").Output() + if err != nil { + return fmt.Errorf("talosctl is required for Talos %s; install a compatible version and try again", version) + } + major, minor, ok := parseToolVersion(string(output)) + if !ok { + return fmt.Errorf("unable to parse talosctl version %q; check that it matches Talos %s", strings.TrimSpace(string(output)), version) + } + talosMajor, talosMinor, ok := parseToolVersion(version) + if !ok || major != talosMajor || minor != talosMinor { + return fmt.Errorf("talosctl %s may be incompatible with Talos %s; check that you have the right talosctl version", strings.TrimSpace(string(output)), version) + } + return nil +} + +func parseToolVersion(raw string) (int, int, bool) { + raw = strings.TrimSpace(raw) + fields := strings.Fields(raw) + if len(fields) > 0 { + raw = fields[len(fields)-1] + } + raw = strings.TrimPrefix(raw, "v") + parts := strings.Split(raw, ".") + if len(parts) < 2 { + return 0, 0, false + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, false + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, false + } + return major, minor, true +} + func FactoryURL(version, arch string) string { version = NormalizeVersion(version) return fmt.Sprintf("https://factory.talos.dev/?arch=%s&extensions=-&extensions=%s&extensions=%s&extensions=%s&extensions=%s&platform=%s&target=%s&version=%s", diff --git a/internal/ui/wizard.go b/internal/ui/wizard.go index f223207..7d4d6e8 100644 --- a/internal/ui/wizard.go +++ b/internal/ui/wizard.go @@ -73,6 +73,9 @@ func RunBootstrapWizard(initial config.Config) (config.Config, error) { latestTalos := talos.LatestVersion() cfg.Talos.Image.TalosVersion = talos.NormalizeVersion(prompt(reader, "Talos version", fallback(cfg.Talos.Image.TalosVersion, latestTalos))) + if err := talos.RequireCLICompatibility(cfg.Talos.Image.TalosVersion); err != nil { + return cfg, err + } 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()