diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go index a34b597a9..355c67e63 100644 --- a/build/generate/featureMatrix.go +++ b/build/generate/featureMatrix.go @@ -39,6 +39,7 @@ func generateFeatureMatrix() error { {"TLSA", "Provider can manage TLSA records"}, {"TXTMulti", "Provider can manage TXT records with multiple strings"}, {"R53_ALIAS", "Provider supports Route 53 limited ALIAS"}, + {"AZURE_ALIAS", "Provider supports Azure DNS limited ALIAS"}, {"dual host", "This provider is recommended for use in 'dual hosting' scenarios. Usually this means the provider allows full control over the apex NS records"}, {"create-domains", "This means the provider can automatically create domains that do not currently exist on your account. The 'dnscontrol create-domains' command will initialize any missing domains"}, @@ -80,6 +81,7 @@ func generateFeatureMatrix() error { setCap("NAPTR", providers.CanUseNAPTR) setCap("PTR", providers.CanUsePTR) setCap("R53_ALIAS", providers.CanUseRoute53Alias) + setCap("AZURE_ALIAS", providers.CanUseAzureAlias) setCap("SRV", providers.CanUseSRV) setCap("SSHFP", providers.CanUseSSHFP) setCap("TLSA", providers.CanUseTLSA) diff --git a/docs/_functions/domain/AZURE_ALIAS.md b/docs/_functions/domain/AZURE_ALIAS.md new file mode 100644 index 000000000..0034a2c7e --- /dev/null +++ b/docs/_functions/domain/AZURE_ALIAS.md @@ -0,0 +1,54 @@ +--- +name: AZURE_ALIAS +parameters: + - name + - type + - target + - modifiers ... +--- + +AZURE_ALIAS is a Azure specific virtual record type that points a record at either another record or an Azure entity. +It is analogous to a CNAME, but is usually resolved at request-time and served as an A record. +Unlike CNAMEs, ALIAS records can be used at the zone apex (`@`) + +Unlike the regular ALIAS directive, AZURE_ALIAS is only supported on AZURE. +Attempting to use AZURE_ALIAS on another provider than Azure will result in an error. + +The name should be the relative label for the domain. + +The type can be any of the following: +* A +* AAAA +* CNAME + +Target should be the Azure Id representing the target. It starts `/subscription/`. The resource id can be found in https://resources.azure.com/. + +The Target can : + +* Point to a public IP resource from a DNS `A/AAAA` record set. +You can create an A/AAAA record set and make it an alias record set to point to a public IP resource (standard or basic). +The DNS record set changes automatically if the public IP address changes or is deleted. +Dangling DNS records that point to incorrect IP addresses are avoided. +There is a current limit of 20 alias records sets per resource. +* Point to a Traffic Manager profile from a DNS `A/AAAA/CNAME` record set. +You can create an A/AAAA or CNAME record set and use alias records to point it to a Traffic Manager profile. +It's especially useful when you need to route traffic at a zone apex, as traditional CNAME records aren't supported for a zone apex. +For example, say your Traffic Manager profile is myprofile.trafficmanager.net and your business DNS zone is contoso.com. +You can create an alias record set of type A/AAAA for contoso.com (the zone apex) and point to myprofile.trafficmanager.net. +* Point to an Azure Content Delivery Network (CDN) endpoint. +This is useful when you create static websites using Azure storage and Azure CDN. +* Point to another DNS record set within the same zone. +Alias records can reference other record sets of the same type. +For example, a DNS CNAME record set can be an alias to another CNAME record set. +This arrangement is useful if you want some record sets to be aliases and some non-aliases. + +{% include startExample.html %} +{% highlight js %} + +D("example.com", REGISTRAR, DnsProvider("AZURE_DNS"), + AZURE_ALIAS("foo", "A", "/subscriptions/726f8cd6-6459-4db4-8e6d-2cd2716904e2/resourceGroups/test/providers/Microsoft.Network/trafficManagerProfiles/testpp2"), // record for traffic manager + AZURE_ALIAS("foo", "CNAME", "/subscriptions/726f8cd6-6459-4db4-8e6d-2cd2716904e2/resourceGroups/test/providers/Microsoft.Network/dnszones/example.com/A/quux."), // record in the same zone +); + +{%endhighlight%} +{% include endExample.html %} \ No newline at end of file diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html index 604cf175d..5fde8ddfc 100644 --- a/docs/_includes/matrix.html +++ b/docs/_includes/matrix.html @@ -263,7 +263,7 @@ - + @@ -706,9 +706,7 @@ R53_ALIAS - - - + @@ -736,6 +734,35 @@ + + AZURE_ALIAS + + + + + + + + + + + + + + + + + + + + + + + + + + + dual host diff --git a/docs/alias.md b/docs/alias.md index 056c69dfe..95c19dd03 100644 --- a/docs/alias.md +++ b/docs/alias.md @@ -24,4 +24,4 @@ func init() { 3. CNAMEs at `@` are disallowed, but ALIAS is allowed. 4. Cloudflare does not have a native ALIAS type, but CNAMEs behave similarly. The Cloudflare provider "rewrites" ALIAS records to CNAME as it sees them. Other providers may not need this step. 5. Route 53 requires the use of R53_ALIAS instead of ALIAS. - +6. Azure DNS requires the use of AZURE_ALIAS instead of ALIAS. diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7775172c1..d72c5603e 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -26,10 +26,10 @@ func init() { flag.Parse() } -func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bool) { +func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bool, map[string]string) { if *providerToRun == "" { t.Log("No provider specified with -provider") - return nil, "", nil + return nil, "", nil, nil } jsons, err := config.LoadProviderConfigs("providers.json") if err != nil { @@ -53,19 +53,19 @@ func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bo fails[i] = true } } - return provider, cfg["domain"], fails + return provider, cfg["domain"], fails, cfg } t.Fatalf("Provider %s not found", *providerToRun) - return nil, "", nil + return nil, "", nil, nil } func TestDNSProviders(t *testing.T) { - provider, domain, fails := getProvider(t) + provider, domain, fails, cfg := getProvider(t) if provider == nil { return } t.Run(fmt.Sprintf("%s", domain), func(t *testing.T) { - runTests(t, provider, domain, fails) + runTests(t, provider, domain, fails, cfg) }) } @@ -84,7 +84,7 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid return dc } -func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, knownFailures map[int]bool) { +func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, knownFailures map[int]bool, origConfig map[string]string) { dc := getDomainConfigWithNameservers(t, prv, domainName) // run tests one at a time end := *endIdx @@ -107,11 +107,22 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, for _, r := range tst.Records { rc := models.RecordConfig(*r) if strings.Contains(rc.GetTargetField(), "**current-domain**") { - rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain**", domainName, 1) + ".") + _ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain**", domainName, 1) + ".") + } + if strings.Contains(rc.GetTargetField(), "**current-domain-no-trailing**") { + _ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**current-domain-no-trailing**", domainName, 1)) } if strings.Contains(rc.GetLabelFQDN(), "**current-domain**") { rc.SetLabelFromFQDN(strings.Replace(rc.GetLabelFQDN(), "**current-domain**", domainName, 1), domainName) } + if providers.ProviderHasCapability(*providerToRun, providers.CanUseAzureAlias) { + if strings.Contains(rc.GetTargetField(), "**subscription-id**") { + _ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**subscription-id**", origConfig["SubscriptionID"], 1)) + } + if strings.Contains(rc.GetTargetField(), "**resource-group**") { + _ = rc.SetTarget(strings.Replace(rc.GetTargetField(), "**resource-group**", origConfig["ResourceGroup"], 1)) + } + } dom.Records = append(dom.Records, &rc) } dom.IgnoredLabels = tst.IgnoredLabels @@ -156,7 +167,7 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, } func TestDualProviders(t *testing.T) { - p, domain, _ := getProvider(t) + p, domain, _, _ := getProvider(t) if p == nil { return } @@ -239,6 +250,14 @@ func r53alias(name, aliasType, target string) *rec { return r } +func azureAlias(name, aliasType, target string) *rec { + r := makeRec(name, target, "AZURE_ALIAS") + r.AzureAlias = map[string]string{ + "type": aliasType, + } + return r +} + func ns(name, target string) *rec { return makeRec(name, target, "NS") } @@ -642,6 +661,20 @@ func makeTests(t *testing.T) []*TestCase { tc("Update 1200 records", manyA("rec%04d", "1.2.3.5", 1200)...), ) } + // AZURE_ALIAS + if !providers.ProviderHasCapability(*providerToRun, providers.CanUseAzureAlias) { + t.Log("Skipping AZURE_ALIAS Tests because provider does not support them") + } else { + t.Log("SubscriptionID: ") + tests = append(tests, tc("Empty"), + tc("create dependent A records", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5")), + tc("ALIAS to A record in same zone", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5"), azureAlias("bar.a", "A", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/A/foo.a")), + tc("change it", a("foo.a", "1.2.3.4"), a("quux.a", "2.3.4.5"), azureAlias("bar.a", "A", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/A/quux.a")), + tc("create dependent CNAME records", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com")), + tc("ALIAS to CNAME record in same zone", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com"), azureAlias("bar", "CNAME", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/CNAME/foo.cname")), + tc("change it", cname("foo.cname", "google.com"), cname("quux.cname", "google2.com"), azureAlias("bar.cname", "CNAME", "/subscriptions/**subscription-id**/resourceGroups/**resource-group**/providers/Microsoft.Network/dnszones/**current-domain-no-trailing**/CNAME/quux.cname")), + ) + } // Empty last tests = append(tests, tc("Empty")) diff --git a/models/domain.go b/models/domain.go index b08bcf555..ad240733b 100644 --- a/models/domain.go +++ b/models/domain.go @@ -79,7 +79,7 @@ func (dc *DomainConfig) Punycode() error { if err != nil { return err } - case "A", "AAAA", "CAA", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA": + case "A", "AAAA", "CAA", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS": // Nothing to do. default: msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type) diff --git a/models/record.go b/models/record.go index c838e9bbb..b6affe1c3 100644 --- a/models/record.go +++ b/models/record.go @@ -95,6 +95,7 @@ type RecordConfig struct { TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one. R53Alias map[string]string `json:"r53_alias,omitempty"` + AzureAlias map[string]string `json:"azure_alias,omitempty"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. } @@ -307,6 +308,12 @@ func (rc *RecordConfig) Key() RecordKey { // label with different alias types are considered separate. t = fmt.Sprintf("%s_%s", t, v) } + } else if rc.AzureAlias != nil { + if v, ok := rc.AzureAlias["type"]; ok { + // Azure aliases append their alias type, so that records for the same + // label with different alias types are considered separate. + t = fmt.Sprintf("%s_%s", t, v) + } } return RecordKey{rc.NameFQDN, t} } diff --git a/models/target.go b/models/target.go index ad81c627b..f443a5960 100644 --- a/models/target.go +++ b/models/target.go @@ -52,6 +52,9 @@ func (rc *RecordConfig) GetTargetCombined() string { case "R53_ALIAS": // Differentiate between multiple R53_ALIASs on the same label. return fmt.Sprintf("%s atype=%s zone_id=%s", rc.Target, rc.R53Alias["type"], rc.R53Alias["zone_id"]) + case "AZURE_ALIAS": + // Differentiate between multiple AZURE_ALIASs on the same label. + return fmt.Sprintf("%s atype=%s", rc.Target, rc.AzureAlias["type"]) case "SOA": return fmt.Sprintf("%s %v %d %d %d %d %d", rc.Target, rc.SoaMbox, rc.SoaSerial, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl) default: @@ -101,6 +104,8 @@ func (rc *RecordConfig) GetTargetDebug() string { content += fmt.Sprintf(" caatag=%s caaflag=%d", rc.CaaTag, rc.CaaFlag) case "R53_ALIAS": content += fmt.Sprintf(" type=%s zone_id=%s", rc.R53Alias["type"], rc.R53Alias["zone_id"]) + case "AZURE_ALIAS": + content += fmt.Sprintf(" type=%s", rc.AzureAlias["type"]) default: panic(fmt.Errorf("rc.String rtype %v unimplemented", rc.Type)) // We panic so that we quickly find any switch statements diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..870d78e95 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==" + } + } +} diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 1e2920394..2050a8f33 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -165,6 +165,31 @@ var AAAA = recordBuilder('AAAA'); // ALIAS(name,target, recordModifiers...) var ALIAS = recordBuilder('ALIAS'); +// AZURE_ALIAS(name, type, target, recordModifiers...) +var AZURE_ALIAS = recordBuilder('AZURE_ALIAS', { + args: [ + ['name', _.isString], + ['type', validateAzureAliasType], + ['target', _.isString], + ], + transform: function(record, args, modifier) { + record.name = args.name; + record.target = args.target; + if (_.isObject(record.azure_alias)) { + record.azure_alias['type'] = args.type; + } else { + record.azure_alias = { type: args.type }; + } + }, +}); + +function validateAzureAliasType(value) { + if (!_.isString(value)) { + return false; + } + return ['A', 'AAAA', 'CNAME'].indexOf(value) !== -1; +} + // R53_ALIAS(name, target, type, recordModifiers...) var R53_ALIAS = recordBuilder('R53_ALIAS', { args: [ @@ -210,7 +235,7 @@ function validateR53AliasType(value) { 'SPF', 'SRV', 'NAPTR', - ].indexOf(value) != -1 + ].indexOf(value) !== -1 ); } @@ -741,7 +766,8 @@ function CAA_BUILDER(value) { (!value.issue && !value.issuewild) || (value.issue && value.issue.length == 0 && - value.issuewild && value.issuewild.length == 0) + value.issuewild && + value.issuewild.length == 0) ) { throw 'CAA_BUILDER requires at least one entry at issue or issuewild'; } diff --git a/pkg/js/parse_tests/026-azure-alias.js b/pkg/js/parse_tests/026-azure-alias.js new file mode 100644 index 000000000..06ea367d5 --- /dev/null +++ b/pkg/js/parse_tests/026-azure-alias.js @@ -0,0 +1,5 @@ +D("foo.com", "none", + AZURE_ALIAS("atest", "A", "foo.com."), + AZURE_ALIAS("aaaatest", "AAAA", "foo.com."), + AZURE_ALIAS("cnametest", "CNAME", "foo.com.") +); \ No newline at end of file diff --git a/pkg/js/parse_tests/026-azure-alias.json b/pkg/js/parse_tests/026-azure-alias.json new file mode 100644 index 000000000..22bed19fe --- /dev/null +++ b/pkg/js/parse_tests/026-azure-alias.json @@ -0,0 +1,37 @@ +{ + "registrars": [], + "dns_providers": [], + "domains": [ + { + "name": "foo.com", + "registrar": "none", + "dnsProviders": {}, + "records": [ + { + "type": "AZURE_ALIAS", + "name": "atest", + "target": "foo.com.", + "azure_alias": { + "type": "A" + } + }, + { + "type": "AZURE_ALIAS", + "name": "aaaatest", + "target": "foo.com.", + "azure_alias": { + "type": "AAAA" + } + }, + { + "type": "AZURE_ALIAS", + "name": "cnametest", + "target": "foo.com.", + "azure_alias": { + "type": "CNAME" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/js/static.go b/pkg/js/static.go index a7adadd89..ce4795023 100644 --- a/pkg/js/static.go +++ b/pkg/js/static.go @@ -212,103 +212,105 @@ var _escData = map[string]*_escFile{ "/helpers.js": { name: "helpers.js", local: "pkg/js/helpers.js", - size: 22107, + size: 22817, modtime: 0, compressed: ` -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 +Ef83AAD//yJmQYUhWQAA `, }, } diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 8f0122c88..fe81d66ac 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -465,6 +465,7 @@ func init() { {"SSHFP", providers.CanUseSSHFP}, {"SRV", providers.CanUseSRV}, {"TLSA", providers.CanUseTLSA}, + {"AZURE_ALIAS", providers.CanUseAzureAlias}, } } diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 0e68c271e..a8ea397fa 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -17,10 +17,11 @@ import ( ) type azureDnsProvider struct { - zonesClient *adns.ZonesClient - recordsClient *adns.RecordSetsClient - zones map[string]*adns.Zone - resourceGroup *string + zonesClient *adns.ZonesClient + recordsClient *adns.RecordSetsClient + zones map[string]*adns.Zone + resourceGroup *string + subscriptionId *string } func newAzureDnsDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { @@ -41,7 +42,7 @@ func newAzureDns(m map[string]string, metadata json.RawMessage) (*azureDnsProvid zonesClient.Authorizer = authorizer recordsClient.Authorizer = authorizer - api := &azureDnsProvider{zonesClient: &zonesClient, recordsClient: &recordsClient, resourceGroup: to.StringPtr(rg)} + api := &azureDnsProvider{zonesClient: &zonesClient, recordsClient: &recordsClient, resourceGroup: to.StringPtr(rg), subscriptionId: to.StringPtr(subId)} err := api.getZones() if err != nil { return nil, err @@ -50,7 +51,7 @@ func newAzureDns(m map[string]string, metadata json.RawMessage) (*azureDnsProvid } var features = providers.DocumentationNotes{ - providers.CanUseAlias: providers.Cannot("Only supported for Azure Resources. Not yet implemented"), + providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."), 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(), @@ -58,15 +59,16 @@ var features = providers.DocumentationNotes{ providers.CanUseSRV: providers.Can(), providers.CanUseTXTMulti: providers.Can(), providers.CanUseCAA: providers.Can(), - providers.CanUseRoute53Alias: providers.Cannot(), providers.CanUseNAPTR: providers.Cannot(), providers.CanUseSSHFP: providers.Cannot(), providers.CanUseTLSA: providers.Cannot(), providers.CanGetZones: providers.Can(), + providers.CanUseAzureAlias: providers.Can(), } func init() { providers.RegisterDomainServiceProviderType("AZURE_DNS", newAzureDnsDsp, features) + providers.RegisterCustomRecordType("AZURE_ALIAS", "AZURE_DNS", "") } func (a *azureDnsProvider) getExistingZones() (*adns.ZoneListResult, error) { @@ -229,7 +231,7 @@ func (a *azureDnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod return nil, fmt.Errorf("no record set found to delete. Name: '%s'. Type: '%s'", k.NameFQDN, k.Type) } } else { - rrset, recordType := recordToNative(k, recs) + rrset, recordType := a.recordToNative(k, recs) var recordName string for _, r := range recs { i := int64(r.TTL) @@ -284,13 +286,13 @@ func (a *azureDnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod func nativeToRecordType(recordType *string) adns.RecordType { recordTypeStripped := strings.TrimPrefix(*recordType, "Microsoft.Network/dnszones/") switch recordTypeStripped { - case "A": + case "A", "AZURE_ALIAS_A": return adns.A - case "AAAA": + case "AAAA", "AZURE_ALIAS_AAAA": return adns.AAAA case "CAA": return adns.CAA - case "CNAME": + case "CNAME", "AZURE_ALIAS_CNAME": return adns.CNAME case "MX": return adns.MX @@ -321,6 +323,17 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig _ = rc.SetTarget(*rec.Ipv4Address) results = append(results, rc) } + } else { + rc := &models.RecordConfig{ + Type: "AZURE_ALIAS", + TTL: uint32(*set.TTL), + AzureAlias: map[string]string{ + "type": "A", + }, + } + rc.SetLabelFromFQDN(*set.Fqdn, origin) + _ = rc.SetTarget(*set.TargetResource.ID) + results = append(results, rc) } case "Microsoft.Network/dnszones/AAAA": if set.AaaaRecords != nil { @@ -331,13 +344,37 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig _ = rc.SetTarget(*rec.Ipv6Address) results = append(results, rc) } + } else { + rc := &models.RecordConfig{ + Type: "AZURE_ALIAS", + TTL: uint32(*set.TTL), + AzureAlias: map[string]string{ + "type": "AAAA", + }, + } + rc.SetLabelFromFQDN(*set.Fqdn, origin) + _ = rc.SetTarget(*set.TargetResource.ID) + results = append(results, rc) } case "Microsoft.Network/dnszones/CNAME": - rc := &models.RecordConfig{TTL: uint32(*set.TTL)} - rc.SetLabelFromFQDN(*set.Fqdn, origin) - rc.Type = "CNAME" - _ = rc.SetTarget(*set.CnameRecord.Cname) - results = append(results, rc) + if set.CnameRecord != nil { + rc := &models.RecordConfig{TTL: uint32(*set.TTL)} + rc.SetLabelFromFQDN(*set.Fqdn, origin) + rc.Type = "CNAME" + _ = rc.SetTarget(*set.CnameRecord.Cname) + results = append(results, rc) + } else { + rc := &models.RecordConfig{ + Type: "AZURE_ALIAS", + TTL: uint32(*set.TTL), + AzureAlias: map[string]string{ + "type": "CNAME", + }, + } + rc.SetLabelFromFQDN(*set.Fqdn, origin) + _ = rc.SetTarget(*set.TargetResource.ID) + results = append(results, rc) + } case "Microsoft.Network/dnszones/NS": for _, rec := range *set.NsRecords { rc := &models.RecordConfig{TTL: uint32(*set.TTL)} @@ -401,7 +438,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig return results } -func recordToNative(recordKey models.RecordKey, recordConfig []*models.RecordConfig) (*adns.RecordSet, adns.RecordType) { +func (a *azureDnsProvider) recordToNative(recordKey models.RecordKey, recordConfig []*models.RecordConfig) (*adns.RecordSet, adns.RecordType) { recordSet := &adns.RecordSet{Type: to.StringPtr(recordKey.Type), RecordSetProperties: &adns.RecordSetProperties{}} for _, rec := range recordConfig { switch recordKey.Type { @@ -450,11 +487,15 @@ func recordToNative(recordKey models.RecordKey, recordConfig []*models.RecordCon recordSet.CaaRecords = &[]adns.CaaRecord{} } *recordSet.CaaRecords = append(*recordSet.CaaRecords, adns.CaaRecord{Value: to.StringPtr(rec.Target), Tag: to.StringPtr(rec.CaaTag), Flags: to.Int32Ptr(int32(rec.CaaFlag))}) + case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": + *recordSet.Type = rec.AzureAlias["type"] + recordSet.TargetResource = &adns.SubResource{ID: to.StringPtr(rec.Target)} default: panic(fmt.Errorf("rc.String rtype %v unimplemented", recordKey.Type)) } } - return recordSet, nativeToRecordType(to.StringPtr(recordKey.Type)) + + return recordSet, nativeToRecordType(to.StringPtr(*recordSet.Type)) } func (a *azureDnsProvider) fetchRecordSets(zoneName string) ([]*adns.RecordSet, error) { diff --git a/providers/capabilities.go b/providers/capabilities.go index 1bb52f628..8f8a5d61f 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -56,6 +56,9 @@ const ( // CanGetZoe indicates the provider supports the get-zones subcommand. CanGetZones + + // CanUseAzureAlias indicates the provider support the specific Azure_ALIAS records that only the Azure provider supports + CanUseAzureAlias ) var providerCapabilities = map[string]map[Capability]bool{}