summaryrefslogtreecommitdiff
path: root/provider
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-08-20 22:16:00 +0200
committerMel <mel@rnrd.eu>2026-08-20 22:16:00 +0200
commitf65bd1b4e1010ec495a9dcbf28a91a5780ef6edf (patch)
treee3522273629a0b420147fad00268b843a2b49732 /provider
parentf896a9775c34c0e095273c193806ca245dfae940 (diff)
downloadddnnss-f65bd1b4e1010ec495a9dcbf28a91a5780ef6edf.tar.zst
ddnnss-f65bd1b4e1010ec495a9dcbf28a91a5780ef6edf.zip
Add Cloudflare API provider
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'provider')
-rw-r--r--provider/cloudflare.go94
-rw-r--r--provider/provider.go7
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)
}