summaryrefslogtreecommitdiff
path: root/provider/cloudflare.go
diff options
context:
space:
mode:
Diffstat (limited to 'provider/cloudflare.go')
-rw-r--r--provider/cloudflare.go94
1 files changed, 94 insertions, 0 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
+}