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.
|
* **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
|
* **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
|
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
|
upstream *upstream.Upstream
|
||||||
|
|
||||||
zMu sync.RWMutex
|
zMu sync.RWMutex
|
||||||
zones map[string]*zone
|
zones zones
|
||||||
}
|
}
|
||||||
|
|
||||||
type zone struct {
|
type zone struct {
|
||||||
id string
|
id string
|
||||||
z *file.Zone
|
z *file.Zone
|
||||||
|
dns string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns new *Route53.
|
type zones map[string][]*zone
|
||||||
func New(ctx context.Context, c route53iface.Route53API, keys map[string]string, up *upstream.Upstream) (*Route53, error) {
|
|
||||||
zones := make(map[string]*zone, len(keys))
|
// 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))
|
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{
|
_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{
|
||||||
DNSName: aws.String(dns),
|
DNSName: aws.String(dns),
|
||||||
HostedZoneId: aws.String(id),
|
HostedZoneId: aws.String(hostedZoneID),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
zones[dns] = &zone{id: id, z: file.NewZone(dns, "")}
|
if _, ok := zones[dns]; !ok {
|
||||||
zoneNames = append(zoneNames, dns)
|
zoneNames = append(zoneNames, dns)
|
||||||
}
|
}
|
||||||
|
zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")})
|
||||||
|
}
|
||||||
|
}
|
||||||
return &Route53{
|
return &Route53{
|
||||||
client: c,
|
client: c,
|
||||||
zoneNames: zoneNames,
|
zoneNames: zoneNames,
|
||||||
|
@ -101,9 +112,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
m.Authoritative, m.RecursionAvailable = true, true
|
m.Authoritative, m.RecursionAvailable = true, true
|
||||||
var result file.Result
|
var result file.Result
|
||||||
|
for _, hostedZone := range z {
|
||||||
h.zMu.RLock()
|
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()
|
h.zMu.RUnlock()
|
||||||
|
if len(m.Answer) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(m.Answer) == 0 && h.Fall.Through(qname) {
|
if len(m.Answer) == 0 && h.Fall.Through(qname) {
|
||||||
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
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)
|
errc := make(chan error)
|
||||||
defer close(errc)
|
defer close(errc)
|
||||||
for zName, z := range h.zones {
|
for zName, z := range h.zones {
|
||||||
go func(zName string, z *zone) {
|
go func(zName string, z []*zone) {
|
||||||
var err error
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
errc <- err
|
errc <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
for i, hostedZone := range z {
|
||||||
newZ := file.NewZone(zName, "")
|
newZ := file.NewZone(zName, "")
|
||||||
newZ.Upstream = *h.upstream
|
newZ.Upstream = *h.upstream
|
||||||
|
|
||||||
in := &route53.ListResourceRecordSetsInput{
|
in := &route53.ListResourceRecordSetsInput{
|
||||||
HostedZoneId: aws.String(z.id),
|
HostedZoneId: aws.String(hostedZone.id),
|
||||||
}
|
}
|
||||||
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
|
err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in,
|
||||||
func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
|
func(out *route53.ListResourceRecordSetsOutput, last bool) bool {
|
||||||
|
@ -169,13 +185,14 @@ func (h *Route53) updateZones(ctx context.Context) error {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.zMu.Lock()
|
h.zMu.Lock()
|
||||||
z.z = newZ
|
(*z[i]).z = newZ
|
||||||
h.zMu.Unlock()
|
h.zMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
}(zName, z)
|
}(zName, z)
|
||||||
}
|
}
|
||||||
// Collect errors (if any). This will also sync on all zones updates
|
// 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" {
|
if aws.StringValue(in.HostedZoneId) == "0987654321" {
|
||||||
return errors.New("bad. zone is bad")
|
return errors.New("bad. zone is bad")
|
||||||
}
|
}
|
||||||
var rrs []*route53.ResourceRecordSet
|
rrsResponse := map[string][]*route53.ResourceRecordSet{}
|
||||||
for _, r := range []struct {
|
for _, r := range []struct {
|
||||||
rType, name, value string
|
rType, name, value, hostedZoneID string
|
||||||
}{
|
}{
|
||||||
{"A", "example.org.", "1.2.3.4"},
|
{"A", "example.org.", "1.2.3.4", "1234567890"},
|
||||||
{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334"},
|
{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"},
|
||||||
{"CNAME", "sample.example.org.", "example.org"},
|
{"CNAME", "sample.example.org.", "example.org", "1234567890"},
|
||||||
{"PTR", "example.org.", "ptr.example.org."},
|
{"PTR", "example.org.", "ptr.example.org.", "1234567890"},
|
||||||
{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"},
|
{"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."},
|
{"NS", "com.", "ns-1536.awsdns-00.co.uk.", "1234567890"},
|
||||||
// Unsupported type should be ignored.
|
// 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),
|
rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType),
|
||||||
Name: aws.String(r.name),
|
Name: aws.String(r.name),
|
||||||
ResourceRecords: []*route53.ResourceRecord{
|
ResourceRecords: []*route53.ResourceRecord{
|
||||||
|
@ -53,9 +60,11 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
|
||||||
},
|
},
|
||||||
TTL: aws.Int64(300),
|
TTL: aws.Int64(300),
|
||||||
})
|
})
|
||||||
|
rrsResponse[r.hostedZoneID] = rrs
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok := fn(&route53.ListResourceRecordSetsOutput{
|
if ok := fn(&route53.ListResourceRecordSetsOutput{
|
||||||
ResourceRecordSets: rrs,
|
ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)],
|
||||||
}, true); !ok {
|
}, true); !ok {
|
||||||
return errors.New("paging function return false")
|
return errors.New("paging function return false")
|
||||||
}
|
}
|
||||||
|
@ -65,7 +74,7 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou
|
||||||
func TestRoute53(t *testing.T) {
|
func TestRoute53(t *testing.T) {
|
||||||
ctx := context.Background()
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create Route53: %v", err)
|
t.Fatalf("Failed to create Route53: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +82,7 @@ func TestRoute53(t *testing.T) {
|
||||||
t.Fatalf("Expected errors for zone bad.")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create Route53: %v", err)
|
t.Fatalf("Failed to create Route53: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -158,7 +167,7 @@ func TestRoute53(t *testing.T) {
|
||||||
qname: "example.org",
|
qname: "example.org",
|
||||||
qtype: dns.TypeSOA,
|
qtype: dns.TypeSOA,
|
||||||
expectedCode: dns.RcodeSuccess,
|
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.
|
// 6. Explicit SOA query for example.org.
|
||||||
{
|
{
|
||||||
|
@ -187,6 +196,13 @@ func TestRoute53(t *testing.T) {
|
||||||
expectedCode: dns.RcodeSuccess,
|
expectedCode: dns.RcodeSuccess,
|
||||||
wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"},
|
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 {
|
for ti, tc := range tests {
|
||||||
|
|
|
@ -35,7 +35,8 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error {
|
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.
|
// Route53 plugin attempts to find AWS credentials by using ChainCredentials.
|
||||||
// And the order of that provider chain is as follows:
|
// 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 {
|
if len(parts) != 2 {
|
||||||
return c.Errf("invalid zone '%s'", args[i])
|
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])
|
return c.Errf("invalid zone '%s'", args[i])
|
||||||
}
|
}
|
||||||
zone := plugin.Host(parts[0]).Normalize()
|
if _, ok := keyPairs[args[i]]; ok {
|
||||||
if v, ok := keys[zone]; ok && v != parts[1] {
|
return c.Errf("conflict zone '%s'", args[i])
|
||||||
return c.Errf("conflict zone '%s' ('%s' vs. '%s')", zone, v, parts[1])
|
|
||||||
}
|
}
|
||||||
keys[zone] = parts[1]
|
|
||||||
|
keyPairs[args[i]] = struct{}{}
|
||||||
|
keys[dns] = append(keys[dns], hostedZoneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
for c.NextBlock() {
|
for c.NextBlock() {
|
||||||
|
|
|
@ -53,6 +53,10 @@ func TestSetupRoute53(t *testing.T) {
|
||||||
aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY
|
aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY
|
||||||
upstream 1.2.3.4
|
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 {
|
c = caddy.NewTestController("dns", `route53 example.org:12345678 {
|
||||||
fallthrough
|
fallthrough
|
||||||
}`)
|
}`)
|
||||||
|
@ -91,4 +95,17 @@ func TestSetupRoute53(t *testing.T) {
|
||||||
if err := setup(c, f); err == nil {
|
if err := setup(c, f); err == nil {
|
||||||
t.Fatalf("Expected errors, but got: %v", err)
|
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