diff options
| -rw-r--r-- | provider/bunny.go | 117 | ||||
| -rw-r--r-- | provider/provider.go | 4 |
2 files changed, 120 insertions, 1 deletions
diff --git a/provider/bunny.go b/provider/bunny.go new file mode 100644 index 0000000..c550a78 --- /dev/null +++ b/provider/bunny.go @@ -0,0 +1,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 +} diff --git a/provider/provider.go b/provider/provider.go index ce4a715..3f147e5 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -15,8 +15,8 @@ import ( type Kind string const ( - // only the generic dummy testing provider for now KindDummy Kind = "dummy" + KindBunny Kind = "bunny" ) type RecordType string @@ -75,6 +75,8 @@ func New(log *slog.Logger, name string, cfg Config) (Provider, error) { switch cfg.Kind { case KindDummy: return newDummy(log) + case KindBunny: + return newBunny(log, cfg) default: return nil, fmt.Errorf("unknown provider kind %q", cfg.Kind) } |
