Repository: StackExchange/dnscontrol
Pull request #2306 (2023-05-01 21:44:18)

ROUTE53: fix R53_ZONE() handling for domains
Fixes https://github.com/StackExchange/dnscontrol/issues/2296


Patch:
diff --git a/commands/getZones.go b/commands/getZones.go
index 9be04e08..3a09c526 100644
--- a/commands/getZones.go
+++ b/commands/getZones.go
@@ -200,7 +200,7 @@ func GetZone(args GetZoneArgs) error {
 	// fetch all of the records
 	zoneRecs := make([]models.Records, len(zones))
 	for i, zone := range zones {
-		recs, err := provider.GetZoneRecords(zone)
+		recs, err := provider.GetZoneRecords(zone, nil)
 		if err != nil {
 			return fmt.Errorf("failed GetZone gzr: %w", err)
 		}
diff --git a/commands/previewPush.go b/commands/previewPush.go
index e47fcada..c4ae92d4 100644
--- a/commands/previewPush.go
+++ b/commands/previewPush.go
@@ -160,7 +160,8 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 		func(domain *models.DomainConfig) {
 			defer wg.Done() // defer notify WaitGroup this anonymous function has finished
 
-			if !args.shouldRunDomain(domain.UniqueName) {
+			uniquename := domain.GetUniqueName()
+			if !args.shouldRunDomain(uniquename) {
 				return
 			}
 
@@ -171,7 +172,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 
 			// Correct the domain...
 
-			out.StartDomain(domain.UniqueName)
+			out.StartDomain(uniquename)
 			var providersWithExistingZone []*models.DNSProviderInstance
 			/// For each DSP...
 			for _, provider := range domain.DNSProviderInstances {
diff --git a/documentation/providers/bind.md b/documentation/providers/bind.md
index a2347001..95174dbd 100644
--- a/documentation/providers/bind.md
+++ b/documentation/providers/bind.md
@@ -90,8 +90,8 @@ file name is the name as specified in the `D()` function plus ".zone".
 The filenameformat is a string with a few printf-like `%` verbs:
 
   * `%U`  the domain name as specified in `D()`
-  * `%D`  the domain name without any split horizon tag
-  * `%T`  the split horizon tag, or "", see `D()`
+  * `%D`  the domain name without any split horizon tag (the "example.com" part of "example.com!tag")
+  * `%T`  the split horizon tag, or "" (the "tag" part of "example.com!tag")
   * `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable.
   * `%%`  `%`
   * ordinary characters (not `%`) are copied unchanged to the output stream
@@ -111,7 +111,7 @@ Typical values:
 
 The last example will generate the same name for both
 `D("example.tld!inside")` and `D("example.tld!outside")`.  This
-assumes two BIND providers are configured in `creds.json`, eacch with
+assumes two BIND providers are configured in `creds.json`, each with
 a different `directory` setting. Otherwise `dnscontrol` will write
 both domains to the same file, flapping between the two back and
 forth.
diff --git a/documentation/providers/route53.md b/documentation/providers/route53.md
index 805f724f..ac0a3730 100644
--- a/documentation/providers/route53.md
+++ b/documentation/providers/route53.md
@@ -78,6 +78,30 @@ D("example.tld", REG_NONE, DnsProvider(DSP_R53),
 ```
 {% endcode %}
 
+## Split horizon
+
+This provider supports spilt horizons using the `R53_ZONE()` domain function.
+
+In this example the domain `testzone.net` appears in the same account twice,
+each with different zone IDs specified using `R53_ZONE()`.
+
+```
+var DSP_R53 = NewDnsProvider("r53_main");
+var REG_NONE = NewRegistrar("none");
+
+D('testzone.net!private', REG_NONE,
+    DnsProvider(DSP_R53),
+    R53_ZONE('Z111111111JCCCP1V7UW'),
+    TXT('me',	'private testzone.net'),
+);
+
+D('testzone.net!public', REG_NONE,
+    DnsProvider(DSP_R53),
+    R53_ZONE('Z222222222INNG98SHJQ2'),
+    TXT('me',	'public testzone.net'),
+);
+```
+
 ## Activation
 DNSControl depends on a standard [AWS access key](https://aws.amazon.com/developers/access-keys/) with permission to list, create and update hosted zones. If you do not have the permissions required you will receive the following error message `Check your credentials, your not authorized to perform actions on Route 53 AWS Service`.
 
diff --git a/models/domain.go b/models/domain.go
index 136d746f..ebabe912 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -2,19 +2,25 @@ package models
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/qdm12/reprint"
 	"golang.org/x/net/idna"
 )
 
+const (
+	DOMAIN_UNIQUENAME = "dnscontrol_uniquename"
+	DOMAIN_TAG        = "dnscontrol_tag"
+)
+
 // DomainConfig describes a DNS domain (technically a DNS zone).
 type DomainConfig struct {
 	Name             string         `json:"name"` // NO trailing "."
-	Tag              string         `json:"-"`    // split horizon tag
-	UniqueName       string         `json:"-"`    // .Name + "!" + .Tag
 	RegistrarName    string         `json:"registrar"`
 	DNSProviderNames map[string]int `json:"dnsProviders"`
 
+	// Metadata[DOMAIN_UNIQUENAME] // .Name + "!" + .Tag
+	// Metadata[DOMAIN_TAG] // split horizon tag
 	Metadata    map[string]string `json:"meta,omitempty"`
 	Records     Records           `json:"records"`
 	Nameservers []*Nameserver     `json:"nameservers,omitempty"`
@@ -38,6 +44,41 @@ type DomainConfig struct {
 	DNSProviderInstances []*DNSProviderInstance `json:"-"`
 }
 
+// GetSplitHorizonNames returns the domain's name, uniquename, and tag.
+func (dc *DomainConfig) GetSplitHorizonNames() (name, uniquename, tag string) {
+	return dc.Name, dc.Metadata[DOMAIN_UNIQUENAME], dc.Metadata[DOMAIN_TAG]
+}
+
+// GetUniqueName returns the domain's uniquename.
+func (dc *DomainConfig) GetUniqueName() (uniquename string) {
+	return dc.Metadata[DOMAIN_UNIQUENAME]
+}
+
+// UpdateSplitHorizonNames updates the split horizon fields
+// (uniquename and tag) based on name.
+func (dc *DomainConfig) UpdateSplitHorizonNames() {
+	name, unique, tag := dc.GetSplitHorizonNames()
+
+	if unique == "" {
+		unique = name
+	}
+
+	if tag == "" {
+		l := strings.SplitN(name, "!", 2)
+		if len(l) == 2 {
+			name = l[0]
+			tag = l[1]
+		}
+	}
+
+	dc.Name = name
+	if dc.Metadata == nil {
+		dc.Metadata = map[string]string{}
+	}
+	dc.Metadata[DOMAIN_UNIQUENAME] = unique
+	dc.Metadata[DOMAIN_TAG] = tag
+}
+
 // Copy returns a deep copy of the DomainConfig.
 func (dc *DomainConfig) Copy() (*DomainConfig, error) {
 	newDc := &DomainConfig{}
diff --git a/models/provider.go b/models/provider.go
index 6adbb630..11c7453b 100644
--- a/models/provider.go
+++ b/models/provider.go
@@ -3,7 +3,7 @@ package models
 // DNSProvider is an interface for DNS Provider plug-ins.
 type DNSProvider interface {
 	GetNameservers(domain string) ([]*Nameserver, error)
-	GetZoneRecords(domain string) (Records, error)
+	GetZoneRecords(domain string, meta map[string]string) (Records, error)
 	GetZoneRecordsCorrections(dc *DomainConfig, existing Records) ([]*Correction, error)
 }
 
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 98b97a88..86baa1da 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -514,48 +514,26 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
 	return errs
 }
 
-// UpdateNameSplitHorizon fills in the split horizon fields.
-func UpdateNameSplitHorizon(dc *models.DomainConfig) {
-	if dc.UniqueName == "" {
-		dc.UniqueName = dc.Name
-	}
-	if dc.Tag == "" {
-		l := strings.SplitN(dc.Name, "!", 2)
-		if len(l) == 2 {
-			dc.Name = l[0]
-			dc.Tag = l[1]
-		}
-	}
-}
-
 // processSplitHorizonDomains finds "domain.tld!tag" domains and pre-processes them.
 func processSplitHorizonDomains(config *models.DNSConfig) error {
 	// Parse out names and tags.
 	for _, d := range config.Domains {
-		UpdateNameSplitHorizon(d)
+		d.UpdateSplitHorizonNames()
 	}
 
 	// Verify uniquenames are unique
 	seen := map[string]bool{}
 	for _, d := range config.Domains {
-		if seen[d.UniqueName] {
-			return fmt.Errorf("duplicate domain name: %q", d.UniqueName)
+		uniquename := d.GetUniqueName()
+		if seen[uniquename] {
+			return fmt.Errorf("duplicate domain name: %q", uniquename)
 		}
-		seen[d.UniqueName] = true
+		seen[uniquename] = true
 	}
 
 	return nil
 }
 
-//// parseDomainSpec parses "domain.tld!tag" into its component parts.
-//func parseDomainSpec(s string) (domain, tag string) {
-//	l := strings.SplitN(s, "!", 2)
-//	if len(l) == 2 {
-//		return l[0], l[1]
-//	}
-//	return l[0], ""
-//}
-
 func checkAutoDNSSEC(dc *models.DomainConfig) (errs []error) {
 	if strings.ToLower(dc.RegistrarName) == "none" {
 		return
diff --git a/pkg/zonerecs/zonerecords.go b/pkg/zonerecs/zonerecords.go
index 4a1cb46a..e56b9225 100644
--- a/pkg/zonerecs/zonerecords.go
+++ b/pkg/zonerecs/zonerecords.go
@@ -8,7 +8,8 @@ import (
 // post-processing, and then calls GetZoneRecordsCorrections.  The
 // name sucks because all the good names were taken.
 func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*models.Correction, error) {
-	existingRecords, err := driver.GetZoneRecords(dc.Name)
+
+	existingRecords, err := driver.GetZoneRecords(dc.Name, dc.Metadata)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
index 055f3826..b197e32e 100644
--- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
+++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
@@ -224,7 +224,7 @@ func (a *edgeDNSProvider) GetNameservers(domain string) ([]*models.Nameserver, e
 }
 
 // GetZoneRecords returns an array of RecordConfig structs for a zone.
-func (a *edgeDNSProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (a *edgeDNSProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/autodns/autoDnsProvider.go b/providers/autodns/autoDnsProvider.go
index 210e79e4..af21496d 100644
--- a/providers/autodns/autoDnsProvider.go
+++ b/providers/autodns/autoDnsProvider.go
@@ -254,7 +254,7 @@ func (api *autoDNSProvider) GetNameservers(domain string) ([]*models.Nameserver,
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *autoDNSProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *autoDNSProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	zone, _ := api.getZone(domain)
 	existingRecords := make([]*models.RecordConfig, len(zone.ResourceRecords))
 	for i, resourceRecord := range zone.ResourceRecords {
diff --git a/providers/axfrddns/axfrddnsProvider.go b/providers/axfrddns/axfrddnsProvider.go
index 6a75f42d..e3ab8ecb 100644
--- a/providers/axfrddns/axfrddnsProvider.go
+++ b/providers/axfrddns/axfrddnsProvider.go
@@ -271,7 +271,7 @@ func (c *axfrddnsProvider) FetchZoneRecords(domain string) ([]dns.RR, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *axfrddnsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *axfrddnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	rawRecords, err := c.FetchZoneRecords(domain)
 	if err != nil {
diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go
index cdddb9ad..ca37fa9a 100644
--- a/providers/azuredns/azureDnsProvider.go
+++ b/providers/azuredns/azureDnsProvider.go
@@ -161,7 +161,7 @@ func (a *azurednsProvider) ListZones() ([]string, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (a *azurednsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (a *azurednsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	existingRecords, _, _, err := a.getExistingRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index 08bc7e70..a651f4b0 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -153,7 +153,7 @@ func (c *bindProvider) ListZones() ([]string, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	if _, err := os.Stat(c.directory); os.IsNotExist(err) {
 		printer.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
@@ -162,6 +162,7 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
 	if c.zonefile == "" {
 		// This layering violation is needed for tests only.
 		// Otherwise, this is set already.
+		// Note: In this situation there is no "uniquename" or "tag".
 		c.zonefile = filepath.Join(c.directory,
 			makeFileName(c.filenameformat, domain, domain, ""))
 	}
@@ -297,7 +298,9 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
 		}
 
 		c.zonefile = filepath.Join(c.directory,
-			makeFileName(c.filenameformat, dc.Name, dc.Name, ""))
+			makeFileName(c.filenameformat,
+				dc.Metadata[models.DOMAIN_UNIQUENAME], dc.Name, dc.Metadata[models.DOMAIN_TAG]),
+		)
 
 		// We only change the serial number if there is a change.
 		if !c.skipNextSoaIncrease {
diff --git a/providers/bind/fnames.go b/providers/bind/fnames.go
index 108e5ca0..0803174a 100644
--- a/providers/bind/fnames.go
+++ b/providers/bind/fnames.go
@@ -11,6 +11,7 @@ import (
 
 // makeFileName uses format to generate a zone's filename.  See the
 func makeFileName(format, uniquename, domain, tag string) string {
+	//fmt.Printf("DEBUG: makeFileName(%q, %q, %q, %q)\n", format, uniquename, domain, tag)
 	if format == "" {
 		fmt.Fprintf(os.Stderr, "BUG: makeFileName called with null format\n")
 		return uniquename
@@ -55,6 +56,7 @@ func makeFileName(format, uniquename, domain, tag string) string {
 		}
 	}
 
+	//fmt.Printf("DEBUG: makeFileName returns= %q\n", b.String())
 	return b.String()
 }
 
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index 179c4e94..21d83957 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -109,7 +109,7 @@ func (c *cloudflareProvider) ListZones() ([]string, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *cloudflareProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	domainID, err := c.getDomainID(domain)
 	if err != nil {
diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go
index 5e5f8dc2..69bb04c2 100644
--- a/providers/cloudns/cloudnsProvider.go
+++ b/providers/cloudns/cloudnsProvider.go
@@ -226,7 +226,7 @@ func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *cloudnsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *cloudnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := c.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/cscglobal/dns.go b/providers/cscglobal/dns.go
index 6a18b141..f2a66e6f 100644
--- a/providers/cscglobal/dns.go
+++ b/providers/cscglobal/dns.go
@@ -9,7 +9,7 @@ import (
 )
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (client *providerClient) GetZoneRecords(domain string) (models.Records, error) {
+func (client *providerClient) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := client.getZoneRecordsAll(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/desec/desecProvider.go b/providers/desec/desecProvider.go
index 7a946c60..f22606e4 100644
--- a/providers/desec/desecProvider.go
+++ b/providers/desec/desecProvider.go
@@ -98,7 +98,7 @@ func (c *desecProvider) GetNameservers(domain string) ([]*models.Nameserver, err
 // }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *desecProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *desecProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := c.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go
index 17ced775..4ce70e66 100644
--- a/providers/digitalocean/digitaloceanProvider.go
+++ b/providers/digitalocean/digitaloceanProvider.go
@@ -115,7 +115,7 @@ func (api *digitaloceanProvider) GetNameservers(domain string) ([]*models.Namese
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *digitaloceanProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *digitaloceanProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := getRecords(api, domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index 4c9100b0..93fa5206 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -75,7 +75,7 @@ func (c *dnsimpleProvider) GetNameservers(_ string) ([]*models.Nameserver, error
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *dnsimpleProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := c.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/dnsmadeeasy/dnsMadeEasyProvider.go b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
index e191dbd9..27f3c137 100644
--- a/providers/dnsmadeeasy/dnsMadeEasyProvider.go
+++ b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
@@ -218,7 +218,7 @@ func (api *dnsMadeEasyProvider) GetNameservers(domain string) ([]*models.Nameser
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *dnsMadeEasyProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *dnsMadeEasyProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := api.fetchDomainRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/domainnameshop/dns.go b/providers/domainnameshop/dns.go
index 0a6d8575..a00978b0 100644
--- a/providers/domainnameshop/dns.go
+++ b/providers/domainnameshop/dns.go
@@ -10,7 +10,7 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
 )
 
-func (api *domainNameShopProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *domainNameShopProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := api.getDNS(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/exoscale/exoscaleProvider.go b/providers/exoscale/exoscaleProvider.go
index 658ab6d9..2e73823d 100644
--- a/providers/exoscale/exoscaleProvider.go
+++ b/providers/exoscale/exoscaleProvider.go
@@ -88,7 +88,7 @@ func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver,
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *exoscaleProvider) GetZoneRecords(domainName string) (models.Records, error) {
+func (c *exoscaleProvider) GetZoneRecords(domainName string, meta map[string]string) (models.Records, error) {
 	//dc.Punycode()
 
 	domain, err := c.findDomainByName(domainName)
diff --git a/providers/gandiv5/gandi_v5Provider.go b/providers/gandiv5/gandi_v5Provider.go
index b60d15b5..4b197e08 100644
--- a/providers/gandiv5/gandi_v5Provider.go
+++ b/providers/gandiv5/gandi_v5Provider.go
@@ -129,7 +129,7 @@ func newHelper(m map[string]string, metadata json.RawMessage) (*gandiv5Provider,
 
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
-func (client *gandiv5Provider) GetZoneRecords(domain string) (models.Records, error) {
+func (client *gandiv5Provider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	g := gandi.NewLiveDNSClient(config.Config{
 		APIKey:    client.apikey,
 		SharingID: client.sharingid,
diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go
index 4b524424..f880e92f 100644
--- a/providers/gcloud/gcloudProvider.go
+++ b/providers/gcloud/gcloudProvider.go
@@ -172,7 +172,7 @@ func keyForRec(r *models.RecordConfig) key {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (g *gcloudProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (g *gcloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	existingRecords, err := g.getZoneSets(domain)
 	return existingRecords, err
 }
diff --git a/providers/gcore/gcoreProvider.go b/providers/gcore/gcoreProvider.go
index 760059f1..ab69f853 100644
--- a/providers/gcore/gcoreProvider.go
+++ b/providers/gcore/gcoreProvider.go
@@ -77,7 +77,7 @@ func (c *gcoreProvider) GetNameservers(domain string) ([]*models.Nameserver, err
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *gcoreProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *gcoreProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	zone, err := c.provider.Zone(c.ctx, domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/hedns/hednsProvider.go b/providers/hedns/hednsProvider.go
index 021b04dc..8602376a 100644
--- a/providers/hedns/hednsProvider.go
+++ b/providers/hedns/hednsProvider.go
@@ -284,7 +284,7 @@ func (c *hednsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, zoneI
 }
 
 // GetZoneRecords returns all the records for the given domain
-func (c *hednsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *hednsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	var zoneRecords []*models.RecordConfig
 
 	// Get Domain ID
diff --git a/providers/hetzner/hetznerProvider.go b/providers/hetzner/hetznerProvider.go
index 20a4a585..34742056 100644
--- a/providers/hetzner/hetznerProvider.go
+++ b/providers/hetzner/hetznerProvider.go
@@ -154,7 +154,7 @@ func (api *hetznerProvider) GetNameservers(domain string) ([]*models.Nameserver,
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *hetznerProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *hetznerProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := api.getAllRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go
index f974e84c..f66f4f05 100644
--- a/providers/hexonet/records.go
+++ b/providers/hexonet/records.go
@@ -37,7 +37,7 @@ type HXRecord struct {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (n *HXClient) GetZoneRecords(domain string) (models.Records, error) {
+func (n *HXClient) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := n.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/hostingde/hostingdeProvider.go b/providers/hostingde/hostingdeProvider.go
index 8b68846e..af38a9d0 100644
--- a/providers/hostingde/hostingdeProvider.go
+++ b/providers/hostingde/hostingdeProvider.go
@@ -93,7 +93,7 @@ func (hp *hostingdeProvider) GetNameservers(domain string) ([]*models.Nameserver
 	return models.ToNameservers(hp.nameservers)
 }
 
-func (hp *hostingdeProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (hp *hostingdeProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	zone, err := hp.getZone(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go
index 401ee853..dc54267f 100644
--- a/providers/inwx/inwxProvider.go
+++ b/providers/inwx/inwxProvider.go
@@ -288,7 +288,7 @@ func (api *inwxAPI) GetNameservers(domain string) ([]*models.Nameserver, error)
 }
 
 // GetZoneRecords receives the current records from Inwx and converts them to models.RecordConfig.
-func (api *inwxAPI) GetZoneRecords(domain string) (models.Records, error) {
+func (api *inwxAPI) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	info, err := api.client.Nameservers.Info(&goinwx.NameserverInfoRequest{Domain: domain})
 	if err != nil {
 		return nil, err
diff --git a/providers/linode/linodeProvider.go b/providers/linode/linodeProvider.go
index ad5fd6ce..16305211 100644
--- a/providers/linode/linodeProvider.go
+++ b/providers/linode/linodeProvider.go
@@ -108,7 +108,7 @@ func (api *linodeProvider) GetNameservers(domain string) ([]*models.Nameserver,
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *linodeProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *linodeProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	if api.domainIndex == nil {
 		if err := api.fetchDomainList(); err != nil {
 			return nil, err
diff --git a/providers/loopia/loopiaProvider.go b/providers/loopia/loopiaProvider.go
index e3313841..55601648 100644
--- a/providers/loopia/loopiaProvider.go
+++ b/providers/loopia/loopiaProvider.go
@@ -158,7 +158,7 @@ func (c *APIClient) ListZones() ([]string, error) {
 
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
-func (c *APIClient) GetZoneRecords(domain string) (models.Records, error) {
+func (c *APIClient) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	// Two approaches. One: get all SubDomains, and get their respective records
 	// simultaneously, or first get subdomains then fill each subdomain with its
diff --git a/providers/luadns/luadnsProvider.go b/providers/luadns/luadnsProvider.go
index 46f91d8d..90da43d6 100644
--- a/providers/luadns/luadnsProvider.go
+++ b/providers/luadns/luadnsProvider.go
@@ -78,7 +78,7 @@ func (l *luadnsProvider) ListZones() ([]string, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (l *luadnsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (l *luadnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	domainID, err := l.getDomainID(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/msdns/msdnsProvider.go b/providers/msdns/msdnsProvider.go
index 33ffa3da..544ef053 100644
--- a/providers/msdns/msdnsProvider.go
+++ b/providers/msdns/msdnsProvider.go
@@ -71,7 +71,7 @@ func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSSe
 
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
-func (client *msdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (client *msdnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	// Get the existing DNS records in native format.
 	nativeExistingRecords, err := client.shell.GetDNSZoneRecords(client.dnsserver, domain)
diff --git a/providers/namecheap/namecheapProvider.go b/providers/namecheap/namecheapProvider.go
index 8bb6eddd..a7d74b32 100644
--- a/providers/namecheap/namecheapProvider.go
+++ b/providers/namecheap/namecheapProvider.go
@@ -114,7 +114,7 @@ func doWithRetry(f func() error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (n *namecheapProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (n *namecheapProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	sld, tld := splitDomain(domain)
 	var records *nc.DomainDNSGetHostsResult
 	var err error
diff --git a/providers/namedotcom/records.go b/providers/namedotcom/records.go
index e9446f9b..f70bc1d2 100644
--- a/providers/namedotcom/records.go
+++ b/providers/namedotcom/records.go
@@ -20,7 +20,7 @@ var defaultNameservers = []*models.Nameserver{
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (n *namedotcomProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (n *namedotcomProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := n.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/netcup/netcupProvider.go b/providers/netcup/netcupProvider.go
index c9085ece..c441894a 100644
--- a/providers/netcup/netcupProvider.go
+++ b/providers/netcup/netcupProvider.go
@@ -44,7 +44,7 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *netcupProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *netcupProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := api.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/netlify/netlifyProvider.go b/providers/netlify/netlifyProvider.go
index dc31418f..9522e809 100644
--- a/providers/netlify/netlifyProvider.go
+++ b/providers/netlify/netlifyProvider.go
@@ -86,7 +86,7 @@ func (n *netlifyProvider) getZone(domain string) (*dnsZone, error) {
 	return nil, fmt.Errorf("no zones found for this domain")
 }
 
-func (n *netlifyProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (n *netlifyProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	zone, err := n.getZone(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/ns1/ns1Provider.go b/providers/ns1/ns1Provider.go
index 9eda93de..b42b2ce0 100644
--- a/providers/ns1/ns1Provider.go
+++ b/providers/ns1/ns1Provider.go
@@ -89,7 +89,7 @@ func (n *nsone) GetNameservers(domain string) ([]*models.Nameserver, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (n *nsone) GetZoneRecords(domain string) (models.Records, error) {
+func (n *nsone) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	z, _, err := n.Zones.Get(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/oracle/oracleProvider.go b/providers/oracle/oracleProvider.go
index 63841044..b60077d4 100644
--- a/providers/oracle/oracleProvider.go
+++ b/providers/oracle/oracleProvider.go
@@ -148,7 +148,7 @@ func (o *oracleProvider) GetNameservers(domain string) ([]*models.Nameserver, er
 	return models.ToNameservers(nss)
 }
 
-func (o *oracleProvider) GetZoneRecords(zone string) (models.Records, error) {
+func (o *oracleProvider) GetZoneRecords(zone string, meta map[string]string) (models.Records, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
 
diff --git a/providers/ovh/ovhProvider.go b/providers/ovh/ovhProvider.go
index a2f75a1f..cf4db7ce 100644
--- a/providers/ovh/ovhProvider.go
+++ b/providers/ovh/ovhProvider.go
@@ -95,7 +95,7 @@ func (c *ovhProvider) ListZones() (zones []string, err error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *ovhProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *ovhProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	if !c.zones[domain] {
 		return nil, errNoExist{domain}
 	}
diff --git a/providers/packetframe/packetframeProvider.go b/providers/packetframe/packetframeProvider.go
index e32a6c87..e3cc7dfb 100644
--- a/providers/packetframe/packetframeProvider.go
+++ b/providers/packetframe/packetframeProvider.go
@@ -75,7 +75,7 @@ func (api *packetframeProvider) getZone(domain string) (*zoneInfo, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *packetframeProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *packetframeProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 
 	zone, err := api.getZone(domain)
 	if err != nil {
diff --git a/providers/porkbun/porkbunProvider.go b/providers/porkbun/porkbunProvider.go
index 7011edb5..6778229c 100644
--- a/providers/porkbun/porkbunProvider.go
+++ b/providers/porkbun/porkbunProvider.go
@@ -191,7 +191,7 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *porkbunProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (c *porkbunProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := c.getRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/powerdns/dns.go b/providers/powerdns/dns.go
index c3878a55..58014409 100644
--- a/providers/powerdns/dns.go
+++ b/providers/powerdns/dns.go
@@ -20,7 +20,7 @@ func (dsp *powerdnsProvider) GetNameservers(string) ([]*models.Nameserver, error
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (dsp *powerdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (dsp *powerdnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	zone, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/providers.go b/providers/providers.go
index d8e8a47c..ae1fb5f7 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -157,7 +157,7 @@ func (n None) GetNameservers(string) ([]*models.Nameserver, error) {
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (n None) GetZoneRecords(domain string) (models.Records, error) {
+func (n None) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	return nil, nil
 }
 
diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go
index 18f5ff2b..792e8d03 100644
--- a/providers/route53/route53Provider.go
+++ b/providers/route53/route53Provider.go
@@ -134,7 +134,6 @@ func (r *route53Provider) ListZones() ([]string, error) {
 }
 
 func (r *route53Provider) getZones() error {
-	// TODO(tlim) This should memoize itself.
 
 	if r.zonesByDomain != nil {
 		return nil
@@ -212,20 +211,34 @@ func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, e
 	return models.ToNameservers(nss)
 }
 
-func (r *route53Provider) GetZoneRecords(domain string) (models.Records, error) {
+func (r *route53Provider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	if err := r.getZones(); err != nil {
 		return nil, err
 	}
 
+	var zone r53Types.HostedZone
+
+	// If the zone_id is specified in meta, use it.
+	if zoneID, ok := meta["zone_id"]; ok {
+		zone = r.zonesByID[zoneID]
+		return r.getZoneRecords(zone)
+	}
+
+	//	fmt.Printf("DEBUG: ROUTE53 zones:\n")
+	//	for i, j := range r.zonesByDomain {
+	//		fmt.Printf("       %s: %v\n", i, aws.ToString(j.Id))
+	//	}
+
+	// Otherwise, use the domain name to look up the zone.
 	if zone, ok := r.zonesByDomain[domain]; ok {
 		return r.getZoneRecords(zone)
 	}
 
+	// Not found there?  Error.
 	return nil, errDomainNoExist{domain}
 }
 
 func (r *route53Provider) getZone(dc *models.DomainConfig) (r53Types.HostedZone, error) {
-	// TODO(tlim) This should memoize itself.
 
 	if err := r.getZones(); err != nil {
 		return r53Types.HostedZone{}, err
diff --git a/providers/rwth/dns.go b/providers/rwth/dns.go
index 82583956..dd3d6700 100644
--- a/providers/rwth/dns.go
+++ b/providers/rwth/dns.go
@@ -13,7 +13,7 @@ import (
 var RWTHDefaultNs = []string{"dns-1.dfn.de", "dns-2.dfn.de", "zs1.rz.rwth-aachen.de", "zs2.rz.rwth-aachen.de"}
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *rwthProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *rwthProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	records, err := api.getAllRecords(domain)
 	if err != nil {
 		return nil, err
diff --git a/providers/softlayer/softlayerProvider.go b/providers/softlayer/softlayerProvider.go
index adcbb5b6..cf4a4bdf 100644
--- a/providers/softlayer/softlayerProvider.go
+++ b/providers/softlayer/softlayerProvider.go
@@ -61,7 +61,7 @@ func (s *softlayerProvider) GetNameservers(domain string) ([]*models.Nameserver,
 
 // GetZoneRecords gets all the records for domainName and converts
 // them to model.RecordConfig.
-func (s *softlayerProvider) GetZoneRecords(domainName string) (models.Records, error) {
+func (s *softlayerProvider) GetZoneRecords(domainName string, meta map[string]string) (models.Records, error) {
 	domain, err := s.getDomain(&domainName)
 	if err != nil {
 		return nil, err
diff --git a/providers/transip/transipProvider.go b/providers/transip/transipProvider.go
index 9fd130ef..c058511a 100644
--- a/providers/transip/transipProvider.go
+++ b/providers/transip/transipProvider.go
@@ -255,7 +255,7 @@ func canUpdateDNSEntry(desired *models.RecordConfig, existing *models.RecordConf
 	return desired.Name == existing.Name && desired.TTL == existing.TTL && desired.Type == existing.Type
 }
 
-func (n *transipProvider) GetZoneRecords(domainName string) (models.Records, error) {
+func (n *transipProvider) GetZoneRecords(domainName string, meta map[string]string) (models.Records, error) {
 
 	entries, err := n.domains.GetDNSEntries(domainName)
 	if err != nil {
diff --git a/providers/vultr/vultrProvider.go b/providers/vultr/vultrProvider.go
index aebf4fec..bf74a84f 100644
--- a/providers/vultr/vultrProvider.go
+++ b/providers/vultr/vultrProvider.go
@@ -76,10 +76,10 @@ func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSSe
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *vultrProvider) GetZoneRecords(domain string) (models.Records, error) {
+func (api *vultrProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
 	listOptions := &govultr.ListOptions{}
-	records, meta, err := api.client.DomainRecord.List(context.Background(), domain, listOptions)
-	curRecords := make(models.Records, meta.Total)
+	records, recordsMeta, err := api.client.DomainRecord.List(context.Background(), domain, listOptions)
+	curRecords := make(models.Records, recordsMeta.Total)
 	nextI := 0
 
 	for {
@@ -97,11 +97,11 @@ func (api *vultrProvider) GetZoneRecords(domain string) (models.Records, error)
 		}
 		nextI = currentI + 1
 
-		if meta.Links.Next == "" {
+		if recordsMeta.Links.Next == "" {
 			break
 		} else {
-			listOptions.Cursor = meta.Links.Next
-			records, meta, err = api.client.DomainRecord.List(context.Background(), domain, listOptions)
+			listOptions.Cursor = recordsMeta.Links.Next
+			records, recordsMeta, err = api.client.DomainRecord.List(context.Background(), domain, listOptions)
 			continue
 		}
 	}


Test patch:
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index de6eadc2..9ead1608 100644
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -14,7 +14,6 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/credsfile"
 	"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
 	"github.com/StackExchange/dnscontrol/v3/pkg/nameservers"
-	"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
 	"github.com/StackExchange/dnscontrol/v3/pkg/zonerecs"
 	"github.com/StackExchange/dnscontrol/v3/providers"
 	_ "github.com/StackExchange/dnscontrol/v3/providers/_all"
@@ -112,7 +111,7 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
 	dc := &models.DomainConfig{
 		Name: domainName,
 	}
-	normalize.UpdateNameSplitHorizon(dc)
+	dc.UpdateSplitHorizonNames()
 
 	// fix up nameservers
 	ns, err := prv.GetNameservers(domainName)