1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

NEW PROVIDER: Bunny DNS (#2265) (#2760)

This commit is contained in:
Pascal Mathis
2024-01-06 15:19:40 +01:00
committed by GitHub
parent 5daeafc73a
commit 961eaa7862
15 changed files with 730 additions and 3 deletions

View File

@ -87,7 +87,7 @@ jobs:
Write-Host "Integration test providers: $Providers"
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
env:
PROVIDERS: "['AZURE_DNS','BIND','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']"
PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']"
ENV_CONTEXT: ${{ toJson(env) }}
VARS_CONTEXT: ${{ toJson(vars) }}
SECRETS_CONTEXT: ${{ toJson(secrets) }}
@ -106,6 +106,7 @@ jobs:
# Set it to the domain name to use during the test.
AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
@ -129,6 +130,8 @@ jobs:
AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
BUNNY_DNS_API_KEY: ${{ secrets.BUNNY_DNS_API_KEY }}
CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}

View File

@ -36,7 +36,7 @@ changelog:
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
order: 1
- title: 'Provider-specific changes:'
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*"
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*"
order: 2
- title: 'Documentation:'
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"

1
OWNERS
View File

@ -4,6 +4,7 @@ providers/axfrddns @hnrgrgr
providers/azuredns @vatsalyagoel
providers/azureprivatedns @matthewmgamble
providers/bind @tlimoncelli
providers/bunnydns @ppmathis
providers/cloudflare @tresni
providers/cloudns @pragmaton
providers/cscglobal @mikenz

View File

@ -23,6 +23,7 @@ Currently supported DNS providers:
- Azure DNS
- Azure Private DNS
- BIND
- Bunny DNS
- Cloudflare
- ClouDNS
- deSEC

View File

@ -104,6 +104,7 @@
* [Azure DNS](providers/azure_dns.md)
* [Azure Private DNS](providers/azure_private_dns.md)
* [BIND](providers/bind.md)
* [Bunny DNS](providers/bunny_dns.md)
* [Cloudflare](providers/cloudflareapi.md)
* [ClouDNS](providers/cloudns.md)
* [CSC Global](providers/cscglobal.md)

View File

@ -20,6 +20,7 @@ If a feature is definitively not supported for whatever reason, we would also li
| [`AZURE_DNS`](providers/azure_dns.md) | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
| [`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md) | ✅ | ✅ | ❌ | ❌ | ❌ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
| [`BIND`](providers/bind.md) | ✅ | ✅ | ❌ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| [`BUNNY_DNS`](providers/bunny_dns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| [`CLOUDFLAREAPI`](providers/cloudflareapi.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❌ | ✅ | ✅ | ✅ |
| [`CLOUDNS`](providers/cloudns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
| [`CSCGLOBAL`](providers/cscglobal.md) | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ✅ |
@ -110,6 +111,7 @@ Providers in this category and their maintainers are:
|[`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md)|@matthewmgamble|
|[`AKAMAIEDGEDNS`](providers/akamaiedgedns.md)|@svernick|
|[`AXFRDDNS`](providers/axfrddns.md)|@hnrgrgr|
|[`BUNNY_DNS`](providers/bunny_dns.md)|@ppmathis|
|[`CLOUDFLAREAPI`](providers/cloudflareapi.md)|@tresni|
|[`CLOUDNS`](providers/cloudns.md)|@pragmaton|
|[`CSCGLOBAL`](providers/cscglobal.md)|@Air-New-Zealand|
@ -154,7 +156,6 @@ code to support this provider, we'd be glad to help in any way.
* [1984 Hosting](https://github.com/StackExchange/dnscontrol/issues/1251) (#1251)
* [Alibaba Cloud DNS](https://github.com/StackExchange/dnscontrol/issues/420)(#420)
* [BunnyDNS](https://github.com/StackExchange/dnscontrol/issues/2265)(#2265)
* [Constellix (DNSMadeEasy)](https://github.com/StackExchange/dnscontrol/issues/842) (#842)
* [CoreDNS](https://github.com/StackExchange/dnscontrol/issues/1284) (#1284)
* [EU.ORG](https://github.com/StackExchange/dnscontrol/issues/1176) (#1176)

View File

@ -0,0 +1,69 @@
# Configuration
To use this provider, add an entry to `creds.json` with `TYPE` set to `BUNNY_DNS` along with
your [Bunny API Key](https://dash.bunny.net/account/settings).
Example:
{% code title="creds.json" %}
```json
{
"bunny_dns": {
"TYPE": "BUNNY_DNS",
"api_key": "your-bunny-api-key"
}
}
```
{% endcode %}
You can also use environment variables:
```shell
export BUNNY_API_KEY=XXXXXXXXX
```
{% code title="creds.json" %}
```json
{
"bunny_dns": {
"TYPE": "BUNNY_DNS",
"api_key": "$BUNNY_API_KEY"
}
}
```
{% endcode %}
## Metadata
This provider does not recognize any special metadata fields unique to Bunny DNS.
## Usage
An example configuration:
{% code title="dnsconfig.js" %}
```javascript
var REG_NONE = NewRegistrar("none");
var DSP_BUNNY_DNS = NewDnsProvider("bunny_dns");
D("example.com", REG_NONE, DnsProvider(DSP_BUNNY_DNS),
A("test", "1.2.3.4")
);
```
{% endcode %}
# Activation
DNSControl depends on the [Bunny API](https://docs.bunny.net/reference/bunnynet-api-overview) to manage your DNS
records. You will need to generate an [API key](https://dash.bunny.net/account/settings) to use this provider.
## New domains
If a domain does not exist in your Bunny account, DNSControl will automatically add it with the `push` command.
## Caveats
- Bunny DNS does not support dual-hosting or configuring custom TTLs for NS records on the zone apex.
- While custom nameservers are properly recognized by this provider, it is currently not possible to configure them.
- Any custom record types like Script, Redirect, Flatten or Pull Zone are currently not supported by this provider. Such
records will be completely ignored by DNSControl and left as-is.

View File

@ -38,6 +38,11 @@
"TYPE": "BIND",
"domain": "$BIND_DOMAIN"
},
"BUNNY_DNS": {
"TYPE": "BUNNY_DNS",
"domain": "$BUNNY_DNS_DOMAIN",
"api_key": "$BUNNY_DNS_API_KEY"
},
"CLOUDFLAREAPI": {
"TYPE": "CLOUDFLAREAPI",
"accountid": "$CLOUDFLAREAPI_ACCOUNTID",

View File

@ -9,6 +9,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/azuredns"
_ "github.com/StackExchange/dnscontrol/v4/providers/azureprivatedns"
_ "github.com/StackExchange/dnscontrol/v4/providers/bind"
_ "github.com/StackExchange/dnscontrol/v4/providers/bunnydns"
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudns"
_ "github.com/StackExchange/dnscontrol/v4/providers/cscglobal"

227
providers/bunnydns/api.go Normal file
View File

@ -0,0 +1,227 @@
package bunnydns
import (
"bytes"
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"golang.org/x/exp/slices"
"io"
"net/http"
"strconv"
)
const (
baseURL = "https://api.bunny.net"
pageSize = 100
)
type zone struct {
ID int64 `json:"Id"`
Domain string `json:"Domain"`
Nameserver1 string `json:"Nameserver1"`
Nameserver2 string `json:"Nameserver2"`
}
func (zone *zone) Nameservers() []string {
return []string{zone.Nameserver1, zone.Nameserver2}
}
type record struct {
ID int64 `json:"Id,omitempty"`
Type recordType `json:"Type"`
Name string `json:"Name"`
Value string `json:"Value"`
Disabled bool `json:"Disabled"`
TTL uint32 `json:"Ttl"`
Flags uint8 `json:"Flags"`
Priority uint16 `json:"Priority"`
Weight uint16 `json:"Weight"`
Port uint16 `json:"Port"`
Tag string `json:"Tag"`
}
type listZonesResponse struct {
Items []zone `json:"Items"`
TotalItems int32 `json:"TotalItems"`
HasMoreItems bool `json:"HasMoreItems"`
}
type getZoneResponse struct {
zone
Records []record `json:"Records"`
}
type queryParams map[string]string
func (b *bunnydnsProvider) getImplicitRecordConfigs(zone *zone) (models.Records, error) {
nameservers := zone.Nameservers()
records := make(models.Records, 0, len(nameservers))
// NS records on the zone apex must be implicitly added, as Bunny DNS does not expose them via API
for _, ns := range nameservers {
rc := &models.RecordConfig{
Type: "NS",
Original: &record{},
}
rc.SetLabelFromFQDN(zone.Domain, zone.Domain)
if err := rc.SetTarget(ns + "."); err != nil {
return nil, err
}
records = append(records, rc)
}
return records, nil
}
func (b *bunnydnsProvider) findZoneByDomain(domain string) (*zone, error) {
if b.zones == nil {
zones, err := b.getAllZones()
if err != nil {
return nil, err
}
b.zones = make(map[string]*zone, len(zones))
for _, zone := range zones {
b.zones[zone.Domain] = zone
}
}
zone, ok := b.zones[domain]
if !ok {
return nil, fmt.Errorf("%q is not a zone in this BUNNY_DNS account", domain)
}
return zone, nil
}
func (b *bunnydnsProvider) getAllZones() ([]*zone, error) {
var zones []*zone
page := 1
for {
res := listZonesResponse{}
query := queryParams{"page": strconv.Itoa(page), "perPage": strconv.Itoa(pageSize)}
if err := b.request("GET", "/dnszone", query, nil, &res, nil); err != nil {
return nil, fmt.Errorf("could not fetch zones: %w", err)
}
if zones == nil {
zones = make([]*zone, 0, res.TotalItems)
}
for i := range res.Items {
zones = append(zones, &res.Items[i])
}
if !res.HasMoreItems {
break
}
page++
}
return zones, nil
}
func (b *bunnydnsProvider) createZone(domain string) (*zone, error) {
zone := &zone{}
body := map[string]string{"domain": domain}
err := b.request("POST", "/dnszone", nil, body, &zone, []int{http.StatusCreated})
if err != nil {
return nil, err
}
b.zones[domain] = zone
return zone, nil
}
func (b *bunnydnsProvider) getAllRecords(zoneID int64) ([]*record, error) {
zone := &getZoneResponse{}
err := b.request("GET", fmt.Sprintf("/dnszone/%d", zoneID), nil, nil, zone, nil)
if err != nil {
return nil, err
}
records := make([]*record, 0, len(zone.Records))
for i := range zone.Records {
records = append(records, &zone.Records[i])
}
return records, nil
}
func (b *bunnydnsProvider) createRecord(zoneID int64, r *record) error {
url := fmt.Sprintf("/dnszone/%d/records", zoneID)
return b.request("PUT", url, nil, r, nil, []int{http.StatusCreated})
}
func (b *bunnydnsProvider) modifyRecord(zoneID int64, recordID int64, r *record) error {
url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
return b.request("POST", url, nil, r, nil, []int{http.StatusNoContent})
}
func (b *bunnydnsProvider) deleteRecord(zoneID, recordID int64) error {
url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
return b.request("DELETE", url, nil, nil, nil, []int{http.StatusNoContent})
}
func (b *bunnydnsProvider) request(method, endpoint string, query queryParams, body, target any, validStatus []int) error {
if validStatus == nil {
validStatus = []int{http.StatusOK}
}
var requestBody io.Reader
if body != nil {
requestBodyJSON, err := json.Marshal(body)
if err != nil {
return err
}
requestBody = bytes.NewBuffer(requestBodyJSON)
}
req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
if err != nil {
return err
}
req.Header.Add("AccessKey", b.apiKey)
if requestBody != nil {
req.Header.Add("Content-Type", "application/json")
}
if query != nil {
q := req.URL.Query()
for k, v := range query {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
cleanup := func() {
if err := resp.Body.Close(); err != nil {
printer.Printf("BUNNY_DNS: Could not close response body after API call: %q\n", err)
}
}
if !slices.Contains(validStatus, resp.StatusCode) {
data, _ := io.ReadAll(resp.Body)
printer.Println(fmt.Sprintf("BUNNY_DNS: Bad API response for %s %s: %s", method, endpoint, string(data)))
cleanup()
return fmt.Errorf("bad status code from BUNNY_DNS: %d not in %v", resp.StatusCode, validStatus)
}
if target == nil {
cleanup()
return nil
}
err = json.NewDecoder(resp.Body).Decode(target)
cleanup()
return err
}

View File

@ -0,0 +1,17 @@
package bunnydns
import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
)
// AuditRecords returns a list of errors corresponding to the records
// that aren't supported by this provider. If all records are
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-02
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2024-01-02
return a.Audit(records)
}

View File

@ -0,0 +1,61 @@
package bunnydns
import (
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/providers"
)
var features = providers.DocumentationNotes{
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDHCID: providers.Cannot(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSOA: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
}
type bunnydnsProvider struct {
apiKey string
zones map[string]*zone
}
func init() {
fns := providers.DspFuncs{
Initializer: newBunnydns,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("BUNNY_DNS", fns, features)
}
func newBunnydns(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
apiKey := settings["api_key"]
if apiKey == "" {
return nil, fmt.Errorf("missing BUNNY_DNS api_key")
}
return &bunnydnsProvider{
apiKey: apiKey,
}, nil
}
func (b *bunnydnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
zone, err := b.findZoneByDomain(domain)
if err != nil {
return nil, err
}
return models.ToNameservers(zone.Nameservers())
}

View File

@ -0,0 +1,161 @@
package bunnydns
import (
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/miekg/dns/dnsutil"
"golang.org/x/exp/slices"
"strings"
)
var fqdnTypes = []recordType{recordTypeCNAME, recordTypeMX, recordTypeNS, recordTypePTR, recordTypeSRV}
func fromRecordConfig(rc *models.RecordConfig) (*record, error) {
r := record{
Type: recordTypeFromString(rc.Type),
Name: rc.GetLabel(),
Value: rc.GetTargetField(),
TTL: rc.TTL,
}
// While Bunny DNS does not use trailing dots, it still accepts and even preserves them for certain record types.
// To avoid confusion, any trailing dots are removed from the record value.
if slices.Contains(fqdnTypes, r.Type) && strings.HasSuffix(r.Value, ".") {
r.Value = strings.TrimSuffix(r.Value, ".")
}
switch r.Type {
case recordTypeNS:
if r.Name == "" {
r.TTL = 0
}
case recordTypeSRV:
r.Priority = rc.SrvPriority
r.Weight = rc.SrvWeight
r.Port = rc.SrvPort
case recordTypeCAA:
r.Flags = rc.CaaFlag
r.Tag = rc.CaaTag
case recordTypeMX:
r.Priority = rc.MxPreference
}
return &r, nil
}
func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) {
rc := models.RecordConfig{
Type: recordTypeToString(r.Type),
TTL: r.TTL,
Original: r,
}
rc.SetLabel(r.Name, domain)
// Bunny DNS always operates with fully-qualified names and does not use any trailing dots.
// If a record already contains a trailing dot, which the provider UI also accepts, the record value is left as-is.
recordValue := r.Value
if slices.Contains(fqdnTypes, r.Type) && !strings.HasSuffix(r.Value, ".") {
recordValue = dnsutil.AddOrigin(r.Value+".", domain)
}
var err error
switch rc.Type {
case "CAA":
err = rc.SetTargetCAA(r.Flags, r.Tag, recordValue)
case "MX":
err = rc.SetTargetMX(r.Priority, recordValue)
case "SRV":
err = rc.SetTargetSRV(r.Priority, r.Weight, r.Port, recordValue)
default:
err = rc.PopulateFromStringFunc(rc.Type, recordValue, domain, nil)
}
if err != nil {
return nil, err
}
return &rc, nil
}
type recordType int
const (
recordTypeA recordType = 0
recordTypeAAAA recordType = 1
recordTypeCNAME recordType = 2
recordTypeTXT recordType = 3
recordTypeMX recordType = 4
recordTypeRedirect recordType = 5
recordTypeFlatten recordType = 6
recordTypePullZone recordType = 7
recordTypeSRV recordType = 8
recordTypeCAA recordType = 9
recordTypePTR recordType = 10
recordTypeScript recordType = 11
recordTypeNS recordType = 12
)
func recordTypeFromString(t string) recordType {
switch t {
case "A":
return recordTypeA
case "AAAA":
return recordTypeAAAA
case "CNAME":
return recordTypeCNAME
case "TXT":
return recordTypeTXT
case "MX":
return recordTypeMX
case "REDIRECT":
return recordTypeRedirect
case "FLATTEN":
return recordTypeFlatten
case "PULL_ZONE":
return recordTypePullZone
case "SRV":
return recordTypeSRV
case "CAA":
return recordTypeCAA
case "PTR":
return recordTypePTR
case "SCRIPT":
return recordTypeScript
case "NS":
return recordTypeNS
default:
panic(fmt.Errorf("BUNNY_DNS: rtype %v unimplemented", t))
}
}
func recordTypeToString(t recordType) string {
switch t {
case recordTypeA:
return "A"
case recordTypeAAAA:
return "AAAA"
case recordTypeCNAME:
return "CNAME"
case recordTypeTXT:
return "TXT"
case recordTypeMX:
return "MX"
case recordTypeRedirect:
return "REDIRECT"
case recordTypeFlatten:
return "FLATTEN"
case recordTypePullZone:
return "PULL_ZONE"
case recordTypeSRV:
return "SRV"
case recordTypeCAA:
return "CAA"
case recordTypePTR:
return "PTR"
case recordTypeScript:
return "SCRIPT"
case recordTypeNS:
return "NS"
default:
panic(fmt.Errorf("BUNNY_DNS: native rtype %v unimplemented", t))
}
}

View File

@ -0,0 +1,32 @@
package bunnydns
import "github.com/StackExchange/dnscontrol/v4/pkg/printer"
func (b *bunnydnsProvider) ListZones() ([]string, error) {
zones, err := b.getAllZones()
if err != nil {
return nil, err
}
zoneNames := make([]string, 0, len(zones))
for _, zone := range zones {
zoneNames = append(zoneNames, zone.Domain)
}
return zoneNames, nil
}
func (b *bunnydnsProvider) EnsureZoneExists(domain string) error {
_, err := b.findZoneByDomain(domain)
if err == nil {
return nil
}
zone, err := b.createZone(domain)
if err != nil {
return err
}
printer.Warnf("BUNNY_DNS: Added zone %s with ID %d", domain, zone.ID)
return nil
}

View File

@ -0,0 +1,147 @@
package bunnydns
import (
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"golang.org/x/exp/slices"
)
func (b *bunnydnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
zone, err := b.findZoneByDomain(domain)
if err != nil {
return nil, err
}
nativeRecs, err := b.getAllRecords(zone.ID)
if err != nil {
return nil, err
}
implicitRecs, err := b.getImplicitRecordConfigs(zone)
if err != nil {
return nil, err
}
recs := make(models.Records, 0, len(nativeRecs)+len(implicitRecs))
recs = append(recs, implicitRecs...)
// Define a list of record types that are currently not supported by this provider.
unsupportedTypes := []recordType{
recordTypeRedirect,
recordTypeFlatten,
recordTypePullZone,
recordTypeScript,
}
// Loop through all native records and convert them to standardized RecordConfigs
// Unsupported record types are ignored with a warning and will remain untouched in the zone.
for _, nativeRec := range nativeRecs {
if slices.Contains(unsupportedTypes, nativeRec.Type) {
printer.Warnf("BUNNY_DNS: ignoring unsupported record type %s\n", recordTypeToString(nativeRec.Type))
continue
}
rc, err := toRecordConfig(zone.Domain, nativeRec)
if err != nil {
return nil, err
}
recs = append(recs, rc)
}
return recs, nil
}
func (b *bunnydnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
// Bunny DNS never returns NS records for the apex domain, so these are artificially added when retrieving records.
// As no TTL can be configured or retrieved for these NS records, we set it to 0 to avoid unnecessary updates.
for _, rc := range dc.Records {
if rc.Name == "@" && rc.Type == "NS" {
rc.TTL = 0
}
}
zone, err := b.findZoneByDomain(dc.Name)
if err != nil {
return nil, err
}
instructions, err := diff2.ByRecord(existing, dc, nil)
if err != nil {
return nil, err
}
var corrections []*models.Correction
for _, inst := range instructions {
switch inst.Type {
case diff2.REPORT:
corrections = append(corrections, &models.Correction{
Msg: inst.MsgsJoined,
})
case diff2.CREATE:
corrections = append(corrections, b.mkCreateCorrection(
zone.ID, inst.New[0], inst.Msgs[0],
))
case diff2.CHANGE:
corrections = append(corrections, b.mkChangeCorrection(
zone.ID, inst.Old[0], inst.New[0], inst.Msgs[0],
))
case diff2.DELETE:
corrections = append(corrections, b.mkDeleteCorrection(
zone.ID, inst.Old[0], inst.Msgs[0],
))
default:
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
}
}
return corrections, nil
}
func (b *bunnydnsProvider) mkCreateCorrection(zoneID int64, newRec *models.RecordConfig, msg string) *models.Correction {
return &models.Correction{
Msg: msg,
F: func() error {
desired, err := fromRecordConfig(newRec)
if err != nil {
return err
}
return b.createRecord(zoneID, desired)
},
}
}
func (b *bunnydnsProvider) mkChangeCorrection(zoneID int64, oldRec, newRec *models.RecordConfig, msg string) *models.Correction {
return &models.Correction{
Msg: msg,
F: func() error {
existingID := oldRec.Original.(*record).ID
if existingID == 0 {
return fmt.Errorf("BUNNY_DNS: cannot change implicit records")
}
desired, err := fromRecordConfig(newRec)
if err != nil {
return err
}
return b.modifyRecord(zoneID, existingID, desired)
},
}
}
func (b *bunnydnsProvider) mkDeleteCorrection(zoneID int64, oldRec *models.RecordConfig, msg string) *models.Correction {
return &models.Correction{
Msg: msg,
F: func() error {
existingID := oldRec.Original.(*record).ID
if existingID == 0 {
return fmt.Errorf("BUNNY_DNS: cannot delete implicit records")
}
return b.deleteRecord(zoneID, existingID)
},
}
}