summaryrefslogtreecommitdiff
path: root/provider/cloudflare.go
blob: 8ca58bb5ca66b6ddfc01ae767a2ca26577d1047e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
}