343 lines
14 KiB
Go
343 lines
14 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"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)
|
|
cicdTemplateDir := filepath.Join(workspace, "maidn-cicd-cluster-template")
|
|
if err := ensureRepo(cicdTemplateDir, r.Config.Templates.CICDRepoURL, r.Config.Templates.CICDRepoRef); err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
clusterDir := filepath.Join(dir, strings.TrimPrefix(r.Config.Flux.ClusterPath, "./"))
|
|
if err := copyDir(filepath.Join(cicdTemplateDir, "base"), filepath.Join(dir, "base")); err != nil {
|
|
return err
|
|
}
|
|
if err := copyDir(filepath.Join(cicdTemplateDir, "clusters", "template"), clusterDir); err != nil {
|
|
return err
|
|
}
|
|
if err := renderCiliumConfig(filepath.Join(dir, "base", "cilium"), r.Config); err != nil {
|
|
return err
|
|
}
|
|
if err := renderCiliumConfig(filepath.Join(dir, "base", "cilium-config"), r.Config); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureCiliumKustomizations(clusterDir); err != nil {
|
|
return err
|
|
}
|
|
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 := 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 := waitForTalosReboot(generatedDir, r.Config); err != nil {
|
|
return err
|
|
}
|
|
if _, err := utils.RunCommandQuietOutputInDir(generatedDir, "talosctl", "health", "--talosconfig=./clusterconfig/talosconfig", "--endpoints="+r.Config.Talos.BootstrapEndpoint, "--nodes="+r.Config.Talos.BootstrapNode, "--wait-timeout=20s"); err != nil {
|
|
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", "health", "--talosconfig=./clusterconfig/talosconfig", "--endpoints="+r.Config.Talos.BootstrapEndpoint, "--nodes="+r.Config.Talos.BootstrapNode, "--wait-timeout=2m"); 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 := installCilium(generatedDir, r.Config); err != nil {
|
|
return err
|
|
}
|
|
if err := utils.RunCommandInDir(generatedDir, "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, "--cluster-domain="+r.Config.Flux.ClusterDomain, "--username="+r.Config.Git.Username, "--password="+r.Config.Git.Token, "--token-auth", "--kubeconfig=kubeconfig"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func renderCiliumConfig(dir string, cfg config.Config) error {
|
|
replacements := strings.NewReplacer(
|
|
"${CILIUM_K8S_SERVICE_HOST}", cfg.Talos.KubeconfigEndpoint,
|
|
"${CILIUM_TRAFFIC_INTERFACE}", cfg.Cilium.TrafficInterface,
|
|
"${CILIUM_LB_START}", cfg.Cilium.LoadBalancerStart,
|
|
"${CILIUM_LB_END}", cfg.Cilium.LoadBalancerEnd,
|
|
)
|
|
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return err
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(replacements.Replace(string(content))), info.Mode())
|
|
})
|
|
}
|
|
|
|
func ensureCiliumKustomizations(clusterDir string) error {
|
|
path := filepath.Join(clusterDir, "kustomization.yaml")
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updated := string(content)
|
|
for _, resource := range []string{"cilium-kustomization.yaml", "cilium-config-kustomization.yaml"} {
|
|
if !strings.Contains(updated, resource) {
|
|
updated += " - " + resource + "\n"
|
|
}
|
|
}
|
|
if updated == string(content) {
|
|
return nil
|
|
}
|
|
return os.WriteFile(path, []byte(updated), 0644)
|
|
}
|
|
|
|
func installCilium(dir string, cfg config.Config) error {
|
|
helmDir := filepath.Join(dir, ".helm")
|
|
if err := os.MkdirAll(helmDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return utils.RunCommandInDir(dir, "helm", "upgrade", "--install", "cilium", "cilium", "--repo=https://helm.cilium.io", "--version=1.19.6", "--repository-config="+filepath.Join(helmDir, "repositories.yaml"), "--repository-cache="+helmDir, "--namespace=kube-system", "--create-namespace", "--kubeconfig=kubeconfig", "--wait", "--timeout=5m", "--set=kubeProxyReplacement=true", "--set=ipam.mode=kubernetes", "--set=k8sServiceHost="+cfg.Talos.KubeconfigEndpoint, "--set=k8sServicePort=6443", "--set=gatewayAPI.enabled=true", "--set=l2announcements.enabled=true")
|
|
}
|
|
|
|
func copyDir(source, destination string) error {
|
|
return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relative, err := filepath.Rel(source, path)
|
|
if err != nil || relative == "." {
|
|
return err
|
|
}
|
|
target := filepath.Join(destination, relative)
|
|
if info.IsDir() {
|
|
return os.MkdirAll(target, 0755)
|
|
}
|
|
if _, err := os.Stat(target); err == nil {
|
|
return nil
|
|
}
|
|
input, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer input.Close()
|
|
output, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, info.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer output.Close()
|
|
_, err = io.Copy(output, input)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func waitForTalosReboot(dir string, cfg config.Config) error {
|
|
deadline := time.Now().Add(5 * time.Minute)
|
|
offline := false
|
|
for time.Now().Before(deadline) {
|
|
output, err := utils.RunCommandQuietOutputInDir(dir, "talosctl", "get", "machinestatus", "--output=json", "--talosconfig=./clusterconfig/talosconfig", "--endpoints="+cfg.Talos.BootstrapEndpoint, "--nodes="+cfg.Talos.BootstrapNode)
|
|
if err != nil {
|
|
offline = true
|
|
} else if offline && strings.Contains(string(output), `"stage": "running"`) {
|
|
return nil
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
return fmt.Errorf("Talos API did not return after applying its machine configuration")
|
|
}
|
|
|
|
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 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 = \"none\"\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, ", ")
|
|
}
|