feat: init commit with main func
This commit is contained in:
140
traefik.go
Normal file
140
traefik.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Router represents a single Traefik HTTP router as returned by /api/http/routers.
|
||||
type Router struct {
|
||||
Name string `json:"name"`
|
||||
Rule string `json:"rule"`
|
||||
Status string `json:"status"`
|
||||
EntryPoints []string `json:"entryPoints"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
// hostRuleRe extracts hostnames from a Traefik rule such as:
|
||||
//
|
||||
// Host(`app.example.com`) or Host(`a.com`, `b.com`) or HostRegexp(...)
|
||||
//
|
||||
// We capture only bare backtick-quoted tokens which are exact hostnames.
|
||||
var hostRuleRe = regexp.MustCompile("`([^`]+)`")
|
||||
|
||||
// FetchRouters calls the Traefik API and returns the full list of HTTP routers.
|
||||
// Returns an error if Traefik is unreachable or returns a non-200 status.
|
||||
func FetchRouters(cfg *Config) ([]Router, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
cfg.TraefikURL+"/api/http/routers?per_page=1000", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
if cfg.TraefikUsername != "" {
|
||||
req.SetBasicAuth(cfg.TraefikUsername, cfg.TraefikPassword)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("traefik API unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("traefik API returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var routers []Router
|
||||
if err := json.NewDecoder(resp.Body).Decode(&routers); err != nil {
|
||||
return nil, fmt.Errorf("decode routers response: %w", err)
|
||||
}
|
||||
return routers, nil
|
||||
}
|
||||
|
||||
// ParseHostRule extracts all exact hostnames from a Traefik rule string.
|
||||
// Host(`foo.example.com`) → ["foo.example.com"]
|
||||
// Host(`a.com`, `b.com`) → ["a.com", "b.com"]
|
||||
// HostRegexp patterns (containing special regex chars) are skipped.
|
||||
func ParseHostRule(rule string) []string {
|
||||
var hosts []string
|
||||
for _, m := range hostRuleRe.FindAllStringSubmatch(rule, -1) {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
h := strings.ToLower(strings.TrimSpace(m[1]))
|
||||
// Skip if it looks like a regex pattern (contains regexp metacharacters).
|
||||
if strings.ContainsAny(h, `^${}*()+?|\\`) {
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
// FilterDomains builds the desired DNS state from active Traefik routers.
|
||||
// Returns a map of zone → set of relative subdomain labels that should have records.
|
||||
func FilterDomains(routers []Router, cfg *Config) map[string]map[string]struct{} {
|
||||
result := make(map[string]map[string]struct{}, len(cfg.Zones))
|
||||
for _, zone := range cfg.Zones {
|
||||
result[zone] = make(map[string]struct{})
|
||||
}
|
||||
|
||||
for _, r := range routers {
|
||||
// Only consider routers Traefik reports as active.
|
||||
if r.Status != "enabled" {
|
||||
continue
|
||||
}
|
||||
// Exclude Traefik's own internal routers (api@internal, dashboard@internal, etc.).
|
||||
if strings.HasSuffix(r.Name, "@internal") {
|
||||
continue
|
||||
}
|
||||
// Exclude routers explicitly listed in config.
|
||||
if _, excluded := cfg.ExcludeRouters[r.Name]; excluded {
|
||||
continue
|
||||
}
|
||||
// Must have a Host() rule to generate a DNS record.
|
||||
if !strings.Contains(r.Rule, "Host(") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, host := range ParseHostRule(r.Rule) {
|
||||
zone, sub, ok := matchZone(host, cfg.Zones)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
result[zone][sub] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// matchZone finds the longest matching zone for a given FQDN.
|
||||
// Returns (zone, subdomain, true) on match.
|
||||
// Apex domain (fqdn == zone) returns subdomain "".
|
||||
func matchZone(fqdn string, zones []string) (zone, sub string, ok bool) {
|
||||
fqdn = strings.TrimSuffix(fqdn, ".")
|
||||
best := ""
|
||||
for _, z := range zones {
|
||||
z = strings.TrimSuffix(z, ".")
|
||||
if fqdn == z || strings.HasSuffix(fqdn, "."+z) {
|
||||
if len(z) > len(best) {
|
||||
best = z
|
||||
}
|
||||
}
|
||||
}
|
||||
if best == "" {
|
||||
return "", "", false
|
||||
}
|
||||
if fqdn == best {
|
||||
return best, "", true
|
||||
}
|
||||
return best, strings.TrimSuffix(fqdn, "."+best), true
|
||||
}
|
||||
Reference in New Issue
Block a user