mirror of
https://github.com/tiennm99/traefik-cloudflare-dns.git
synced 2026-10-11 03:13:52 +00:00
Create a record for each Host(...) in running containers' Traefik router labels, tagged with a configurable comment and optional tags, and delete owned records once their host has been down for DELETE_AFTER.
164 lines
4.0 KiB
Go
164 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const defaultComment = "managed by traefik-cloudflare-dns"
|
|
|
|
// Config holds every setting, read from environment variables.
|
|
type Config struct {
|
|
APIToken string
|
|
ZoneID string
|
|
Domain string
|
|
Target string
|
|
RecordType string
|
|
Proxied bool
|
|
TTL int
|
|
Comment string
|
|
Tags []string
|
|
DeleteAfter time.Duration
|
|
ResyncInterval time.Duration
|
|
DryRun bool
|
|
DockerHost string
|
|
LogLevel string
|
|
}
|
|
|
|
// LoadConfig reads the configuration through getenv and validates it.
|
|
func LoadConfig(getenv func(string) string) (Config, error) {
|
|
c := Config{
|
|
APIToken: strings.TrimSpace(getenv("CF_API_TOKEN")),
|
|
ZoneID: strings.TrimSpace(getenv("CF_ZONE_ID")),
|
|
Domain: strings.ToLower(strings.Trim(strings.TrimSpace(getenv("DOMAIN")), ".")),
|
|
Target: strings.TrimSpace(getenv("TARGET")),
|
|
Comment: getenv("RECORD_COMMENT"),
|
|
DockerHost: strings.TrimSpace(getenv("DOCKER_HOST")),
|
|
LogLevel: strings.ToLower(strings.TrimSpace(getenv("LOG_LEVEL"))),
|
|
}
|
|
|
|
var missing []string
|
|
for name, value := range map[string]string{
|
|
"CF_API_TOKEN": c.APIToken,
|
|
"CF_ZONE_ID": c.ZoneID,
|
|
"DOMAIN": c.Domain,
|
|
"TARGET": c.Target,
|
|
} {
|
|
if value == "" {
|
|
missing = append(missing, name)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
slices.Sort(missing)
|
|
return Config{}, fmt.Errorf("missing required variables: %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
c.RecordType = recordTypeFor(c.Target)
|
|
if c.RecordType == "CNAME" {
|
|
c.Target = strings.ToLower(strings.TrimSuffix(c.Target, "."))
|
|
}
|
|
|
|
if c.Comment == "" {
|
|
c.Comment = defaultComment
|
|
}
|
|
if strings.ContainsAny(c.Comment, "\r\n") {
|
|
return Config{}, fmt.Errorf("RECORD_COMMENT must not contain line breaks")
|
|
}
|
|
|
|
for _, tag := range strings.Split(getenv("RECORD_TAGS"), ",") {
|
|
tag = strings.TrimSpace(tag)
|
|
if tag == "" {
|
|
continue
|
|
}
|
|
if !strings.Contains(tag, ":") {
|
|
return Config{}, fmt.Errorf("RECORD_TAGS entry %q is not in name:value form", tag)
|
|
}
|
|
c.Tags = append(c.Tags, tag)
|
|
}
|
|
|
|
var err error
|
|
if c.Proxied, err = parseBool(getenv, "PROXIED", false); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.DryRun, err = parseBool(getenv, "DRY_RUN", false); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.TTL, err = parseInt(getenv, "TTL", 1); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.DeleteAfter, err = parseDuration(getenv, "DELETE_AFTER", time.Hour); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.ResyncInterval, err = parseDuration(getenv, "RESYNC_INTERVAL", time.Minute); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.ResyncInterval <= 0 {
|
|
return Config{}, fmt.Errorf("RESYNC_INTERVAL must be positive")
|
|
}
|
|
|
|
if c.DockerHost == "" {
|
|
c.DockerHost = "unix:///var/run/docker.sock"
|
|
}
|
|
if c.LogLevel == "" {
|
|
c.LogLevel = "info"
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// recordTypeFor returns A or AAAA for an IP target and CNAME for a hostname.
|
|
func recordTypeFor(target string) string {
|
|
ip := net.ParseIP(target)
|
|
switch {
|
|
case ip == nil:
|
|
return "CNAME"
|
|
case ip.To4() != nil:
|
|
return "A"
|
|
default:
|
|
return "AAAA"
|
|
}
|
|
}
|
|
|
|
func parseBool(getenv func(string) string, name string, def bool) (bool, error) {
|
|
v := strings.TrimSpace(getenv(name))
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s: %q is not a boolean", name, v)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func parseInt(getenv func(string) string, name string, def int) (int, error) {
|
|
v := strings.TrimSpace(getenv(name))
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s: %q is not an integer", name, v)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func parseDuration(getenv func(string) string, name string, def time.Duration) (time.Duration, error) {
|
|
v := strings.TrimSpace(getenv(name))
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil || d < 0 {
|
|
return 0, fmt.Errorf("%s: %q is not a non-negative duration such as 1h or 30m", name, v)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// osGetenv is the production getenv.
|
|
var osGetenv = os.Getenv
|