package config import ( "errors" "net/netip" "os" "github.com/Pingu-Studio/MaidnCLI/internal/talos" "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 } applyDefaults(&cfg) return cfg, Validate(cfg) } func Save(path string, cfg Config) error { applyDefaults(&cfg) 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 applyDefaults(cfg *Config) { if cfg.Git.Provider == "" { cfg.Git.Provider = "forgejo" } if cfg.Git.BaseURL == "" { cfg.Git.BaseURL = "https://git.pingu.pw" } if cfg.Git.CloneParent == "" { cfg.Git.CloneParent = cfg.WorkspaceDir } if cfg.Flux.Branch == "" { cfg.Flux.Branch = "main" } if cfg.Flux.ClusterPath == "" { cfg.Flux.ClusterPath = "./clusters/maidn-cd-0" } if cfg.Flux.ManifestsRepo == "" { cfg.Flux.ManifestsRepo = "cicd-deployment-manifests" } if cfg.Templates.TalosRepoURL == "" { cfg.Templates.TalosRepoURL = "https://git.pingu.pw/Maidn/maidn-talos-proxmox.git" } if cfg.Templates.TalosRepoRef == "" { cfg.Templates.TalosRepoRef = "main" } if cfg.Templates.CICDRepoURL == "" { cfg.Templates.CICDRepoURL = "https://git.pingu.pw/Maidn/maidn-cicd-cluster-template.git" } if cfg.Templates.CICDRepoRef == "" { cfg.Templates.CICDRepoRef = "main" } if cfg.Cilium.TrafficInterface == "" { cfg.Cilium.TrafficInterface = "eth1" } if cfg.Talos.RepoDirName == "" { cfg.Talos.RepoDirName = "maidn-talos-proxmox" } if cfg.Talos.TerraformDir == "" { cfg.Talos.TerraformDir = "terraform" } if cfg.Talos.GeneratedDir == "" { cfg.Talos.GeneratedDir = "generated" } if cfg.Talos.ConfigFileName == "" { cfg.Talos.ConfigFileName = "terraform.tfvars" } if cfg.Talos.Image.UpdateMode == "" { cfg.Talos.Image.UpdateMode = "download" } if cfg.Talos.Image.Storage == "" { cfg.Talos.Image.Storage = "local" } if cfg.Talos.Image.Architecture == "" { cfg.Talos.Image.Architecture = "amd64" } if cfg.Talos.BootstrapEndpoint == "" { cfg.Talos.BootstrapEndpoint = cfg.Talos.BootstrapNode } if cfg.Talos.KubeconfigEndpoint == "" { cfg.Talos.KubeconfigEndpoint = cfg.Talos.KubeconfigNode } } func Validate(cfg Config) error { if cfg.WorkspaceDir == "" { return errors.New("workspaceDir is required") } if cfg.Git.BaseURL == "" || cfg.Git.Username == "" || cfg.Git.Owner == "" { return errors.New("git baseUrl, username, and owner are required") } if cfg.Flux.RepoName == "" || cfg.Flux.ClusterPath == "" || cfg.Flux.ManifestsRepo == "" { return errors.New("flux repoName, manifestsRepo, and clusterPath are required") } if cfg.Templates.TalosRepoURL == "" || cfg.Talos.RepoDirName == "" { return errors.New("talos template repo and checkout dir 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.APITokenID == "" || cfg.Talos.Proxmox.APITokenSecret == "" { return errors.New("talos proxmox apiUrl, apiTokenId, and apiTokenSecret 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.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") } if len(cfg.Talos.Nodes) == 0 { return errors.New("at least one talos node is required") } if cfg.Cilium.LoadBalancerStart == "" || cfg.Cilium.LoadBalancerEnd == "" { return errors.New("cilium loadBalancerStart and loadBalancerEnd are required") } start, err := netip.ParseAddr(cfg.Cilium.LoadBalancerStart) if err != nil { return errors.New("cilium loadBalancerStart must be an IP address") } end, err := netip.ParseAddr(cfg.Cilium.LoadBalancerEnd) if err != nil || start.BitLen() != end.BitLen() || start.Compare(end) > 0 { return errors.New("cilium loadBalancerEnd must be an IP address after loadBalancerStart") } for _, node := range cfg.Talos.Nodes { if len(node.Networks) < 2 { return errors.New("cilium requires a second static traffic network on every talos node") } traffic := node.Networks[1] prefix, err := netip.ParsePrefix(traffic.CIDR) if err != nil || traffic.IP == "" || traffic.Gateway != "" { return errors.New("cilium traffic networks require a static IP, valid CIDR, and no gateway") } nodeIP, err := netip.ParseAddr(traffic.IP) if err != nil || !prefix.Contains(nodeIP) || nodeIP == start || nodeIP == end { return errors.New("cilium traffic node IP must belong to its CIDR and not use the LoadBalancer range") } if !prefix.Contains(start) || !prefix.Contains(end) { return errors.New("cilium LoadBalancer range must belong to every node traffic network") } } return nil }