package provider import ( "errors" "fmt" "io" "log/slog" "net/http" "net/netip" "os" "strings" "time" ) type Kind string const ( KindDummy Kind = "dummy" KindBunny Kind = "bunny" KindCloudflare Kind = "cloudflare" ) type RecordType string const ( TypeA RecordType = "A" TypeAAAA RecordType = "AAAA" ) type Config struct { Kind Kind `json:"kind"` // exactly one of these supplies the api key. // both expand any $environment variables! Key string `json:"key,omitempty"` KeyFile string `json:"keyFile,omitempty"` } func (c Config) key() (string, error) { if (c.Key == "") == (c.KeyFile == "") { return "", errors.New("exactly one of key or keyFile must be set") } if c.Key != "" { key := os.ExpandEnv(c.Key) if key == "" { return "", fmt.Errorf("key %q expanded to nothing", c.Key) } return key, nil } // expand those environment variables raw, err := os.ReadFile(os.ExpandEnv(c.KeyFile)) if err != nil { return "", err } return strings.TrimSpace(string(raw)), nil } type Provider interface { // Ensure makes name.zone of the given type point at addr, // creating the record when it doesn't exist yet. // Returns the previous value (if any) and whether anything was changed. Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error) } func New(log *slog.Logger, name string, cfg Config) (Provider, error) { log = log.With( slog.Group( "provider", slog.String("name", name), slog.String("kind", string(cfg.Kind)), ), ) switch cfg.Kind { case KindDummy: return newDummy(log) case KindBunny: return newBunny(log, cfg) case KindCloudflare: return newCloudflare(log, cfg) default: return nil, fmt.Errorf("unknown provider kind %q", cfg.Kind) } } var client = http.Client{Timeout: 30 * time.Second} // generic request sending helper, since most providers will need it. func request(method, url string, headers map[string]string, body io.Reader) ([]byte, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode > 299 { return nil, fmt.Errorf("%s %s: %s: %.200s", method, url, resp.Status, raw) } return raw, nil }