Skip to content

Commit be081cd

Browse files
floklitlimoncelli
andauthored
AUTODNS: Enable "get-zones" (ListZones, EnsureZoneExists, GetRegistrarCorrections) (#3568)
Co-authored-by: Tom Limoncelli <[email protected]>
1 parent 277a260 commit be081cd

File tree

4 files changed

+179
-11
lines changed

4 files changed

+179
-11
lines changed

providers/autodns/api.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"io"
89
"math"
910
"net/http"
11+
"os"
1012
"sort"
1113
"time"
1214

@@ -62,7 +64,8 @@ func (api *autoDNSProvider) request(method string, requestPath string, data inte
6264

6365
responseText, _ := io.ReadAll(response.Body)
6466

65-
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusTooManyRequests {
67+
// FUTUREWORK: 202 starts a long-running task. Should we instead poll here until task is completed?
68+
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted && response.StatusCode != http.StatusTooManyRequests {
6669
return nil, errors.New("Request to " + requestURL.Path + " failed: " + string(responseText))
6770
}
6871

@@ -99,16 +102,55 @@ func (api *autoDNSProvider) findZoneSystemNameServer(domain string) (*models.Nam
99102
}
100103

101104
var responseObject JSONResponseDataZone
102-
_ = json.Unmarshal(responseData, &responseObject)
105+
if err := json.Unmarshal(responseData, &responseObject); err != nil {
106+
return nil, err
107+
}
108+
103109
if len(responseObject.Data) != 1 {
104-
return nil, errors.New("Domain " + domain + " could not be found in AutoDNS")
110+
return nil, fmt.Errorf("Zone "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist)
105111
}
106112

107113
systemNameServer := &models.Nameserver{Name: responseObject.Data[0].SystemNameServer}
108114

109115
return systemNameServer, nil
110116
}
111117

118+
func (api *autoDNSProvider) createZone(domain string, zone *Zone) (*Zone, error) {
119+
responseData, err := api.request("POST", "zone", zone)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
var responseObject JSONResponseDataZone
125+
if err := json.Unmarshal(responseData, &responseObject); err != nil {
126+
return nil, err
127+
}
128+
129+
if len(responseObject.Data) != 1 {
130+
return nil, errors.New("Zone " + domain + " not returned")
131+
}
132+
133+
return responseObject.Data[0], nil
134+
}
135+
136+
func (api *autoDNSProvider) getDomain(domain string) (*Domain, error) {
137+
responseData, err := api.request("GET", "domain/"+domain, nil)
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
var responseObject JSONResponseDataDomain
143+
if err := json.Unmarshal(responseData, &responseObject); err != nil {
144+
return nil, err
145+
}
146+
147+
if len(responseObject.Data) != 1 {
148+
return nil, fmt.Errorf("Domain "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist)
149+
}
150+
151+
return responseObject.Data[0], nil
152+
}
153+
112154
func (api *autoDNSProvider) getZone(domain string) (*Zone, error) {
113155
systemNameServer, err := api.findZoneSystemNameServer(domain)
114156
if err != nil {
@@ -124,14 +166,32 @@ func (api *autoDNSProvider) getZone(domain string) (*Zone, error) {
124166

125167
var responseObject JSONResponseDataZone
126168
// make sure that the response is valid, the zone is in AutoDNS but we're not sure the returned data meets our expectation
127-
unmErr := json.Unmarshal(responseData, &responseObject)
128-
if unmErr != nil {
129-
return nil, unmErr
169+
if err := json.Unmarshal(responseData, &responseObject); err != nil {
170+
return nil, err
130171
}
131172

132173
return responseObject.Data[0], nil
133174
}
134175

176+
func (api *autoDNSProvider) getZones() ([]string, error) {
177+
responseData, err := api.request("POST", "zone/_search", nil)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
var responseObject JSONResponseDataZone
183+
if err := json.Unmarshal(responseData, &responseObject); err != nil {
184+
return nil, err
185+
}
186+
187+
names := make([]string, 0, len(responseObject.Data))
188+
for _, zone := range responseObject.Data {
189+
names = append(names, zone.Origin)
190+
}
191+
192+
return names, nil
193+
}
194+
135195
func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*ResourceRecord, nameServers []*models.Nameserver, zoneTTL uint32) error {
136196
systemNameServer, err := api.findZoneSystemNameServer(domain)
137197
if err != nil {
@@ -172,3 +232,12 @@ func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*Resourc
172232

173233
return nil
174234
}
235+
236+
func (api *autoDNSProvider) updateDomain(name string, domain *Domain) error {
237+
_, err := api.request("PUT", "domain/"+name, domain)
238+
if err != nil {
239+
return err
240+
}
241+
242+
return nil
243+
}

providers/autodns/auditrecords.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
func AuditRecords(records []*models.RecordConfig) []error {
1212
a := rejectif.Auditor{}
1313

14-
a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25
14+
a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25
15+
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2025-05-13
1516

1617
return a.Audit(records)
1718
}

providers/autodns/autoDnsProvider.go

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import (
66
"fmt"
77
"net/http"
88
"net/url"
9+
"os"
910
"regexp"
11+
"sort"
1012
"strconv"
1113
"strings"
1214

1315
"github.com/StackExchange/dnscontrol/v4/models"
1416
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
1517
"github.com/StackExchange/dnscontrol/v4/providers"
18+
"github.com/StackExchange/dnscontrol/v4/providers/bind"
1619
)
1720

1821
var features = providers.DocumentationNotes{
@@ -41,15 +44,19 @@ func init() {
4144
const providerName = "AUTODNS"
4245
const providerMaintainer = "@arnoschoon"
4346
fns := providers.DspFuncs{
44-
Initializer: New,
47+
Initializer: func(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
48+
return new(settings), nil
49+
},
4550
RecordAuditor: AuditRecords,
4651
}
52+
providers.RegisterRegistrarType(providerName, func(settings map[string]string) (providers.Registrar, error) {
53+
return new(settings), nil
54+
}, features)
4755
providers.RegisterDomainServiceProviderType(providerName, fns, features)
4856
providers.RegisterMaintainer(providerName, providerMaintainer)
4957
}
5058

51-
// New creates a new API handle.
52-
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
59+
func new(settings map[string]string) *autoDNSProvider {
5360
api := &autoDNSProvider{}
5461

5562
api.baseURL = url.URL{
@@ -68,7 +75,7 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
6875
"X-Domainrobot-Context": []string{settings["context"]},
6976
}
7077

71-
return api, nil
78+
return api
7279
}
7380

7481
// 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
243250
return existingRecords, nil
244251
}
245252

253+
func (api *autoDNSProvider) EnsureZoneExists(domain string) error {
254+
// try to get zone
255+
_, err := api.getZone(domain)
256+
257+
if !errors.Is(err, os.ErrNotExist) {
258+
return err
259+
}
260+
261+
_, err = api.createZone(domain, &Zone{
262+
Origin: domain,
263+
NameServers: []*models.Nameserver{
264+
{Name: "a.ns14.net"}, {Name: "b.ns14.net"},
265+
{Name: "c.ns14.net"}, {Name: "d.ns14.net"},
266+
},
267+
Soa: &bind.SoaDefaults{
268+
Expire: 1209600,
269+
Refresh: 43200,
270+
Retry: 7200,
271+
TTL: 86400,
272+
},
273+
})
274+
275+
return err
276+
}
277+
278+
func (api *autoDNSProvider) ListZones() ([]string, error) {
279+
return api.getZones()
280+
}
281+
282+
func (api *autoDNSProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
283+
domain, err := api.getDomain(dc.Name)
284+
if err != nil {
285+
return nil, err
286+
}
287+
288+
existingNs := make([]string, 0, len(domain.NameServers))
289+
for _, ns := range domain.NameServers {
290+
existingNs = append(existingNs, ns.Name)
291+
}
292+
sort.Strings(existingNs)
293+
existing := strings.Join(existingNs, ",")
294+
295+
desiredNs := models.NameserversToStrings(dc.Nameservers)
296+
sort.Strings(desiredNs)
297+
desired := strings.Join(desiredNs, ",")
298+
299+
if existing != desired {
300+
return []*models.Correction{
301+
{
302+
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", existing, desired),
303+
F: func() error {
304+
nameservers := make([]*NameServer, 0, len(desiredNs))
305+
for _, name := range desiredNs {
306+
nameservers = append(nameservers, &NameServer{
307+
Name: name,
308+
})
309+
}
310+
return api.updateDomain(dc.Name, &Domain{
311+
NameServers: nameservers,
312+
})
313+
},
314+
},
315+
}, nil
316+
}
317+
318+
return nil, nil
319+
}
320+
246321
func toRecordConfig(domain string, record *ResourceRecord) (*models.RecordConfig, error) {
247322
rc := &models.RecordConfig{
248323
Type: record.Type,

providers/autodns/types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,31 @@ type Zone struct {
6161
SystemNameServer string `json:"virtualNameServer,omitempty"`
6262
}
6363

64+
// Domain represents the Domain in API calls.
65+
// These are only present for domains where AUTODNS also is a registrar.
66+
type Domain struct {
67+
Name string `json:"name,omitempty"`
68+
69+
NameServers []*NameServer `json:"nameServers"`
70+
Zone *Zone `json:"zone,omitempty"`
71+
}
72+
73+
type NameServer struct {
74+
// Host name of the nameserver written as a Fully-Qualified-Domain-Name (FQDN).
75+
Name string `json:"name"`
76+
// Time-to-live value of the nameservers in seconds
77+
TTL uint64 `json:"ttl,omitempty"`
78+
// 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.
79+
IPAddresses []string `json:"ipAddresses,omitempty"`
80+
}
81+
6482
// JSONResponseDataZone represents the response to the DataZone call.
6583
type JSONResponseDataZone struct {
6684
// 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.
6785
Data []*Zone `json:"data"`
6886
}
87+
88+
type JSONResponseDataDomain struct {
89+
// 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.
90+
Data []*Domain `json:"data"`
91+
}

0 commit comments

Comments
 (0)