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 }