diff options
| author | Mel <mel@rnrd.eu> | 2026-08-20 22:16:00 +0200 |
|---|---|---|
| committer | Mel <mel@rnrd.eu> | 2026-08-20 22:16:00 +0200 |
| commit | f65bd1b4e1010ec495a9dcbf28a91a5780ef6edf (patch) | |
| tree | e3522273629a0b420147fad00268b843a2b49732 | |
| parent | f896a9775c34c0e095273c193806ca245dfae940 (diff) | |
| download | ddnnss-f65bd1b4e1010ec495a9dcbf28a91a5780ef6edf.tar.zst ddnnss-f65bd1b4e1010ec495a9dcbf28a91a5780ef6edf.zip | |
Add Cloudflare API provider
Signed-off-by: Mel <mel@rnrd.eu>
| -rw-r--r-- | provider/cloudflare.go | 94 | ||||
| -rw-r--r-- | provider/provider.go | 7 |
2 files changed, 99 insertions, 2 deletions
diff --git a/provider/cloudflare.go b/provider/cloudflare.go new file mode 100644 index 0000000..8ca58bb --- /dev/null +++ b/provider/cloudflare.go @@ -0,0 +1,94 @@ +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 +} diff --git a/provider/provider.go b/provider/provider.go index 3f147e5..613d05f 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -15,8 +15,9 @@ import ( type Kind string const ( - KindDummy Kind = "dummy" - KindBunny Kind = "bunny" + KindDummy Kind = "dummy" + KindBunny Kind = "bunny" + KindCloudflare Kind = "cloudflare" ) type RecordType string @@ -77,6 +78,8 @@ func New(log *slog.Logger, name string, cfg Config) (Provider, error) { 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) } |
