dnscontrol/providers/netlify/netlifyProvider.go
2023-10-22 13:56:13 -04:00

226 lines
5.7 KiB
Go

package netlify
import (
"encoding/json"
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns"
)
var features = providers.DocumentationNotes{
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot("Netlify does not allow sufficient control over the apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
fns := providers.DspFuncs{
Initializer: newNetlify,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NETLIFY", fns, features)
providers.RegisterCustomRecordType("NETLIFY", "NETLIFY", "")
providers.RegisterCustomRecordType("NETLIFYv6", "NETLIFY", "")
}
type netlifyProvider struct {
apiToken string // the account access token
accountSlug string // the account identifier slug. optional.
}
func newNetlify(m map[string]string, message json.RawMessage) (providers.DNSServiceProvider, error) {
api := &netlifyProvider{}
api.apiToken = m["token"]
if api.apiToken == "" {
return nil, fmt.Errorf("missing Netlify personal access token")
}
api.accountSlug = m["slug"]
return api, nil
}
func (n *netlifyProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
zone, err := n.getZone(domain)
if err != nil {
return nil, err
}
return models.ToNameservers(zone.DNSServers)
}
func (n *netlifyProvider) getZone(domain string) (*dnsZone, error) {
zones, err := n.getDNSZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
if zone.Name == domain {
return zone, nil
}
}
return nil, fmt.Errorf("no zones found for this domain")
}
func (n *netlifyProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
zone, err := n.getZone(domain)
if err != nil {
return nil, err
}
records, err := n.getDNSRecords(zone.ID)
if err != nil {
return nil, err
}
cleanRecords := make(models.Records, 0)
for _, r := range records {
if r.Type == "SOA" {
continue
}
rec := &models.RecordConfig{
TTL: uint32(r.TTL),
Original: r,
}
rec.SetLabelFromFQDN(r.Hostname, domain) // netlify returns the FQDN
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" {
r.Value = dns.CanonicalName(r.Value)
}
switch rtype := r.Type; rtype {
case "NETLIFY", "NETLIFYv6": // transparently ignore
continue
case "MX":
err = rec.SetTargetMX(uint16(r.Priority), r.Value)
case "SRV":
parts := strings.Fields(r.Value)
if len(parts) == 3 {
r.Value += "."
}
err = rec.SetTargetSRV(uint16(r.Priority), r.Weight, r.Port, r.Value)
case "TXT":
err = rec.SetTargetTXT(r.Value)
case "CAA":
err = rec.SetTargetCAA(uint8(r.Flag), r.Tag, r.Value)
default:
err = rec.PopulateFromString(r.Type, r.Value, domain)
}
if err != nil {
return nil, fmt.Errorf("unparsable record received from Netlify: %w", err)
}
cleanRecords = append(cleanRecords, rec)
}
return cleanRecords, nil
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func (n *netlifyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(records)
if err != nil {
return nil, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
zone, err := n.getZone(dc.Name)
if err != nil {
return nil, err
}
// Deletes first so changing type works etc.
for _, m := range del {
id := m.Existing.Original.(*dnsRecord).ID
corr := &models.Correction{
Msg: m.String(),
F: func() error {
return n.deleteDNSRecord(zone.ID, id)
},
}
corrections = append(corrections, corr)
}
for _, m := range create {
req := toReq(m.Desired)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
_, err := n.createDNSRecord(zone.ID, req)
return err
},
}
corrections = append(corrections, corr)
}
for _, m := range modify {
id := m.Existing.Original.(*dnsRecord).ID
req := toReq(m.Desired)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
if err := n.deleteDNSRecord(zone.ID, id); err != nil {
return err
}
_, err := n.createDNSRecord(zone.ID, req)
return err
},
}
corrections = append(corrections, corr)
}
return corrections, nil
}
func toReq(rc *models.RecordConfig) *dnsRecordCreate {
name := rc.GetLabelFQDN() // Netlify wants the FQDN
target := rc.GetTargetField()
priority := int64(0)
switch rc.Type {
case "MX":
priority = int64(rc.MxPreference)
case "SRV":
priority = int64(rc.SrvPriority)
case "TXT":
target = rc.GetTargetTXTJoined()
default:
// no action required
}
return &dnsRecordCreate{
Type: rc.Type,
Hostname: name,
Value: target,
TTL: int64(rc.TTL),
Priority: priority,
Port: int64(rc.SrvPort),
Weight: int64(rc.SrvWeight),
Tag: rc.CaaTag,
Flag: int64(rc.CaaFlag),
}
}