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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
package provider
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/netip"
"net/url"
)
type bunny struct {
log *slog.Logger
key string
}
var _ Provider = (*bunny)(nil)
func newBunny(log *slog.Logger, cfg Config) (*bunny, error) {
key, err := cfg.key()
if err != nil {
return nil, fmt.Errorf("failed instantiating bunny provider: %w", err)
}
return &bunny{key: key, log: log}, nil
}
const bunnyAPI = "https://api.bunny.net/dnszone"
// bunny encodes record types numerically.
var bunnyTypes = map[RecordType]int{TypeA: 0, TypeAAAA: 1}
func (b *bunny) headers() map[string]string {
return map[string]string{"AccessKey": b.key}
}
func (b *bunny) Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error) {
b.log.Info("updating record via bunny.net api")
var zones struct {
Items []struct {
ID int64 `json:"Id"`
Domain string `json:"Domain"`
} `json:"Items"`
}
raw, err := request("GET", bunnyAPI+"?search="+url.QueryEscape(zone),
b.headers(), nil)
if err != nil {
return "", false, err
}
if err := json.Unmarshal(raw, &zones); err != nil {
return "", false, err
}
var zoneID int64 = -1
for _, z := range zones.Items {
if z.Domain == zone {
zoneID = z.ID
}
}
if zoneID < 0 {
return "", false, fmt.Errorf("zone %s not found in bunny", zone)
}
var details struct {
Records []struct {
ID int64 `json:"Id"`
Type int `json:"Type"`
Name string `json:"Name"`
Value string `json:"Value"`
} `json:"Records"`
}
raw, err = request("GET", fmt.Sprintf("%s/%d", bunnyAPI, zoneID), b.headers(), nil)
if err != nil {
return "", false, fmt.Errorf("failed to request zone details from bunny: %w", err)
}
if err := json.Unmarshal(raw, &details); err != nil {
return "", false, fmt.Errorf("received malformed zone details from bunny: %w", err)
}
body, _ := json.Marshal(map[string]any{
"Type": bunnyTypes[rtype],
"Name": name,
"Value": addr.String(),
"Ttl": 300,
})
for _, r := range details.Records {
if r.Type != bunnyTypes[rtype] || r.Name != name {
continue
}
if r.Value == addr.String() {
return r.Value, false, nil
}
b.log.Info(
"bunny knows this record, attempting update",
slog.Group(
"bunny",
slog.Int64("id", r.ID),
slog.Int("type", r.Type),
slog.String("name", r.Name),
slog.String("value", r.Value),
),
)
// if found, update
_, err = request("POST", fmt.Sprintf("%s/%d/records/%d", bunnyAPI, zoneID, r.ID),
b.headers(), bytes.NewReader(body))
return r.Value, true, err
}
b.log.Info("bunny does not know this record, attempting to create")
// if not found, create
_, err = request("PUT", fmt.Sprintf("%s/%d/records", bunnyAPI, zoneID),
b.headers(), bytes.NewReader(body))
return "", true, err
}
|