package config import ( "bytes" "errors" "fmt" "io" "net/netip" "net/url" "os" "path/filepath" "regexp" "strings" "github.com/Pingu-Studio/MaidnCLI/internal/talos" "gopkg.in/yaml.v3" ) func Load(path string) (Config, error) { cfg, err := LoadRaw(path) if err != nil { return cfg, err } return Resolve(cfg) } func LoadRaw(path string) (Config, error) { var cfg Config data, err := os.ReadFile(path) if err != nil { return cfg, err } decoder := yaml.NewDecoder(bytes.NewReader(data)) decoder.KnownFields(true) if err := decoder.Decode(&cfg); err != nil { return cfg, err } if err := decoder.Decode(&Config{}); !errors.Is(err, io.EOF) { return cfg, errors.New("bootstrap config must contain one YAML document") } return cfg, nil } func Save(path string, cfg Config) error { resolved, err := Resolve(cfg) if err != nil { return err } data, err := yaml.Marshal(resolved) if err != nil { return err } return os.WriteFile(path, data, 0600) } func WriteRedacted(path string, cfg Config) error { redacted := cfg redacted.Git.Token = "" redacted.Git.BaseURL = RedactURL(redacted.Git.BaseURL) redacted.Talos.Proxmox.APITokenSecret = "" redacted.Talos.Proxmox.APIURL = RedactURL(redacted.Talos.Proxmox.APIURL) redacted.DemocraticCSI.TrueNASAPIKey = "" redacted.Templates.TalosRepoURL = RedactURL(redacted.Templates.TalosRepoURL) redacted.Templates.CICDRepoURL = RedactURL(redacted.Templates.CICDRepoURL) redacted.Templates.ManifestsRepoURL = RedactURL(redacted.Templates.ManifestsRepoURL) redacted.Templates.TektonCatalogRepoURL = RedactURL(redacted.Templates.TektonCatalogRepoURL) redacted.Delivery.AppRepoURL = RedactURL(redacted.Delivery.AppRepoURL) redacted.Delivery.ImageRepository = RedactURL(redacted.Delivery.ImageRepository) data, err := yaml.Marshal(redacted) if err != nil { return err } return os.WriteFile(path, data, 0600) } // RedactURL removes credential-bearing URL components before persistent output. func RedactURL(value string) string { if strings.Contains(value, "://") { parsed, err := url.Parse(value) if err != nil { return "" } parsed.User = nil parsed.RawQuery = "" parsed.ForceQuery = false parsed.Fragment = "" return parsed.String() } if at := strings.Index(value, "@"); at > 0 && !strings.Contains(value[:at], "/") && strings.Contains(value[at+1:], ":") { return value[at+1:] } if strings.ContainsAny(value, "@?#") { return "" } return value } func Resolve(cfg Config) (Config, error) { applyDefaults(&cfg) return cfg, Validate(cfg) } // ResolveDelivery applies delivery defaults for an explicitly requested app operation. func ResolveDelivery(cfg Config) (Config, error) { applyDeliveryDefaults(&cfg) return cfg, ValidateDelivery(cfg) } // ResolveFreshBootstrap validates the scoped contract needed before a new // Forgejo organization can be scaffolded. It deliberately does not validate // recovery, SOPS, Proxmox, or Talos state. func ResolveFreshBootstrap(cfg Config, organization string, enableDelivery bool) (Config, error) { if !validRepositoryPart(organization) { return cfg, errors.New("organization must be a Forgejo owner name") } if cfg.Git.Owner != "" && cfg.Git.Owner != organization { return cfg, errors.New("git owner must match --organization") } cfg.Git.Owner = organization if cfg.WorkspaceDir == "" || !filepath.IsAbs(cfg.WorkspaceDir) { return cfg, errors.New("workspaceDir must be an absolute isolated workspace path") } if cfg.Git.CloneParent == "" || !filepath.IsAbs(cfg.Git.CloneParent) || !isChildPath(cfg.WorkspaceDir, cfg.Git.CloneParent) { return cfg, errors.New("git cloneParent must be an absolute child of workspaceDir") } if cfg.Git.Provider != "forgejo" || cfg.Git.BaseURL == "" || cfg.Git.Username == "" || cfg.Git.Token == "" { return cfg, errors.New("git provider, baseUrl, username, and token are required for fresh bootstrap") } if err := validateForgejoOrigin(cfg.Git.BaseURL); err != nil { return cfg, err } if !validRepositoryPart(cfg.Flux.RepoName) || !validRepositoryPart(cfg.Flux.ManifestsRepo) || !validRepositoryPart(cfg.Flux.TektonCatalogRepo) || cfg.Flux.Branch == "" || cfg.Flux.ClusterPath == "" || cfg.Flux.ClusterDomain == "" { return cfg, errors.New("flux repoName, manifestsRepo, tektonCatalogRepo, branch, clusterPath, and clusterDomain are required") } if cfg.Talos.RepoDirName == "" || filepath.Base(cfg.Talos.RepoDirName) != cfg.Talos.RepoDirName || cfg.Talos.GeneratedDir == "" { return cfg, errors.New("talos repoDirName and generatedDir are required for template locking") } for _, source := range []struct{ URL, Ref string }{ {cfg.Templates.TalosRepoURL, cfg.Templates.TalosRepoRef}, {cfg.Templates.CICDRepoURL, cfg.Templates.CICDRepoRef}, {cfg.Templates.ManifestsRepoURL, cfg.Templates.ManifestsRepoRef}, {cfg.Templates.TektonCatalogRepoURL, cfg.Templates.TektonCatalogRepoRef}, } { if source.URL == "" || source.Ref == "" || RedactURL(source.URL) != source.URL { return cfg, errors.New("template repository URLs and refs must be explicit and credential-free") } if err := validateRepositoryURL(source.URL); err != nil { return cfg, err } } return cfg, nil } // ResolveAppOnboarding validates only the source-owned delivery contract. func ResolveAppOnboarding(cfg Config) (Config, error) { if cfg.Git.Owner == "" || cfg.Git.Username == "" || cfg.Git.Token == "" || cfg.Flux.RepoName == "" || cfg.Flux.ManifestsRepo == "" || cfg.Flux.Branch == "" || cfg.Flux.ClusterDomain == "" { return cfg, errors.New("git owner, username, token, flux repoName, manifestsRepo, branch, and clusterDomain are required for app onboarding") } if err := validateForgejoOrigin(cfg.Git.BaseURL); err != nil { return cfg, err } resolved, err := ResolveDelivery(cfg) if err != nil { return cfg, err } owner, _, err := deliveryRepositoryOwner(resolved.Delivery.AppRepoURL) if err != nil { return cfg, err } if owner != resolved.Git.Owner { return cfg, errors.New("delivery appRepoUrl owner must match git owner for app onboarding") } if resolved.Delivery.ProductionBranch != "production" { return cfg, errors.New("delivery productionBranch must be literal production for app onboarding") } return resolved, nil } func deliveryRepositoryOwner(value string) (string, string, error) { parsed, err := url.Parse(value) if err != nil { return "", "", errors.New("delivery appRepoUrl must identify one Forgejo owner/repository.git") } parts := strings.Split(strings.Trim(parsed.Path, "/"), "/") if len(parts) != 2 || !strings.HasSuffix(parts[1], ".git") || !validRepositoryPart(parts[0]) || !validRepositoryPart(strings.TrimSuffix(parts[1], ".git")) { return "", "", errors.New("delivery appRepoUrl must identify one Forgejo owner/repository.git") } return parts[0], strings.TrimSuffix(parts[1], ".git"), nil } func validRepositoryPart(value string) bool { return value != "" && !strings.Contains(value, "..") && regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`).MatchString(value) } func isChildPath(parent, child string) bool { relative, err := filepath.Rel(filepath.Clean(parent), filepath.Clean(child)) return err == nil && relative != "." && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative) } func validateForgejoOrigin(value string) error { parsed, err := url.Parse(value) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawPath != "" || parsed.RawQuery != "" || parsed.Fragment != "" || strings.Trim(parsed.Path, "/") != "" { return errors.New("git baseUrl must be a credential-free HTTPS origin") } return nil } func validateRepositoryURL(value string) error { parsed, err := url.Parse(value) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || strings.Trim(parsed.Path, "/") == "" { return errors.New("template repository URL must be a credential-free HTTPS repository URL") } return nil } func applyDefaults(cfg *Config) { if cfg.ClusterID == "" { cfg.ClusterID = cfg.Talos.Cluster.Name } 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.Flux.TektonCatalogRepo == "" { cfg.Flux.TektonCatalogRepo = "tekton-pipelines" } 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.Templates.ManifestsRepoURL == "" { cfg.Templates.ManifestsRepoURL = "https://git.pingu.pw/Maidn/cicd-deployment-manifests-template.git" } if cfg.Templates.ManifestsRepoRef == "" { cfg.Templates.ManifestsRepoRef = "main" } if cfg.Templates.TektonCatalogRepoURL == "" { cfg.Templates.TektonCatalogRepoURL = "https://git.pingu.pw/Maidn/tekton-pipelines.git" } if cfg.Templates.TektonCatalogRepoRef == "" { cfg.Templates.TektonCatalogRepoRef = "main" } if cfg.Cilium.TrafficInterface == "" { cfg.Cilium.TrafficInterface = "eth1" } if cfg.DemocraticCSI.PortalGroup == "" { cfg.DemocraticCSI.PortalGroup = "1" } if cfg.DemocraticCSI.InitiatorGroup == "" { cfg.DemocraticCSI.InitiatorGroup = "1" } 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.Image.KubernetesVersion == "" && strings.HasPrefix(cfg.Talos.Image.TalosVersion, "v1.13.") { cfg.Talos.Image.KubernetesVersion = "v1.33.4" } if cfg.SOPS.AgeKeyPath == "" && cfg.WorkspaceDir != "" { cfg.SOPS.AgeKeyPath = filepath.Join(cfg.WorkspaceDir, ".age", "key.txt") } if cfg.SOPS.OperationalSecretsPath == "" && cfg.WorkspaceDir != "" { cfg.SOPS.OperationalSecretsPath = filepath.Join(cfg.WorkspaceDir, "operational-secrets.sops.yaml") } if cfg.SOPS.RecoveryIdentityPath == "" && cfg.WorkspaceDir != "" { cfg.SOPS.RecoveryIdentityPath = filepath.Join(cfg.WorkspaceDir, ".age", "recovery-key.txt") } if cfg.SOPS.RecoveryBundlePath == "" && cfg.WorkspaceDir != "" { cfg.SOPS.RecoveryBundlePath = filepath.Join(cfg.WorkspaceDir, ".recovery", "openbao-recovery.age") } if cfg.Talos.BootstrapEndpoint == "" { cfg.Talos.BootstrapEndpoint = cfg.Talos.BootstrapNode } if cfg.Talos.KubeconfigEndpoint == "" { cfg.Talos.KubeconfigEndpoint = cfg.Talos.KubeconfigNode } } func applyDeliveryDefaults(cfg *Config) { if cfg.Delivery.AppRepoRef == "" { cfg.Delivery.AppRepoRef = cfg.Flux.Branch } if cfg.Delivery.BuildOutputDirectory == "" { cfg.Delivery.BuildOutputDirectory = "dist" } if cfg.Delivery.BuildConfiguration == "" { cfg.Delivery.BuildConfiguration = "production" } if cfg.Delivery.WebhookHostname == "" && cfg.Flux.ClusterDomain != "" { cfg.Delivery.WebhookHostname = "tekton." + cfg.Flux.ClusterDomain } if cfg.Delivery.WebhookPath == "" { cfg.Delivery.WebhookPath = "/" } } func Validate(cfg Config) error { if !regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`).MatchString(cfg.ClusterID) { return errors.New("clusterId must be a lowercase DNS label") } if cfg.WorkspaceDir == "" { return errors.New("workspaceDir is required") } if cfg.Git.Provider != "forgejo" { return errors.New("git provider must be forgejo") } if cfg.Git.BaseURL == "" || cfg.Git.Username == "" || cfg.Git.Owner == "" { return errors.New("git baseUrl, username, and owner are required") } if cfg.Git.Token == "" { return errors.New("git token is required; SSH bootstrap is not implemented") } if RedactURL(cfg.Git.BaseURL) != cfg.Git.BaseURL { return errors.New("git baseUrl must not contain credentials, a query, or a fragment") } if cfg.Flux.RepoName == "" || cfg.Flux.ClusterPath == "" || cfg.Flux.ManifestsRepo == "" || cfg.Flux.TektonCatalogRepo == "" || cfg.Flux.ClusterDomain == "" { return errors.New("flux repoName, clusterDomain, manifestsRepo, tektonCatalogRepo, and clusterPath are required") } if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`).MatchString(cfg.Flux.TektonCatalogRepo) || strings.Contains(cfg.Flux.TektonCatalogRepo, "..") { return errors.New("flux tektonCatalogRepo must be a repository name") } if len(cfg.Flux.ClusterDomain) > 253 || !regexp.MustCompile(`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(\.[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)*$`).MatchString(cfg.Flux.ClusterDomain) { return errors.New("flux clusterDomain must be a lowercase DNS subdomain") } if cfg.Delivery.Configured() { if err := ValidateDelivery(cfg); err != nil { return err } } if err := ValidateSecretGrants(cfg.SecretGrants); err != nil { return err } if cfg.Templates.TalosRepoURL == "" || cfg.Templates.TalosRepoRef == "" || cfg.Templates.CICDRepoURL == "" || cfg.Templates.CICDRepoRef == "" || cfg.Templates.ManifestsRepoURL == "" || cfg.Templates.ManifestsRepoRef == "" || cfg.Templates.TektonCatalogRepoURL == "" || cfg.Templates.TektonCatalogRepoRef == "" { return errors.New("all template repository URLs and refs are required") } if cfg.Talos.RepoDirName == "" { return errors.New("talos checkout dir is required") } if cfg.Talos.TerraformDir == "" || cfg.Talos.GeneratedDir == "" || cfg.Talos.ConfigFileName == "" { return errors.New("talos terraformDir, generatedDir, and configFileName are required") } generatedDir := filepath.Clean(cfg.Talos.GeneratedDir) if filepath.IsAbs(generatedDir) || generatedDir == "." || generatedDir == ".." || strings.HasPrefix(generatedDir, ".."+string(filepath.Separator)) { return errors.New("talos generatedDir must be a child directory") } 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.KubernetesVersion == "" || cfg.Talos.Image.SchematicID == "" { return errors.New("talos version, kubernetesVersion, and schematicId are required") } if len(cfg.Talos.Image.SchematicID) < 12 { return errors.New("talos schematicId must contain at least 12 characters") } 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") } if cfg.DemocraticCSI.TrueNASAPIKey == "" || cfg.DemocraticCSI.TrueNASHost == "" || cfg.DemocraticCSI.TargetPortal == "" || cfg.DemocraticCSI.ShareHost == "" || cfg.DemocraticCSI.DatasetParentNFS == "" || cfg.DemocraticCSI.DatasetSnapshotsNFS == "" || cfg.DemocraticCSI.AllowedNetworks == "" || cfg.DemocraticCSI.NameSuffix == "" || cfg.DemocraticCSI.PortalGroup == "" || cfg.DemocraticCSI.InitiatorGroup == "" { return errors.New("all democraticCsi settings are required") } if _, err := netip.ParsePrefix(cfg.DemocraticCSI.AllowedNetworks); err != nil { return errors.New("democraticCsi allowedNetworks must be a CIDR") } seenNames := map[string]bool{} seenVMIDs := map[int]bool{} seenIPs := map[netip.Addr]bool{} seenMACs := map[string]bool{} controlPlanes := 0 trafficVLAN := 0 for index, node := range cfg.Talos.Nodes { if node.Name == "" || seenNames[node.Name] || node.VMID <= 0 || seenVMIDs[node.VMID] { return errors.New("talos node names and VMIDs must be unique and non-zero") } seenNames[node.Name] = true seenVMIDs[node.VMID] = true if node.Role != "controlplane" && node.Role != "worker" { return fmt.Errorf("talos node %q role must be controlplane or worker", node.Name) } if node.Role == "controlplane" { controlPlanes++ } if index == 0 && node.Role != "controlplane" { return errors.New("the first talos node must be a controlplane") } if len(node.Networks) < 2 { return errors.New("cilium requires a second static traffic network on every talos node") } primary := node.Networks[0] primaryIP, err := netip.ParseAddr(primary.IP) if err != nil || primary.Gateway == "" { return errors.New("talos primary networks require a static IP and gateway") } if seenIPs[primaryIP] { return errors.New("talos node IPs must be unique") } seenIPs[primaryIP] = true 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.Compare(start) >= 0 && nodeIP.Compare(end) <= 0) { return errors.New("cilium traffic node IP must belong to its CIDR and not use the LoadBalancer range") } if seenIPs[nodeIP] { return errors.New("talos node IPs must be unique") } seenIPs[nodeIP] = true if trafficVLAN == 0 { trafficVLAN = traffic.VLANID } else if trafficVLAN != traffic.VLANID { return errors.New("cilium traffic networks must use one VLAN") } if !prefix.Contains(start) || !prefix.Contains(end) { return errors.New("cilium LoadBalancer range must belong to every node traffic network") } for _, network := range node.Networks { if network.VLANID < 1 || network.VLANID > 4094 { return errors.New("talos VLAN IDs must be between 1 and 4094") } if network.MACAddress != "" { mac := strings.ToLower(network.MACAddress) if seenMACs[mac] { return errors.New("talos network MAC addresses must be unique") } seenMACs[mac] = true } } } if controlPlanes == 0 { return errors.New("at least one controlplane node is required") } return nil } // ValidateSecretGrants prevents a configuration change from widening an // application's OpenBao policy outside its own path or named shared grants. func ValidateSecretGrants(grants []SecretGrant) error { name := regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) seen := map[string]bool{} for _, grant := range grants { if !name.MatchString(grant.Application) { return errors.New("secret grant application must be a lowercase DNS label") } if grant.Consumer != "build" && grant.Consumer != "publish" && grant.Consumer != "runtime" { return errors.New("secret grant consumer must be build, publish, or runtime") } if grant.Consumer == "runtime" { if grant.Environment != "staging" && grant.Environment != "production" { return errors.New("runtime secret grant environment must be staging or production") } } else if grant.Environment != "" { return errors.New("build and publish secret grants must not set environment") } key := grant.Application + "/" + grant.Consumer + "/" + grant.Environment if seen[key] { return errors.New("duplicate secret grant consumer") } seen[key] = true if len(grant.Secrets) == 0 { return errors.New("secret grant requires at least one application secret") } secretNames := map[string]bool{} for _, secret := range grant.Secrets { if !name.MatchString(secret) || secretNames[secret] { return errors.New("secret grant application secret names must be unique lowercase DNS labels") } secretNames[secret] = true } for _, shared := range grant.Shared { if !name.MatchString(shared) { return errors.New("shared secret grant name must be a lowercase DNS label") } } } return nil } // ValidateDelivery requires the complete app-delivery contract before rendering or publishing it. func ValidateDelivery(cfg Config) error { if cfg.Delivery.AppName == "" || cfg.Delivery.AppRepoURL == "" || cfg.Delivery.AppRepoRef == "" || cfg.Delivery.ProductionBranch == "" || cfg.Delivery.ImageRepository == "" || cfg.Delivery.BuildOutputDirectory == "" || cfg.Delivery.BuildConfiguration == "" || cfg.Delivery.WebhookHostname == "" || cfg.Delivery.WebhookPath == "" { return errors.New("delivery appName, appRepoUrl, appRepoRef, productionBranch, imageRepository, buildOutputDirectory, buildConfiguration, webhookHostname, and webhookPath are required") } if !regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`).MatchString(cfg.Delivery.AppName) { return errors.New("delivery appName must be a lowercase DNS label") } if cfg.Delivery.ProductionBranch == cfg.Delivery.AppRepoRef || !validDeliveryBranch(cfg.Delivery.ProductionBranch) { return errors.New("delivery productionBranch must be a valid branch distinct from appRepoRef") } if RedactURL(cfg.Delivery.AppRepoURL) != cfg.Delivery.AppRepoURL { return errors.New("delivery appRepoUrl must not contain credentials, a query, or a fragment") } if err := validateDeliveryRepositoryOrigin(cfg.Git.BaseURL, cfg.Delivery.AppRepoURL); err != nil { return err } if strings.ContainsAny(cfg.Delivery.WebhookHostname, "/:@?#") || !strings.HasPrefix(cfg.Delivery.WebhookPath, "/") || strings.ContainsAny(cfg.Delivery.WebhookPath, "?#") { return errors.New("delivery webhookHostname must be a hostname and webhookPath must be an absolute path") } return nil } func validateDeliveryRepositoryOrigin(baseURL, repositoryURL string) error { base, err := url.Parse(baseURL) if err != nil || base.Scheme != "https" || base.Host == "" || base.User != nil || base.RawPath != "" || base.RawQuery != "" || base.Fragment != "" || strings.Trim(base.Path, "/") != "" { return errors.New("git baseUrl must be a credential-free HTTPS origin") } repository, err := url.Parse(repositoryURL) if err != nil || repository.Scheme != "https" || repository.Host == "" || repository.User != nil || repository.RawPath != "" || repository.RawQuery != "" || repository.Fragment != "" || !strings.EqualFold(repository.Scheme+"://"+repository.Host, base.Scheme+"://"+base.Host) { return errors.New("delivery appRepoUrl must be a credential-free HTTPS repository on the configured Forgejo origin") } return nil } func validDeliveryBranch(value string) bool { if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*$`).MatchString(value) || strings.Contains(value, "..") || strings.Contains(value, "//") || strings.HasSuffix(value, ".") || strings.HasSuffix(value, ".lock") { return false } for _, part := range strings.Split(value, "/") { if strings.HasPrefix(part, ".") { return false } } return true } func Preflight(cfg Config) error { return talos.RequireCLICompatibility(cfg.Talos.Image.TalosVersion) }