package provider import ( "bytes" "encoding/json" "fmt" "log/slog" "net/netip" "net/url" ) type cloudflare struct { log *slog.Logger key string } var _ Provider = (*cloudflare)(nil) func newCloudflare(log *slog.Logger, cfg Config) (*cloudflare, error) { key, err := cfg.key() if err != nil { return nil, fmt.Errorf("failed instantiating cloudflare provider: %w", err) } return &cloudflare{key: key, log: log}, nil } const cfAPI = "https://api.cloudflare.com/client/v4" func (c *cloudflare) headers() map[string]string { return map[string]string{"Authorization": "Bearer " + c.key} } func (c *cloudflare) Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error) { c.log.Info("updating record via cloudflare api") var zones struct { Result []struct { ID string `json:"id"` } `json:"result"` } raw, err := request("GET", cfAPI+"/zones?name="+url.QueryEscape(zone), c.headers(), nil) if err != nil { return "", false, err } if err := json.Unmarshal(raw, &zones); err != nil { return "", false, err } if len(zones.Result) == 0 { return "", false, fmt.Errorf("zone %s not found in cloudflare", zone) } zoneID := zones.Result[0].ID fqdn := name + "." + zone var records struct { Result []struct { ID string `json:"id"` Content string `json:"content"` } `json:"result"` } raw, err = request("GET", fmt.Sprintf("%s/zones/%s/dns_records?type=%s&name=%s", cfAPI, zoneID, string(rtype), url.QueryEscape(fqdn)), c.headers(), nil) if err != nil { return "", false, fmt.Errorf("failed to search for record in cloudflare: %w", err) } if err := json.Unmarshal(raw, &records); err != nil { return "", false, fmt.Errorf("received malformed search results from cloudflare: %w", err) } body, _ := json.Marshal(map[string]any{ "type": string(rtype), "name": fqdn, "content": addr.String(), "ttl": 300, }) if len(records.Result) == 0 { // if nothing found, create c.log.Info("record not found in cloudflare, creating") _, err = request("POST", fmt.Sprintf("%s/zones/%s/dns_records", cfAPI, zoneID), c.headers(), bytes.NewReader(body)) return "", true, err } rec := records.Result[0] if rec.Content == addr.String() { return rec.Content, false, nil } // if record found, update c.log.Info("record found in cloudflare, updating", slog.Group("cloudflare", slog.String("id", rec.ID), slog.String("content", rec.Content))) _, err = request("PUT", fmt.Sprintf("%s/zones/%s/dns_records/%s", cfAPI, zoneID, rec.ID), c.headers(), bytes.NewReader(body)) return rec.Content, true, err }