plugin/route53: add split zone support (#2160)
Automatically submitted.
This commit is contained in:
parent
49c776df4c
commit
8432f14207
5 changed files with 123 additions and 60 deletions
|
@ -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
|
||||
}
|
||||
~~~
|
||||
|
|
|
@ -30,29 +30,40 @@ type Route53 struct {
|
|||
upstream *upstream.Upstream
|
||||
|
||||
zMu sync.RWMutex
|
||||
zones map[string]*zone
|
||||
zones zones
|
||||
}
|
||||
|
||||
type zone struct {
|
||||
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 {
|
||||
for dns, hostedZoneIDs := range keys {
|
||||
for _, hostedZoneID := range hostedZoneIDs {
|
||||
_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{
|
||||
DNSName: aws.String(dns),
|
||||
HostedZoneId: aws.String(id),
|
||||
HostedZoneId: aws.String(hostedZoneID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zones[dns] = &zone{id: id, z: file.NewZone(dns, "")}
|
||||
if _, ok := zones[dns]; !ok {
|
||||
zoneNames = append(zoneNames, dns)
|
||||
}
|
||||
zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")})
|
||||
}
|
||||
}
|
||||
return &Route53{
|
||||
client: c,
|
||||
zoneNames: zoneNames,
|
||||
|
@ -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
|
||||
for _, hostedZone := range z {
|
||||
h.zMu.RLock()
|
||||
m.Answer, m.Ns, m.Extra, result = z.z.Lookup(state, qname)
|
||||
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,17 +162,17 @@ 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
|
||||
}()
|
||||
|
||||
for i, hostedZone := range z {
|
||||
newZ := file.NewZone(zName, "")
|
||||
newZ.Upstream = *h.upstream
|
||||
|
||||
in := &route53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: aws.String(z.id),
|
||||
HostedZoneId: aws.String(hostedZone.id),
|
||||
}
|
||||
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
|
||||
func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
|
||||
|
@ -169,13 +185,14 @@ func (h *Route53) updateZones(ctx context.Context) error {
|
|||
return true
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, z.id, err)
|
||||
err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.zMu.Lock()
|
||||
z.z = newZ
|
||||
(*z[i]).z = newZ
|
||||
h.zMu.Unlock()
|
||||
}
|
||||
|
||||
}(zName, z)
|
||||
}
|
||||
// Collect errors (if any). This will also sync on all zones updates
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue