diff --git a/OWNERS b/OWNERS index b1b524718..7c34732a4 100644 --- a/OWNERS +++ b/OWNERS @@ -2,6 +2,7 @@ providers/akamaiedgedns @svernick providers/autodns @arnoschoon providers/axfrddns @hnrgrgr providers/azuredns @vatsalyagoel +providers/azure_private_dns @matthewmgamble providers/bind @tlimoncelli providers/cloudflare @tresni providers/cloudns @pragmaton diff --git a/README.md b/README.md index 1b658f66b..bacfcf734 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Currently supported DNS providers: - AWS Route 53 - AXFR+DDNS - Azure DNS +- Azure Private DNS - BIND - Cloudflare - ClouDNS diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index b8e9f8791..b26e56638 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -100,6 +100,7 @@ * [AutoDNS](providers/autodns.md) * [AXFR+DDNS](providers/axfrddns.md) * [Azure DNS](providers/azure_dns.md) + * [Azure Private DNS](providers/azure_private_dns.md) * [BIND](providers/bind.md) * [Cloudflare](providers/cloudflareapi.md) * [ClouDNS](providers/cloudns.md) diff --git a/documentation/providers.md b/documentation/providers.md index 4f2c37a2a..798450a83 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -76,6 +76,7 @@ Providers in this category and their maintainers are: |Name|Maintainer| |---|---| |[`AZURE_DNS`](providers/azure_dns.md)|@vatsalyagoel| +[[`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md)|@matthewmgamble] |[`BIND`](providers/bind.md)|@tlimoncelli| |[`CLOUDFLAREAPI`](providers/cloudflareapi.md)|@tresni| |[`CSCGLOBAL`](providers/cscglobal.md)|@mikenz| diff --git a/documentation/providers/azure_private_dns.md b/documentation/providers/azure_private_dns.md new file mode 100644 index 000000000..b55feccd7 --- /dev/null +++ b/documentation/providers/azure_private_dns.md @@ -0,0 +1,74 @@ +## Configuration + +This provider is for the [Azure Private DNS Service](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview). This provider can only manage Azure Private DNS zones and will not manage public Azure DNS zones. To use this provider, add an entry to `creds.json` with `TYPE` set to `AZURE_PRIVATE_DNS` +along with the API credentials. + +Example: + +{% code title="creds.json" %} +```json +{ + "azure_private_dns_main": { + "TYPE": "AZURE_PRIVATE_DNS", + "SubscriptionID": "AZURE_PRIVATE_SUBSCRIPTION_ID", + "ResourceGroup": "AZURE_PRIVATE_RESOURCE_GROUP", + "TenantID": "AZURE_PRIVATE_TENANT_ID", + "ClientID": "AZURE_PRIVATE_CLIENT_ID", + "ClientSecret": "AZURE_PRIVATE_CLIENT_SECRET" + } +} +``` +{% endcode %} + +You can also use environment variables: + +```shell +export AZURE_SUBSCRIPTION_ID=XXXXXXXXX +export AZURE_RESOURCE_GROUP=YYYYYYYYY +export AZURE_TENANT_ID=ZZZZZZZZ +export AZURE_CLIENT_ID=AAAAAAAAA +export AZURE_CLIENT_SECRET=BBBBBBBBB +``` + +{% code title="creds.json" %} +```json +{ + "azure_private_dns_main": { + "TYPE": "AZURE_PRIVATE_DNS", + "SubscriptionID": "$AZURE_PRIVATE_SUBSCRIPTION_ID", + "ResourceGroup": "$AZURE_PRIVATE_RESOURCE_GROUP", + "ClientID": "$AZURE_PRIVATE_CLIENT_ID", + "TenantID": "$AZURE_PRIVATE_TENANT_ID", + "ClientSecret": "$AZURE_PRIVATE_CLIENT_SECRET" + } +} +``` +{% endcode %} + +## Metadata +This provider does not recognize any special metadata fields unique to Azure Private DNS. + +## Usage +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_AZURE_PRIVATE_MAIN = NewDnsProvider("azure_private_dns_main"); + +D("example.com", REG_NONE, DnsProvider(DSP_AZURE_PRIVATE_MAIN), + A("test", "1.2.3.4") +); +``` +{% endcode %} + +## Activation +DNSControl depends on a standard [Client credentials Authentication](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) with permission to list, create and update private zones. + +## New domains + +If a domain does not exist in your Azure account, DNSControl will *not* automatically add it with the `push` command. You can do that manually via the control panel. + +## Caveats + +The ResourceGroup is case sensitive. diff --git a/go.mod b/go.mod index 14b6f7689..38f253058 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.21.1 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 github.com/PuerkitoBio/goquery v1.8.1 diff --git a/go.sum b/go.sum index cb9d22efd..885ea93a7 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,12 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EX github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 h1:rR8ZW79lE/ppfXTfiYSnMFv5EzmVuY4pfZWIkscIJ64= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= diff --git a/providers/azure_private_dns/auditrecords.go b/providers/azure_private_dns/auditrecords.go new file mode 100644 index 000000000..6c478a955 --- /dev/null +++ b/providers/azure_private_dns/auditrecords.go @@ -0,0 +1,17 @@ +package azure_private_dns + +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("MX", rejectif.MxNull) // Last verified 2020-12-28 + + return a.Audit(records) +} diff --git a/providers/azure_private_dns/azurePrivateDnsProvider.go b/providers/azure_private_dns/azurePrivateDnsProvider.go new file mode 100644 index 000000000..7ba34adc0 --- /dev/null +++ b/providers/azure_private_dns/azurePrivateDnsProvider.go @@ -0,0 +1,534 @@ +package azure_private_dns + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + aauth "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + adns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + "github.com/Azure/go-autorest/autorest/to" + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" + "github.com/StackExchange/dnscontrol/v4/providers" +) + +type azurednsProvider struct { + zonesClient *adns.PrivateZonesClient + recordsClient *adns.RecordSetsClient + zones map[string]*adns.PrivateZone + resourceGroup *string + subscriptionID *string + rawRecords map[string][]*adns.RecordSet + zoneName map[string]string +} + +func newAzureDNSDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + return newAzureDNS(conf, metadata) +} + +func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvider, error) { + subID, rg := m["SubscriptionID"], m["ResourceGroup"] + clientID, clientSecret, tenantID := m["ClientID"], m["ClientSecret"], m["TenantID"] + credential, authErr := aauth.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) + if authErr != nil { + return nil, authErr + } + zonesClient, zoneErr := adns.NewPrivateZonesClient(subID, credential, nil) + if zoneErr != nil { + return nil, zoneErr + } + recordsClient, recordErr := adns.NewRecordSetsClient(subID, credential, nil) + if recordErr != nil { + return nil, recordErr + } + + api := &azurednsProvider{ + zonesClient: zonesClient, + recordsClient: recordsClient, + resourceGroup: to.StringPtr(rg), + subscriptionID: to.StringPtr(subID), + rawRecords: map[string][]*adns.RecordSet{}, + zoneName: map[string]string{}, + } + err := api.getZones() + if err != nil { + return nil, err + } + return api, nil +} + +var features = providers.DocumentationNotes{ + providers.CanGetZones: providers.Can(), + providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."), + providers.CanUseAzureAlias: providers.Can(), + providers.CanUseCAA: providers.Cannot("Azure Private DNS does not support CAA records"), + providers.CanUseLOC: providers.Cannot(), + providers.CanUseNAPTR: providers.Cannot(), + providers.CanUsePTR: providers.Can(), + providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Cannot(), + providers.CanUseTLSA: providers.Cannot(), + providers.DocCreateDomains: providers.Can(), + providers.DocDualHost: providers.Can("Azure does not permit modifying the existing NS records, only adding/removing additional records."), + providers.DocOfficiallySupported: providers.Can(), +} + +func init() { + fns := providers.DspFuncs{ + Initializer: newAzureDNSDsp, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType("AZURE_PRIVATE_DNS", fns, features) + providers.RegisterCustomRecordType("AZURE_ALIAS", "AZURE_PRIVATE_DNS", "") +} + +func (a *azurednsProvider) getExistingZones() ([]*adns.PrivateZone, error) { + ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second) + defer cancel() + zonesPager := a.zonesClient.NewListByResourceGroupPager(*a.resourceGroup, nil) + var zones []*adns.PrivateZone + for zonesPager.More() { + nextResult, zonesErr := zonesPager.NextPage(ctx) + if zonesErr != nil { + return nil, zonesErr + } + zones = append(zones, nextResult.Value...) + } + return zones, nil +} + +func (a *azurednsProvider) getZones() error { + a.zones = make(map[string]*adns.PrivateZone) + + zones, err := a.getExistingZones() + if err != nil { + return err + } + + for _, z := range zones { + zone := z + domain := strings.TrimSuffix(*z.Name, ".") + a.zones[domain] = zone + } + + return nil +} + +type errNoExist struct { + domain string +} + +func (e errNoExist) Error() string { + return fmt.Sprintf("Private Domain %s not found in you Azure account", e.domain) +} + +func (a *azurednsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { + // Azure Private DNS does not have the concept of "Name Servers" since these are local, private views of zones unique to the Azure environment + var nss []string + return models.ToNameserversStripTD(nss) +} + +func (a *azurednsProvider) ListZones() ([]string, error) { + zonesResult, err := a.getExistingZones() + if err != nil { + return nil, err + } + var zones []string + + for _, z := range zonesResult { + domain := strings.TrimSuffix(*z.Name, ".") + zones = append(zones, domain) + } + + return zones, nil +} + +// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. +func (a *azurednsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { + existingRecords, _, _, err := a.getExistingRecords(domain) + if err != nil { + return nil, err + } + + return existingRecords, nil +} + +func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, []*adns.RecordSet, string, error) { + zone, ok := a.zones[domain] + if !ok { + return nil, nil, "", errNoExist{domain} + } + zoneName := *zone.Name + rawRecords, err := a.fetchRecordSets(zoneName) + if err != nil { + return nil, nil, "", err + } + + var existingRecords models.Records + for _, set := range rawRecords { + existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...) + } + + a.rawRecords[domain] = rawRecords + a.zoneName[domain] = zoneName + + return existingRecords, rawRecords, zoneName, nil +} + +// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. +func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(existingRecords) // Autosplit long TXT records + + var corrections []*models.Correction + + changes, err := diff2.ByRecordSet(existingRecords, dc, nil) + if err != nil { + return nil, err + } + + for _, change := range changes { + + // Copy all param values to local variables to avoid overwrites + msgs := change.MsgsJoined + dcn := dc.Name + chaKey := change.Key + + switch change.Type { + case diff2.REPORT: + corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined}) + case diff2.CHANGE, diff2.CREATE: + changeNew := change.New + corrections = append(corrections, &models.Correction{ + Msg: msgs, + F: func() error { + return a.recordCreate(dcn, chaKey, changeNew) + }, + }) + case diff2.DELETE: + corrections = append(corrections, &models.Correction{ + Msg: msgs, + F: func() error { + return a.recordDelete(dcn, chaKey, change.Old) + }, + }) + default: + panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) + } + } + + return corrections, nil +} + +func (a *azurednsProvider) recordCreate(zoneName string, reckey models.RecordKey, recs models.Records) error { + + rrset, azRecType, err := a.recordToNativeDiff2(reckey, recs) + if err != nil { + return err + } + + var recordName string + var i int64 + for _, r := range recs { + i = int64(r.TTL) + recordName = r.Name + } + rrset.Properties.TTL = &i + + waitTime := 1 +retry: + + ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second) + defer cancel() + _, err = a.recordsClient.CreateOrUpdate(ctx, *a.resourceGroup, zoneName, azRecType, recordName, *rrset, nil) + + if e, ok := err.(*azcore.ResponseError); ok { + if e.StatusCode == 429 { + waitTime = waitTime * 2 + if waitTime > 300 { + return err + } + printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime) + time.Sleep(time.Duration(waitTime+1) * time.Second) + goto retry + } + } + + return err +} + +func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey, recs models.Records) error { + + shortName := strings.TrimSuffix(reckey.NameFQDN, "."+zoneName) + if shortName == zoneName { + shortName = "@" + } + + azRecType, err := nativeToRecordTypeDiff(to.StringPtr(reckey.Type)) + if err != nil { + return nil + } + + waitTime := 1 +retry: + + ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second) + defer cancel() + _, err = a.recordsClient.Delete(ctx, *a.resourceGroup, zoneName, azRecType, shortName, nil) + + if e, ok := err.(*azcore.ResponseError); ok { + if e.StatusCode == 429 { + waitTime = waitTime * 2 + if waitTime > 300 { + return err + } + printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime) + time.Sleep(time.Duration(waitTime+1) * time.Second) + goto retry + } + } + + return err +} + +func nativeToRecordTypeDiff(recordType *string) (adns.RecordType, error) { + recordTypeStripped := strings.TrimPrefix(*recordType, "Microsoft.Network/dnszones/") + switch recordTypeStripped { + case "A", "AZURE_ALIAS_A": + return adns.RecordTypeA, nil + case "AAAA", "AZURE_ALIAS_AAAA": + return adns.RecordTypeAAAA, nil + case "CAA": + // CAA doesn't make any senese in a private dns zone in azure + return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType) + case "CNAME", "AZURE_ALIAS_CNAME": + return adns.RecordTypeCNAME, nil + case "MX": + return adns.RecordTypeMX, nil + case "NS": + // NS record types don't make any sense in a private azure dns zone + return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType) + case "PTR": + return adns.RecordTypePTR, nil + case "SRV": + return adns.RecordTypeSRV, nil + case "TXT": + return adns.RecordTypeTXT, nil + case "SOA": + return adns.RecordTypeSOA, nil + default: + // Unimplemented type. Return adns.A as a decoy, but send an error. + return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType) + } +} + +func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig { + var results []*models.RecordConfig + switch rtype := *set.Type; rtype { + case "Microsoft.Network/privateDnsZones/A": + if set.Properties.ARecords != nil { + // This is an A recordset. Process all the targets there. + for _, rec := range set.Properties.ARecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "A" + _ = rc.SetTarget(*rec.IPv4Address) + results = append(results, rc) + } + } else { + panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type)) + + } + case "Microsoft.Network/privateDnsZones/AAAA": + if set.Properties.AaaaRecords != nil { + // This is an AAAA recordset. Process all the targets there. + for _, rec := range set.Properties.AaaaRecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "AAAA" + _ = rc.SetTarget(*rec.IPv6Address) + results = append(results, rc) + } + } else { + panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type)) + } + case "Microsoft.Network/privateDnsZones/CNAME": + if set.Properties.CnameRecord != nil { + // This is a CNAME recordset. Process the targets. (there can only be one) + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "CNAME" + _ = rc.SetTarget(*set.Properties.CnameRecord.Cname) + results = append(results, rc) + } else { + panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type)) + } + case "Microsoft.Network/privateDnsZones/PTR": + for _, rec := range set.Properties.PtrRecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "PTR" + _ = rc.SetTarget(*rec.Ptrdname) + results = append(results, rc) + } + case "Microsoft.Network/privateDnsZones/TXT": + if len(set.Properties.TxtRecords) == 0 { // Empty String Record Parsing + // This is a null TXT record. + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "TXT" + _ = rc.SetTargetTXT("") + results = append(results, rc) + } else { + // This is a normal TXT record. Collect all its segments. + for _, rec := range set.Properties.TxtRecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "TXT" + var txts []string + for _, txt := range rec.Value { + txts = append(txts, *txt) + } + _ = rc.SetTargetTXTs(txts) + results = append(results, rc) + } + } + case "Microsoft.Network/privateDnsZones/MX": + for _, rec := range set.Properties.MxRecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "MX" + _ = rc.SetTargetMX(uint16(*rec.Preference), *rec.Exchange) + results = append(results, rc) + } + case "Microsoft.Network/privateDnsZones/SRV": + for _, rec := range set.Properties.SrvRecords { + rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set} + rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin) + rc.Type = "SRV" + _ = rc.SetTargetSRV(uint16(*rec.Priority), uint16(*rec.Weight), uint16(*rec.Port), *rec.Target) + results = append(results, rc) + } + case "Microsoft.Network/privateDnsZones/SOA": + default: + panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type)) + } + return results +} + +// NOTE recordToNativeDiff2 is really "convert []RecordConfig to rrset". + +func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recordConfig []*models.RecordConfig) (*adns.RecordSet, adns.RecordType, error) { + + recordKeyType := recordKey.Type + // if recordKeyType == "AZURE_ALIAS" { + // fmt.Fprintf(os.Stderr, "DEBUG: XXXXXXXXXXXXXXXXXXXXXXX %v\n", recordKeyType) + // } + + recordSet := &adns.RecordSet{Type: to.StringPtr(recordKeyType), Properties: &adns.RecordSetProperties{}} + for _, rec := range recordConfig { + switch recordKeyType { + case "A": + if recordSet.Properties.ARecords == nil { + recordSet.Properties.ARecords = []*adns.ARecord{} + } + recordSet.Properties.ARecords = append(recordSet.Properties.ARecords, &adns.ARecord{IPv4Address: to.StringPtr(rec.GetTargetField())}) + case "AAAA": + if recordSet.Properties.AaaaRecords == nil { + recordSet.Properties.AaaaRecords = []*adns.AaaaRecord{} + } + recordSet.Properties.AaaaRecords = append(recordSet.Properties.AaaaRecords, &adns.AaaaRecord{IPv6Address: to.StringPtr(rec.GetTargetField())}) + case "CNAME": + recordSet.Properties.CnameRecord = &adns.CnameRecord{Cname: to.StringPtr(rec.GetTargetField())} + case "PTR": + if recordSet.Properties.PtrRecords == nil { + recordSet.Properties.PtrRecords = []*adns.PtrRecord{} + } + recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: to.StringPtr(rec.GetTargetField())}) + case "TXT": + if recordSet.Properties.TxtRecords == nil { + recordSet.Properties.TxtRecords = []*adns.TxtRecord{} + } + // Empty TXT record needs to have no value set in it's properties + if rec.GetTargetTXTJoined() == "" { + var txts []*string + for _, txt := range rec.GetTargetTXTSegmented() { + txts = append(txts, to.StringPtr(txt)) + } + recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, &adns.TxtRecord{Value: txts}) + } + case "MX": + if recordSet.Properties.MxRecords == nil { + recordSet.Properties.MxRecords = []*adns.MxRecord{} + } + recordSet.Properties.MxRecords = append(recordSet.Properties.MxRecords, &adns.MxRecord{Exchange: to.StringPtr(rec.GetTargetField()), Preference: to.Int32Ptr(int32(rec.MxPreference))}) + case "SRV": + if recordSet.Properties.SrvRecords == nil { + recordSet.Properties.SrvRecords = []*adns.SrvRecord{} + } + recordSet.Properties.SrvRecords = append(recordSet.Properties.SrvRecords, &adns.SrvRecord{Target: to.StringPtr(rec.GetTargetField()), Port: to.Int32Ptr(int32(rec.SrvPort)), Weight: to.Int32Ptr(int32(rec.SrvWeight)), Priority: to.Int32Ptr(int32(rec.SrvPriority))}) + /* CAA records don't work in a private zone */ + case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": + return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder + default: + return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder + } + } + + rt, err := nativeToRecordTypeDiff(to.StringPtr(*recordSet.Type)) + if err != nil { + return nil, adns.RecordTypeA, err // adns.A is a placeholder + } + return recordSet, rt, nil +} + +func (a *azurednsProvider) fetchRecordSets(zoneName string) ([]*adns.RecordSet, error) { + if zoneName == "" { + return nil, nil + } + var records []*adns.RecordSet + ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second) + defer cancel() + + recordsPager := a.recordsClient.NewListPager(*a.resourceGroup, zoneName, nil) + + for recordsPager.More() { + + waitTime := 1 + retry: + + nextResult, recordsErr := recordsPager.NextPage(ctx) + + if recordsErr != nil { + err := recordsErr + if e, ok := err.(*azcore.ResponseError); ok { + + if e.StatusCode == 429 { + waitTime = waitTime * 2 + if waitTime > 300 { + return nil, err + } + printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime) + time.Sleep(time.Duration(waitTime+1) * time.Second) + goto retry + } + } + } + + records = append(records, nextResult.Value...) + } + + return records, nil +} + +func (a *azurednsProvider) EnsureZoneExists(domain string) error { + if _, ok := a.zones[domain]; ok { + return nil + } + return nil +}