AUTODNS: Enable "get-zones" (ListZones, EnsureZoneExists, GetRegistrarCorrections) (#3568)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Florian Klink 2025-05-13 22:12:40 +03:00 committed by GitHub
parent 277a260d64
commit be081cddad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 179 additions and 11 deletions

View file

@ -4,9 +4,11 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"os"
"sort"
"time"
@ -62,7 +64,8 @@ func (api *autoDNSProvider) request(method string, requestPath string, data inte
responseText, _ := io.ReadAll(response.Body)
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusTooManyRequests {
// FUTUREWORK: 202 starts a long-running task. Should we instead poll here until task is completed?
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted && response.StatusCode != http.StatusTooManyRequests {
return nil, errors.New("Request to " + requestURL.Path + " failed: " + string(responseText))
}
@ -99,9 +102,12 @@ func (api *autoDNSProvider) findZoneSystemNameServer(domain string) (*models.Nam
}
var responseObject JSONResponseDataZone
_ = json.Unmarshal(responseData, &responseObject)
if err := json.Unmarshal(responseData, &responseObject); err != nil {
return nil, err
}
if len(responseObject.Data) != 1 {
return nil, errors.New("Domain " + domain + " could not be found in AutoDNS")
return nil, fmt.Errorf("Zone "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist)
}
systemNameServer := &models.Nameserver{Name: responseObject.Data[0].SystemNameServer}
@ -109,6 +115,42 @@ func (api *autoDNSProvider) findZoneSystemNameServer(domain string) (*models.Nam
return systemNameServer, nil
}
func (api *autoDNSProvider) createZone(domain string, zone *Zone) (*Zone, error) {
responseData, err := api.request("POST", "zone", zone)
if err != nil {
return nil, err
}
var responseObject JSONResponseDataZone
if err := json.Unmarshal(responseData, &responseObject); err != nil {
return nil, err
}
if len(responseObject.Data) != 1 {
return nil, errors.New("Zone " + domain + " not returned")
}
return responseObject.Data[0], nil
}
func (api *autoDNSProvider) getDomain(domain string) (*Domain, error) {
responseData, err := api.request("GET", "domain/"+domain, nil)
if err != nil {
return nil, err
}
var responseObject JSONResponseDataDomain
if err := json.Unmarshal(responseData, &responseObject); err != nil {
return nil, err
}
if len(responseObject.Data) != 1 {
return nil, fmt.Errorf("Domain "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist)
}
return responseObject.Data[0], nil
}
func (api *autoDNSProvider) getZone(domain string) (*Zone, error) {
systemNameServer, err := api.findZoneSystemNameServer(domain)
if err != nil {
@ -124,14 +166,32 @@ func (api *autoDNSProvider) getZone(domain string) (*Zone, error) {
var responseObject JSONResponseDataZone
// make sure that the response is valid, the zone is in AutoDNS but we're not sure the returned data meets our expectation
unmErr := json.Unmarshal(responseData, &responseObject)
if unmErr != nil {
return nil, unmErr
if err := json.Unmarshal(responseData, &responseObject); err != nil {
return nil, err
}
return responseObject.Data[0], nil
}
func (api *autoDNSProvider) getZones() ([]string, error) {
responseData, err := api.request("POST", "zone/_search", nil)
if err != nil {
return nil, err
}
var responseObject JSONResponseDataZone
if err := json.Unmarshal(responseData, &responseObject); err != nil {
return nil, err
}
names := make([]string, 0, len(responseObject.Data))
for _, zone := range responseObject.Data {
names = append(names, zone.Origin)
}
return names, nil
}
func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*ResourceRecord, nameServers []*models.Nameserver, zoneTTL uint32) error {
systemNameServer, err := api.findZoneSystemNameServer(domain)
if err != nil {
@ -172,3 +232,12 @@ func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*Resourc
return nil
}
func (api *autoDNSProvider) updateDomain(name string, domain *Domain) error {
_, err := api.request("PUT", "domain/"+name, domain)
if err != nil {
return err
}
return nil
}

View file

@ -11,7 +11,8 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25
a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2025-05-13
return a.Audit(records)
}

View file

@ -6,13 +6,16 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/StackExchange/dnscontrol/v4/providers/bind"
)
var features = providers.DocumentationNotes{
@ -41,15 +44,19 @@ func init() {
const providerName = "AUTODNS"
const providerMaintainer = "@arnoschoon"
fns := providers.DspFuncs{
Initializer: New,
Initializer: func(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
return new(settings), nil
},
RecordAuditor: AuditRecords,
}
providers.RegisterRegistrarType(providerName, func(settings map[string]string) (providers.Registrar, error) {
return new(settings), nil
}, features)
providers.RegisterDomainServiceProviderType(providerName, fns, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New creates a new API handle.
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
func new(settings map[string]string) *autoDNSProvider {
api := &autoDNSProvider{}
api.baseURL = url.URL{
@ -68,7 +75,7 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
"X-Domainrobot-Context": []string{settings["context"]},
}
return api, nil
return api
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
@ -243,6 +250,74 @@ func (api *autoDNSProvider) GetZoneRecords(domain string, meta map[string]string
return existingRecords, nil
}
func (api *autoDNSProvider) EnsureZoneExists(domain string) error {
// try to get zone
_, err := api.getZone(domain)
if !errors.Is(err, os.ErrNotExist) {
return err
}
_, err = api.createZone(domain, &Zone{
Origin: domain,
NameServers: []*models.Nameserver{
{Name: "a.ns14.net"}, {Name: "b.ns14.net"},
{Name: "c.ns14.net"}, {Name: "d.ns14.net"},
},
Soa: &bind.SoaDefaults{
Expire: 1209600,
Refresh: 43200,
Retry: 7200,
TTL: 86400,
},
})
return err
}
func (api *autoDNSProvider) ListZones() ([]string, error) {
return api.getZones()
}
func (api *autoDNSProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
domain, err := api.getDomain(dc.Name)
if err != nil {
return nil, err
}
existingNs := make([]string, 0, len(domain.NameServers))
for _, ns := range domain.NameServers {
existingNs = append(existingNs, ns.Name)
}
sort.Strings(existingNs)
existing := strings.Join(existingNs, ",")
desiredNs := models.NameserversToStrings(dc.Nameservers)
sort.Strings(desiredNs)
desired := strings.Join(desiredNs, ",")
if existing != desired {
return []*models.Correction{
{
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", existing, desired),
F: func() error {
nameservers := make([]*NameServer, 0, len(desiredNs))
for _, name := range desiredNs {
nameservers = append(nameservers, &NameServer{
Name: name,
})
}
return api.updateDomain(dc.Name, &Domain{
NameServers: nameservers,
})
},
},
}, nil
}
return nil, nil
}
func toRecordConfig(domain string, record *ResourceRecord) (*models.RecordConfig, error) {
rc := &models.RecordConfig{
Type: record.Type,

View file

@ -61,8 +61,31 @@ type Zone struct {
SystemNameServer string `json:"virtualNameServer,omitempty"`
}
// Domain represents the Domain in API calls.
// These are only present for domains where AUTODNS also is a registrar.
type Domain struct {
Name string `json:"name,omitempty"`
NameServers []*NameServer `json:"nameServers"`
Zone *Zone `json:"zone,omitempty"`
}
type NameServer struct {
// Host name of the nameserver written as a Fully-Qualified-Domain-Name (FQDN).
Name string `json:"name"`
// Time-to-live value of the nameservers in seconds
TTL uint64 `json:"ttl,omitempty"`
// IPv4 and IPv6 addresses of the name server. For GLUE records only; optional. The values for the IP addresses are only relevant for domain operations and are only used there in the case of glue name servers.
IPAddresses []string `json:"ipAddresses,omitempty"`
}
// JSONResponseDataZone represents the response to the DataZone call.
type JSONResponseDataZone struct {
// The data for the response. The type of the objects are depending on the request and are also specified in the responseObject value of the response.
Data []*Zone `json:"data"`
}
type JSONResponseDataDomain struct {
// The data for the response. The type of the objects are depending on the request and are also specified in the responseObject value of the response.
Data []*Domain `json:"data"`
}