package bootstrap import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" 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 := config.Save(filepath.Join(workspace, "maidn-bootstrap.resolved.yaml"), r.Config); err != nil { return err } if err := os.MkdirAll(r.Config.Git.CloneParent, 0755); err != nil { return err } manifestsURL := forgejo.CloneURL(r.Config.Git.BaseURL, r.Config.Git.Owner, r.Config.Flux.ManifestsRepo) fluxConfig := ghrepo.BuildFluxConfig(strings.ToLower(r.Config.Git.Owner), manifestsURL, r.Config.Flux.ManifestsRepo) manager := forgejo.NewRepoManager(r.Config.Git.BaseURL, r.Config.Git.Token, r.Config.Git.Owner, r.Config.Git.Username, r.Config.Flux.ManifestsRepo, r.Config.Flux.RepoName) if err := manager.InitializeAll( func(dir string) error { return ghrepo.WriteManifestsStructure(dir, r.Config.Flux.ManifestsRepo) }, func(dir string) error { return ghrepo.WriteFluxStructure(dir, r.Config.Flux.RepoName, r.Config.Flux.ClusterPath, fluxConfig) }, ); err != nil { return err } repoDir := filepath.Join(r.Config.Git.CloneParent, r.Config.Talos.RepoDirName) if err := ensureRepo(repoDir, r.Config.Templates.TalosRepoURL, r.Config.Templates.TalosRepoRef); 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 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 := retryTalosCommand(generatedDir, "apply-config", "--insecure", "--nodes="+r.Config.Talos.BootstrapNode, "--endpoints="+r.Config.Talos.BootstrapEndpoint, "--file="+configFile); err != nil { return err } if err := retryTalosCommand(generatedDir, "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", "--nodes="+r.Config.Talos.KubeconfigNode, "."); err != nil { return err } } if r.Config.Talos.AutoBootstrapFlux { if err := utils.RunCommand("flux", "bootstrap", "git", "--url="+forgejo.CloneURL(r.Config.Git.BaseURL, r.Config.Git.Owner, r.Config.Flux.RepoName), "--branch="+r.Config.Flux.Branch, "--path="+r.Config.Flux.ClusterPath); 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 } if err := utils.RunCommandInDir(dir, "git", "checkout", ref); err != nil { return err } return utils.RunCommandInDir(dir, "git", "pull", "--ff-only", "origin", ref) } func retryTalosCommand(dir string, args ...string) error { var err error for range 120 { if err = utils.RunCommandInDir(dir, "talosctl", args...); err == nil { return nil } time.Sleep(5 * time.Second) } return err } 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, talhelperPath, "gensecret") if err != nil { return err } if err := os.WriteFile(secretPath, secrets, 0600); err != nil { return err } } 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 { 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.APITokenID+"="+cfg.Talos.Proxmox.APITokenSecret)) 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.SchematicID)) builder.WriteString(fmt.Sprintf("talos_version = %q\n", cfg.Talos.Image.TalosVersion)) builder.WriteString("cni_name = \"flannel\"\n") isoStorage := cfg.Talos.Image.Storage if isoStorage == cfg.Talos.Cluster.DiskStorage { isoStorage = "local" } isoFilename := fmt.Sprintf("talos-%s-%s-%s.iso", strings.TrimPrefix(cfg.Talos.Image.TalosVersion, "v"), cfg.Talos.Image.SchematicID[:12], cfg.Talos.Image.Architecture) builder.WriteString(fmt.Sprintf("talos_iso_file = %q\n", fmt.Sprintf("%s:iso/%s", isoStorage, isoFilename))) builder.WriteString(fmt.Sprintf("talos_image_update_mode = %q\n", cfg.Talos.Image.UpdateMode)) builder.WriteString(fmt.Sprintf("talos_image_storage = %q\n", isoStorage)) builder.WriteString(fmt.Sprintf("talos_image_architecture = %q\n", cfg.Talos.Image.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("node_addresses = {\n") for node, addr := range cfg.Talos.Proxmox.NodeAddresses { builder.WriteString(fmt.Sprintf(" %q = %q\n", node, addr)) } 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, ", ") }