plugin/route53: add split zone support (#2160)

Automatically submitted.
This commit is contained in:
Can Yucel 2018-10-10 10:55:54 -07:00 committed by corbot[bot]
parent 49c776df4c
commit 8432f14207
5 changed files with 123 additions and 60 deletions

View file

@ -21,7 +21,9 @@ route53 [ZONE:HOSTED_ZONE_ID...] {
}
~~~
* **ZONE** the name of the domain to be accessed.
* **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping domains
(private vs. public hosted zone), CoreDNS does the lookup in the given order here. Therefore, for a
non-existing resource record, SOA response will be from the rightmost zone.
* **HOSTED_ZONE_ID** the ID of the hosted zone that contains the resource record sets to be accessed.
* **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** the AWS access key ID and secret access key
to be used when query AWS (optional). If they are not provided, then coredns tries to access
@ -81,3 +83,11 @@ Enable route53 with AWS credentials file:
}
}
~~~
Enable route53 with multiple hosted zones with the same domain:
~~~ txt
. {
route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.org.:Z93A52145678156
}
~~~

View file

@ -30,28 +30,39 @@ type Route53 struct {
upstream *upstream.Upstream
zMu sync.RWMutex
zones map[string]*zone
zones zones
}
type zone struct {
id string
z *file.Zone
id string
z *file.Zone
dns string
}
// New returns new *Route53.
func New(ctx context.Context, c route53iface.Route53API, keys map[string]string, up *upstream.Upstream) (*Route53, error) {
zones := make(map[string]*zone, len(keys))
type zones map[string][]*zone
// New reads from the keys map which uses domain names as its key and hosted
// zone id lists as its values, validates that each domain name/zone id pair does
// exist, and returns a new *Route53. In addition to this, upstream is passed
// for doing recursive queries against CNAMEs.
// Returns error if it cannot verify any given domain name/zone id pair.
func New(ctx context.Context, c route53iface.Route53API, keys map[string][]string, up *upstream.Upstream) (*Route53, error) {
zones := make(map[string][]*zone, len(keys))
zoneNames := make([]string, 0, len(keys))
for dns, id := range keys {
_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{
DNSName: aws.String(dns),
HostedZoneId: aws.String(id),
})
if err != nil {
return nil, err
for dns, hostedZoneIDs := range keys {
for _, hostedZoneID := range hostedZoneIDs {
_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{
DNSName: aws.String(dns),
HostedZoneId: aws.String(hostedZoneID),
})
if err != nil {
return nil, err
}
if _, ok := zones[dns]; !ok {
zoneNames = append(zoneNames, dns)
}
zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")})
}
zones[dns] = &zone{id: id, z: file.NewZone(dns, "")}
zoneNames = append(zoneNames, dns)
}
return &Route53{
client: c,
@ -101,9 +112,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
m.SetReply(r)
m.Authoritative, m.RecursionAvailable = true, true
var result file.Result
h.zMu.RLock()
m.Answer, m.Ns, m.Extra, result = z.z.Lookup(state, qname)
h.zMu.RUnlock()
for _, hostedZone := range z {
h.zMu.RLock()
m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(state, qname)
h.zMu.RUnlock()
if len(m.Answer) != 0 {
break
}
}
if len(m.Answer) == 0 && h.Fall.Through(qname) {
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
@ -146,36 +162,37 @@ func (h *Route53) updateZones(ctx context.Context) error {
errc := make(chan error)
defer close(errc)
for zName, z := range h.zones {
go func(zName string, z *zone) {
go func(zName string, z []*zone) {
var err error
defer func() {
errc <- err
}()
newZ := file.NewZone(zName, "")
newZ.Upstream = *h.upstream
in := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(z.id),
}
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
for _, rrs := range out.ResourceRecordSets {
if err := updateZoneFromRRS(rrs, newZ); err != nil {
// Maybe unsupported record type. Log and carry on.
log.Warningf("Failed to process resource record set: %v", err)
for i, hostedZone := range z {
newZ := file.NewZone(zName, "")
newZ.Upstream = *h.upstream
in := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZone.id),
}
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
for _, rrs := range out.ResourceRecordSets {
if err := updateZoneFromRRS(rrs, newZ); err != nil {
// Maybe unsupported record type. Log and carry on.
log.Warningf("Failed to process resource record set: %v", err)
}
}
}
return true
})
if err != nil {
err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, z.id, err)
return
return true
})
if err != nil {
err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err)
return
}
h.zMu.Lock()
(*z[i]).z = newZ
h.zMu.Unlock()
}
h.zMu.Lock()
z.z = newZ
h.zMu.Unlock()
}(zName, z)
}
// Collect errors (if any). This will also sync on all zones updates

View file

@ -31,19 +31,26 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
if aws.StringValue(in.HostedZoneId) == "0987654321" {
return errors.New("bad. zone is bad")
}
var rrs []*route53.ResourceRecordSet
rrsResponse := map[string][]*route53.ResourceRecordSet{}
for _, r := range []struct {
rType, name, value string
rType, name, value, hostedZoneID string
}{
{"A", "example.org.", "1.2.3.4"},
{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334"},
{"CNAME", "sample.example.org.", "example.org"},
{"PTR", "example.org.", "ptr.example.org."},
{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"},
{"NS", "com.", "ns-1536.awsdns-00.co.uk."},
{"A", "example.org.", "1.2.3.4", "1234567890"},
{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"},
{"CNAME", "sample.example.org.", "example.org", "1234567890"},
{"PTR", "example.org.", "ptr.example.org.", "1234567890"},
{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1234567890"},
{"NS", "com.", "ns-1536.awsdns-00.co.uk.", "1234567890"},
// Unsupported type should be ignored.
{"YOLO", "swag.", "foobar"},
{"YOLO", "swag.", "foobar", "1234567890"},
// hosted zone with the same name, but a different id
{"A", "other-example.org.", "3.5.7.9", "1357986420"},
{"SOA", "org.", "ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1357986420"},
} {
rrs, ok := rrsResponse[r.hostedZoneID]
if !ok {
rrs = make([]*route53.ResourceRecordSet, 0)
}
rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType),
Name: aws.String(r.name),
ResourceRecords: []*route53.ResourceRecord{
@ -53,9 +60,11 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
},
TTL: aws.Int64(300),
})
rrsResponse[r.hostedZoneID] = rrs
}
if ok := fn(&route53.ListResourceRecordSetsOutput{
ResourceRecordSets: rrs,
ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)],
}, true); !ok {
return errors.New("paging function return false")
}
@ -65,7 +74,7 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
func TestRoute53(t *testing.T) {
ctx := context.Background()
r, err := New(ctx, fakeRoute53{}, map[string]string{"bad.": "0987654321"}, &upstream.Upstream{})
r, err := New(ctx, fakeRoute53{}, map[string][]string{"bad.": []string{"0987654321"}}, &upstream.Upstream{})
if err != nil {
t.Fatalf("Failed to create Route53: %v", err)
}
@ -73,7 +82,7 @@ func TestRoute53(t *testing.T) {
t.Fatalf("Expected errors for zone bad.")
}
r, err = New(ctx, fakeRoute53{}, map[string]string{"org.": "1234567890", "gov.": "Z098765432"}, &upstream.Upstream{})
r, err = New(ctx, fakeRoute53{}, map[string][]string{"org.": []string{"1357986420", "1234567890"}, "gov": []string{"Z098765432"}}, &upstream.Upstream{})
if err != nil {
t.Fatalf("Failed to create Route53: %v", err)
}
@ -158,7 +167,7 @@ func TestRoute53(t *testing.T) {
qname: "example.org",
qtype: dns.TypeSOA,
expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"org. 300 IN SOA ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"},
wantAnswer: []string{"org. 300 IN SOA ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"},
},
// 6. Explicit SOA query for example.org.
{
@ -187,6 +196,13 @@ func TestRoute53(t *testing.T) {
expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"},
},
// 10. other-zone.example.org is stored in a different hosted zone. success
{
qname: "other-example.org",
qtype: dns.TypeA,
expectedCode: dns.RcodeSuccess,
wantAnswer: []string{"other-example.org. 300 IN A 3.5.7.9"},
},
}
for ti, tc := range tests {

View file

@ -35,7 +35,8 @@ func init() {
}
func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error {
keys := map[string]string{}
keyPairs := map[string]struct{}{}
keys := map[string][]string{}
// Route53 plugin attempts to find AWS credentials by using ChainCredentials.
// And the order of that provider chain is as follows:
@ -56,14 +57,16 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro
if len(parts) != 2 {
return c.Errf("invalid zone '%s'", args[i])
}
if parts[0] == "" || parts[1] == "" {
dns, hostedZoneID := parts[0], parts[1]
if dns == "" || hostedZoneID == "" {
return c.Errf("invalid zone '%s'", args[i])
}
zone := plugin.Host(parts[0]).Normalize()
if v, ok := keys[zone]; ok && v != parts[1] {
return c.Errf("conflict zone '%s' ('%s' vs. '%s')", zone, v, parts[1])
if _, ok := keyPairs[args[i]]; ok {
return c.Errf("conflict zone '%s'", args[i])
}
keys[zone] = parts[1]
keyPairs[args[i]] = struct{}{}
keys[dns] = append(keys[dns], hostedZoneID)
}
for c.NextBlock() {

View file

@ -53,6 +53,10 @@ func TestSetupRoute53(t *testing.T) {
aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY
upstream 1.2.3.4
}`)
if err := setup(c, f); err != nil {
t.Fatalf("Unexpected errors: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org:12345678 {
fallthrough
}`)
@ -91,4 +95,17 @@ func TestSetupRoute53(t *testing.T) {
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org:12345678 example.org:12345678 {
upstream 1.2.3.4
}`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org {
upstream 1.2.3.4
}`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
}