ROUTE53: Support Route53's ALIAS record type (#239) (#301)

* Stable comparison of metadata (#239)

Iterating over a map in Go never produces twice the same ordering.
Thus when comparing two metadata map with more than one key, the
`differ` is always finding differences.

To properly compare records metadata, we need to iterate the maps
in a deterministic way.

Signed-off-by: Brice Figureau <brice@daysofwonder.com>

* Support for Route53 ALIAS record type (#239)

Route53 ALIAS doesn't behave like a regular ALIAS, and is much more
limited as its target can only be some specific AWS resources or
another record in the same zone.

According to #239, this change adds a new directive R53_ALIAS which
implements this specific alias. This record type can only be used
with the Route53 provider.

This directive usage looks like this:
```js
D("example.com", REGISTRAR, DnsProvider("ROUTE53"),
R53_ALIAS("foo1", "A", "bar") // record in same zone
R53_ALIAS("foo2", "A",
  "blahblah.elasticloadbalancing.us-west-1.amazonaws.com",
   R53_ZONE('Z368ELLRRE2KJ0')) // ELB in us-west-1

```

Unfortunately, Route53 requires indicating the hosted zone id
where the target is defined (those are listed in AWS documentation,
see the R53_ALIAS documentation for links).
This commit is contained in:
Brice Figureau 2018-01-16 11:53:12 +01:00 committed by Tom Limoncelli
parent 2fc55dfdc4
commit 7b8d608019
15 changed files with 455 additions and 104 deletions

View file

@ -35,6 +35,7 @@ func generateFeatureMatrix() error {
{"SRV", "Driver has explicitly implemented SRV record management"},
{"TLSA", "Provider can manage TLSA records"},
{"TXTMulti", "Provider can manage TXT records with multiple strings"},
{"R53_ALIAS", "Provider supports Route 53 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"},
@ -75,6 +76,7 @@ func generateFeatureMatrix() error {
setCap("SRV", providers.CanUseSRV)
setCap("TLSA", providers.CanUseTLSA)
setCap("TXTMulti", providers.CanUseTXTMulti)
setCap("R53_ALIAS", providers.CanUseRoute53Alias)
setDoc("dual host", providers.DocDualHost, false)
setDoc("create-domains", providers.DocCreateDomains, true)

View file

@ -0,0 +1,47 @@
---
name: R53_ALIAS
parameters:
- name
- target
- ZONE_ID modifier
---
R53_ALIAS is a Route53 specific virtual record type that points a record at either another record or an AWS entity (like a Cloudfront distribution, an ELB, etc...). It is analagous 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, R53_ALIAS is only supported on Route53. Attempting to use R53_ALIAS on another provider than Route53 will result in an error.
The name should be the relative label for the domain.
Target should be a string representing the target. If it is a single label we will assume it is a relative name on the current domain. If it contains *any* dots, it should be a fully qualified domain name, ending with a `.`.
The Target can be any of:
* _CloudFront distribution_: in this case specify the domain name that CloudFront assigned whenyou created your distribution (note that your CloudFront distribution must include an alternante domain name that matches the record you're adding)
* _Elastic Beanstalk environment_: specify the CNAME attribute for the environment. The environment must have a regionalized domain name. To get the CNAME, you can use either the [AWS Console](http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customdomains.html), [AWS Elastic Beanstalk API](http://docs.aws.amazon.com/elasticbeanstalk/latest/api/API_DescribeEnvironments.html), or the [AWS CLI](http://docs.aws.amazon.com/cli/latest/reference/elasticbeanstalk/describe-environments.html).
* _ELB load balancer_: specify the DNS name that is associated with the load balancer. To get the DNS name you can use either the AWS Console (on the EC2 page, choose Load Balancers, select the right one, choose the description tab), [ELB API](http://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html), the [AWS ELB CLI](http://docs.aws.amazon.com/cli/latest/reference/elb/describe-load-balancers.html), or the [AWS ELBv2 CLI](http://docs.aws.amazon.com/cli/latest/reference/elbv2/describe-load-balancers.html).
* _S3 bucket_ (configured as website): specify the domain name of the Amazon S3 website endpoint in which you configured the bucket (for instance s3-website-us-east-2.amazonaws.com). For the available values refer to the [Amazon S3 Website Endpoints](http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).
* _Another Route53 record_: specify the value of the name of another record in the same hosted zone.
For all the target type, excluding 'another record', you have to specify the `Zone ID` of the target. This is done by using the `R53_ZONE` record modifier.
The zone id can be found depending on the target type:
* _CloudFront distribution_: specify `Z2FDTNDATAQYW2`
* _Elastic Beanstalk environment_: specify the hosted zone ID for the region in which the environment has been created. Refer to the [List of regions and hosted Zone IDs](http://docs.aws.amazon.com/general/latest/gr/rande.html#elasticbeanstalk_region).
* _ELB load balancer_: specify the value of the hosted zone ID for the load balancer. You can find it in [the List of regions and hosted Zone IDs](http://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region)
* _S3 bucket_ (configured as website): specify the hosted zone ID for the region that you created the bucket in. You can find it in [the List of regions and hosted Zone IDs](http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region)
* _Another Route 53 record_: you can either specify the correct zone id or do not specify anything and dnscontrol will figure out the right zone id. (Note: Route53 alias can't reference a record in a different zone).
{% include startExample.html %}
{% highlight js %}
D("example.com", REGISTRAR, DnsProvider("ROUTE53"),
R53_ALIAS("foo", "A", "bar"), // record in same zone
R53_ALIAS("foo", "A", "bar", R53_ZONE('Z35SXDOTRQ7X7K')), // record in same zone, zone specified
R53_ALIAS("foo", "A", "blahblah.elasticloadbalancing.us-west-1.amazonaws.com", R53_ZONE('Z368ELLRRE2KJ0')), // a classic ELB in us-west-1
R53_ALIAS("foo", "A", "blahblah.elasticbeanstalk.us-west-2.amazonaws.com", R53_ZONE('Z38NKT9BP95V3O')), // an Elastic Beanstalk environment in us-west-2
R53_ALIAS("foo", "A", "blahblah-bucket.s3-website-us-west-1.amazonaws.com", R53_ZONE('Z2F56UZL2M1ACD')), // a website S3 Bucket in us-west-1
);
{%endhighlight%}
{% include endExample.html %}

View file

@ -0,0 +1,11 @@
---
name: R53_ZONE
parameters:
- zone_id
---
R53_ZONE sets the required Route53 hosted zone id in a R53_ALIAS record.
This directive has no impact when used in anything else than a R53_ALIAS.
Please refer to the R53_ALIAS directive for usage.

View file

@ -194,7 +194,7 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="R53 does not provide a generic ALIAS functionality. They do have &#39;ALIAS&#39; CNAME types to point at various AWS infrastructure, but dnscontrol has not implemented those.">
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
@ -376,6 +376,26 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
</tr>
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider supports Route 53 limited ALIAS">R53_ALIAS</th>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
</tr>
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="This provider is recommended for use in &#39;dual hosting&#39; scenarios. Usually this means the provider allows full control over the apex NS records">dual host</th>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="This driver does not manage NS records, so should not be used for dual-host scenarios">

View file

@ -23,4 +23,5 @@ func init() {
2. If you try to use ALIAS records, **all** dns providers for the domain must support ALIAS records. We do not want to serve inconsistent records across providers.
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.

View file

@ -107,8 +107,8 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
for _, r := range tst.Records {
rc := models.RecordConfig(*r)
rc.NameFQDN = dnsutil.AddOrigin(rc.Name, domainName)
if rc.Target == "**current-domain**" {
rc.Target = domainName + "."
if strings.Contains(rc.Target, "**current-domain**") {
rc.Target = strings.Replace(rc.Target, "**current-domain**", domainName, 1) + "."
}
dom.Records = append(dom.Records, &rc)
}
@ -216,6 +216,14 @@ func alias(name, target string) *rec {
return makeRec(name, target, "ALIAS")
}
func r53alias(name, aliasType, target string) *rec {
r := makeRec(name, target, "R53_ALIAS")
r.R53Alias = map[string]string{
"type": aliasType,
}
return r
}
func ns(name, target string) *rec {
return makeRec(name, target, "NS")
}
@ -506,5 +514,16 @@ func makeTests(t *testing.T) []*TestCase {
tc("Add a new record - ignoring foo", a("bar", "1.2.3.4"), ignore("foo")),
)
// R53_ALIAS
if !providers.ProviderHasCabability(*providerToRun, providers.CanUseRoute53Alias) {
t.Log("Skipping Route53 ALIAS Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("create dependent records", a("foo", "1.2.3.4"), a("quux", "2.3.4.5")),
tc("ALIAS to A record in same zone", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "foo.**current-domain**")),
tc("change it", a("foo", "1.2.3.4"), a("quux", "2.3.4.5"), r53alias("bar", "A", "quux.**current-domain**")),
)
}
return tests
}

View file

@ -107,6 +107,7 @@ type RecordConfig struct {
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
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"`
CombinedTarget bool `json:"-"`
@ -132,6 +133,8 @@ func (rc *RecordConfig) String() (content string) {
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
case "CAA":
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"])
default:
msg := fmt.Sprintf("rc.String rtype %v unimplemented", rc.Type)
panic(msg)
@ -386,7 +389,7 @@ func (dc *DomainConfig) Punycode() error {
return err
}
switch rec.Type { // #rtype_variations
case "ALIAS", "MX", "NS", "CNAME", "PTR", "SRV", "URL", "URL301", "FRAME":
case "ALIAS", "MX", "NS", "CNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS":
rec.Target, err = idna.ToASCII(rec.Target)
if err != nil {
return err

View file

@ -165,6 +165,38 @@ var AAAA = recordBuilder('AAAA');
// ALIAS(name,target, recordModifiers...)
var ALIAS = recordBuilder('ALIAS');
// R53_ALIAS(name, target, type, recordModifiers...)
var R53_ALIAS = recordBuilder('R53_ALIAS', {
args: [['name', _.isString], ['type', validateR53AliasType], ['target', _.isString]],
transform: function (record, args, modifiers) {
record.name = args.name;
record.target = args.target;
if (_.isObject(record.r53_alias)) {
record.r53_alias['type'] = args.type;
} else {
record.r53_alias = { 'type': args.type };
}
},
});
// R53_ZONE(zone_id)
function R53_ZONE(zone_id) {
return function (r) {
if (_.isObject(r.r53_alias)) {
r.r53_alias['zone_id'] = zone_id;
} else {
r.r53_alias = { 'zone_id': zone_id }
}
};
}
function validateR53AliasType(value) {
if (!_.isString(value)) {
return false;
}
return ['A', 'AAAA', 'CNAME', 'CAA', 'MX', 'TXT', 'PTR', 'SPF', 'SRV', 'NAPTR'].indexOf(value) != -1;
}
// CAA(name,tag,value, recordModifiers...)
var CAA = recordBuilder('CAA', {
// TODO(tlim): It should be an error if value is not 0 or 128.

View file

@ -0,0 +1,14 @@
D("foo.com", "none",
R53_ALIAS("mxtest", "MX", "foo.com."),
R53_ALIAS("atest", "A", "foo.com."),
R53_ALIAS("atest", "A", "foo.com.", R53_ZONE("Z2FTEDLFRTF")),
R53_ALIAS("aaaatest", "AAAA", "foo.com."),
R53_ALIAS("aaaatest", "AAAA", "foo.com.", R53_ZONE("ERERTFGFGF")),
R53_ALIAS("cnametest", "CNAME", "foo.com."),
R53_ALIAS("ptrtest", "PTR", "foo.com."),
R53_ALIAS("txttest", "TXT", "foo.com."),
R53_ALIAS("srvtest", "SRV", "foo.com."),
R53_ALIAS("spftest", "SPF", "foo.com."),
R53_ALIAS("caatest", "CAA", "foo.com."),
R53_ALIAS("naptrtest", "NAPTR", "foo.com.")
);

View file

@ -0,0 +1,111 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "none",
"dnsProviders": {},
"records": [
{
"type": "R53_ALIAS",
"name": "mxtest",
"target": "foo.com.",
"r53_alias": {
"type": "MX"
}
},
{
"type": "R53_ALIAS",
"name": "atest",
"target": "foo.com.",
"r53_alias": {
"type": "A"
}
},
{
"type": "R53_ALIAS",
"name": "atest",
"target": "foo.com.",
"r53_alias": {
"type": "A",
"zone_id": "Z2FTEDLFRTF"
}
},
{
"type": "R53_ALIAS",
"name": "aaaatest",
"target": "foo.com.",
"r53_alias": {
"type": "AAAA"
}
},
{
"type": "R53_ALIAS",
"name": "aaaatest",
"target": "foo.com.",
"r53_alias": {
"type": "AAAA",
"zone_id": "ERERTFGFGF"
}
},
{
"type": "R53_ALIAS",
"name": "cnametest",
"target": "foo.com.",
"r53_alias": {
"type": "CNAME"
}
},
{
"type": "R53_ALIAS",
"name": "ptrtest",
"target": "foo.com.",
"r53_alias": {
"type": "PTR"
}
},
{
"type": "R53_ALIAS",
"name": "txttest",
"target": "foo.com.",
"r53_alias": {
"type": "TXT"
}
},
{
"type": "R53_ALIAS",
"name": "srvtest",
"target": "foo.com.",
"r53_alias": {
"type": "SRV"
}
},
{
"type": "R53_ALIAS",
"name": "spftest",
"target": "foo.com.",
"r53_alias": {
"type": "SPF"
}
},
{
"type": "R53_ALIAS",
"name": "caatest",
"target": "foo.com.",
"r53_alias": {
"type": "CAA"
}
},
{
"type": "R53_ALIAS",
"name": "naptrtest",
"target": "foo.com.",
"r53_alias": {
"type": "NAPTR"
}
}
]
}
]
}

View file

@ -192,90 +192,93 @@ var _escData = map[string]*_escFile{
"/helpers.js": {
local: "pkg/js/helpers.js",
size: 17575,
size: 18525,
modtime: 0,
compressed: `
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
AP//rK/L5adEAAA=
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
BLLUrwJ1j3MjweL/BgAA//+VzTYAXUgAAA==
`,
},

View file

@ -40,6 +40,9 @@ const (
DocDualHost
// DocCreateDomains means provider can add domains with the `dnscontrol create-domains` command
DocCreateDomains
// CanUseRoute53Alias indicates the provider support the specific R53_ALIAS records that only the Route53 provider supports
CanUseRoute53Alias
)
var providerCapabilities = map[string]map[Capability]bool{}

View file

@ -44,7 +44,16 @@ type differ struct {
func (d *differ) content(r *models.RecordConfig) string {
content := fmt.Sprintf("%v ttl=%d", r.Content(), r.TTL)
for _, f := range d.extraValues {
for k, v := range f(r) {
// sort the extra values map keys to perform a deterministic
// comparison since Golang maps iteration order is not guaranteed
valueMap := f(r)
keys := make([]string, 0)
for k := range valueMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := valueMap[k]
content += fmt.Sprintf(" %s=%s", k, v)
}
}

View file

@ -128,6 +128,26 @@ func TestMetaChange(t *testing.T) {
checkLengths(t, existing, desired, 0, 0, 0, 1, getMeta)
}
func TestMetaOrdering(t *testing.T) {
existing := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
desired := []*models.RecordConfig{
myRecord("www MX 1 1.1.1.1"),
}
existing[0].Metadata["k"] = "aa"
existing[0].Metadata["x"] = "cc"
desired[0].Metadata["k"] = "aa"
desired[0].Metadata["x"] = "cc"
checkLengths(t, existing, desired, 1, 0, 0, 0)
getMeta := func(r *models.RecordConfig) map[string]string {
return map[string]string{
"k": r.Metadata["k"],
}
}
checkLengths(t, existing, desired, 1, 0, 0, 0, getMeta)
}
func checkLengths(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) {
return checkLengthsWithKeepUnknown(t, existing, desired, unCount, createCount, delCount, modCount, false, valFuncs...)
}

View file

@ -56,7 +56,7 @@ func newRoute53(m map[string]string, metadata json.RawMessage) (*route53Provider
}
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. They do have 'ALIAS' CNAME types to point at various AWS infrastructure, but dnscontrol has not implemented those."),
providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead."),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Can(),
@ -64,6 +64,7 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseRoute53Alias: providers.Can(),
}
func init() {
@ -155,29 +156,48 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
var existingRecords = []*models.RecordConfig{}
for _, set := range records {
for _, rec := range set.ResourceRecords {
if *set.Type == "SOA" {
continue
if set.AliasTarget == nil {
for _, rec := range set.ResourceRecords {
if *set.Type == "SOA" {
continue
}
r := &models.RecordConfig{
NameFQDN: unescape(set.Name),
Type: *set.Type,
Target: *rec.Value,
TTL: uint32(*set.TTL),
CombinedTarget: true,
}
existingRecords = append(existingRecords, r)
}
} else {
r := &models.RecordConfig{
NameFQDN: unescape(set.Name),
Type: *set.Type,
Target: *rec.Value,
TTL: uint32(*set.TTL),
Type: "R53_ALIAS",
Target: aws.StringValue(set.AliasTarget.DNSName),
CombinedTarget: true,
TTL: 300,
R53Alias: map[string]string{
"type": *set.Type,
"zone_id": *set.AliasTarget.HostedZoneId,
},
}
existingRecords = append(existingRecords, r)
}
}
for _, want := range dc.Records {
want.MergeToTarget()
// update zone_id to current zone.id if not specified by the user
if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" {
want.R53Alias["zone_id"] = getZoneID(zone, want)
}
}
// Normalize
models.PostProcessRecords(existingRecords)
// diff
differ := diff.New(dc)
differ := diff.New(dc, getAliasMap)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
namesToUpdate := map[key][]string{}
@ -219,7 +239,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
delDesc += strings.Join(namesToUpdate[k], "\n") + "\n"
// on delete just submit the original resource set we got from r53.
for _, r := range records {
if *r.Name == k.Name+"." && *r.Type == k.Type {
if *r.Name == k.Name+"." && (*r.Type == k.Type || k.Type == "R53_ALIAS") {
rrset = r
break
}
@ -230,18 +250,21 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
// on change or create, just build a new record set from our desired state
chg.Action = sPtr("UPSERT")
rrset = &r53.ResourceRecordSet{
Name: sPtr(k.Name),
Type: sPtr(k.Type),
ResourceRecords: []*r53.ResourceRecord{},
Name: sPtr(k.Name),
Type: sPtr(k.Type),
}
for _, r := range recs {
val := r.Target
rr := &r53.ResourceRecord{
Value: &val,
if r.Type != "R53_ALIAS" {
rr := &r53.ResourceRecord{
Value: &val,
}
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
i := int64(r.TTL)
rrset.TTL = &i // TODO: make sure that ttls are consistent within a set
} else {
rrset = aliasToRRSet(zone, r)
}
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
i := int64(r.TTL)
rrset.TTL = &i // TODO: make sure that ttls are consistent within a set
}
}
chg.ResourceRecordSet = rrset
@ -279,6 +302,39 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
}
func getAliasMap(r *models.RecordConfig) map[string]string {
if r.Type != "R53_ALIAS" {
return nil
}
return r.R53Alias
}
func aliasToRRSet(zone *r53.HostedZone, r *models.RecordConfig) *r53.ResourceRecordSet {
rrset := &r53.ResourceRecordSet{
Name: sPtr(r.NameFQDN),
Type: sPtr(r.R53Alias["type"]),
}
zoneID := getZoneID(zone, r)
targetHealth := false
rrset.AliasTarget = &r53.AliasTarget{
DNSName: &r.Target,
HostedZoneId: aws.String(zoneID),
EvaluateTargetHealth: &targetHealth,
}
return rrset
}
func getZoneID(zone *r53.HostedZone, r *models.RecordConfig) string {
zoneID := r.R53Alias["zone_id"]
if zoneID == "" {
zoneID = aws.StringValue(zone.Id)
}
if strings.HasPrefix(zoneID, "/hostedzone/") {
zoneID = strings.TrimPrefix(zoneID, "/hostedzone/")
}
return zoneID
}
func (r *route53Provider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
corrections := []*models.Correction{}
actualSet, err := r.getRegistrarNameservers(&dc.Name)