Add route53 plugin (#1390)

* Update vendor

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Add route53 plugin

This fix adds route53 plugin so that it is possible to
query route53 record through CoreDNS.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
Yong Tang
2018-01-15 09:59:29 -08:00
committed by GitHub
parent d699b89063
commit 584dd87c70
352 changed files with 81636 additions and 1798 deletions

46
plugin/route53/README.md Normal file
View File

@@ -0,0 +1,46 @@
# route53
## Name
*route53* - enables serving zone data from AWS route53.
## Description
The hosts plugin is useful for serving zones from resource record sets in AWS route53.
This plugin only supports A and AAAA records. The route53 plugin can be used when
coredns is deployed on AWS.
## Syntax
~~~ txt
route53 [ZONE:HOSTED_ZONE_ID...] {
[aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY]
}
~~~
* **ZONE** the name of the domain 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
to be used when query AWS (optional). If they are not provided, then coredns tries to access
AWS credentials the same way as AWS CLI, e.g., environmental variables, AWS credentials file,
instance profile credentials, etc.
## Examples
Enable route53, with implicit aws credentials:
~~~ txt
. {
route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7
}
~~~
Enable route53, with explicit aws credentials:
~~~ txt
. {
route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 {
aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
}
}
~~~

97
plugin/route53/route53.go Normal file
View File

@@ -0,0 +1,97 @@
// Package route53 implements a plugin that returns resource records
// from AWS route53
package route53
import (
"net"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/route53/route53iface"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// Route53 is a plugin that returns RR from AWS route53
type Route53 struct {
Next plugin.Handler
zones []string
keys map[string]string
client route53iface.Route53API
}
// ServeDNS implements the plugin.Handler interface.
func (rr Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
qname := state.Name()
zone := plugin.Zones(rr.zones).Matches(qname)
if zone == "" {
return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, w, r)
}
output, err := rr.client.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(rr.keys[zone]),
StartRecordName: aws.String(qname),
StartRecordType: aws.String(state.Type()),
MaxItems: aws.String("1"),
})
if err != nil {
return dns.RcodeServerFailure, err
}
answers := []dns.RR{}
switch state.QType() {
case dns.TypeA:
answers = a(qname, output.ResourceRecordSets)
case dns.TypeAAAA:
answers = aaaa(qname, output.ResourceRecordSets)
}
if len(answers) == 0 {
return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, w, r)
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Answer = answers
state.SizeAndDo(m)
m, _ = state.Scrub(m)
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
func a(zone string, rrss []*route53.ResourceRecordSet) []dns.RR {
answers := []dns.RR{}
for _, rrs := range rrss {
for _, rr := range rrs.ResourceRecords {
r := new(dns.A)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: uint32(aws.Int64Value(rrs.TTL))}
r.A = net.ParseIP(aws.StringValue(rr.Value)).To4()
answers = append(answers, r)
}
}
return answers
}
func aaaa(zone string, rrss []*route53.ResourceRecordSet) []dns.RR {
answers := []dns.RR{}
for _, rrs := range rrss {
for _, rr := range rrs.ResourceRecords {
r := new(dns.AAAA)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: uint32(aws.Int64Value(rrs.TTL))}
r.AAAA = net.ParseIP(aws.StringValue(rr.Value)).To16()
answers = append(answers, r)
}
}
return answers
}
// Name implements the Handler interface.
func (rr Route53) Name() string { return "route53" }

View File

@@ -0,0 +1,81 @@
package route53
import (
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/route53/route53iface"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
type mockedRoute53 struct {
route53iface.Route53API
}
func (mockedRoute53) ListResourceRecordSets(input *route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error) {
return &route53.ListResourceRecordSetsOutput{
ResourceRecordSets: []*route53.ResourceRecordSet{
{
ResourceRecords: []*route53.ResourceRecord{
{
Value: aws.String("10.2.3.4"),
},
},
},
},
}, nil
}
func TestRoute53(t *testing.T) {
r := Route53{
zones: []string{"example.org."},
keys: map[string]string{"example.org.": "1234567890"},
client: mockedRoute53{},
}
tests := []struct {
qname string
qtype uint16
expectedCode int
expectedReply []string // ownernames for the records in the additional section.
expectedErr error
}{
{
qname: "example.org",
qtype: dns.TypeA,
expectedCode: dns.RcodeSuccess,
expectedReply: []string{"example.org."},
expectedErr: nil,
},
}
ctx := context.TODO()
for i, tc := range tests {
req := new(dns.Msg)
req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
rec := dnstest.NewRecorder(&test.ResponseWriter{})
code, err := r.ServeDNS(ctx, rec, req)
if err != tc.expectedErr {
t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err)
}
if code != int(tc.expectedCode) {
t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code)
}
if len(tc.expectedReply) != 0 {
for i, expected := range tc.expectedReply {
actual := rec.Msg.Answer[i].Header().Name
if actual != expected {
t.Errorf("Test %d: Expected answer %s, but got %s", i, expected, actual)
}
}
}
}
}

89
plugin/route53/setup.go Normal file
View File

@@ -0,0 +1,89 @@
package route53
import (
"strings"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/route53/route53iface"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("route53", caddy.Plugin{
ServerType: "dns",
Action: func(c *caddy.Controller) error {
f := func(credential *credentials.Credentials) route53iface.Route53API {
return route53.New(session.Must(session.NewSession(&aws.Config{
Credentials: credential,
})))
}
return setup(c, f)
},
})
}
func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error {
keys := map[string]string{}
var credential *credentials.Credentials
for c.Next() {
args := c.RemainingArgs()
for i := 0; i < len(args); i++ {
parts := strings.SplitN(args[i], ":", 2)
if len(parts) != 2 {
return c.Errf("invalid zone '%s'", args[i])
}
if parts[0] == "" || parts[1] == "" {
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])
}
keys[zone] = parts[1]
}
for c.NextBlock() {
switch c.Val() {
case "aws_access_key":
v := c.RemainingArgs()
if len(v) < 2 {
return c.Errf("invalid access key '%v'", v)
}
credential = credentials.NewStaticCredentials(v[0], v[1], "")
default:
return c.Errf("unknown property '%s'", c.Val())
}
}
}
client := f(credential)
zones := []string{}
for zone, v := range keys {
// Make sure enough credentials is needed
if _, err := client.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(v),
MaxItems: aws.String("1"),
}); err != nil {
return c.Errf("aws error: '%s'", err)
}
zones = append(zones, zone)
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return Route53{
Next: next,
keys: keys,
zones: zones,
client: client,
}
})
return nil
}

View File

@@ -0,0 +1,37 @@
package route53
import (
"testing"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/service/route53/route53iface"
"github.com/mholt/caddy"
)
func TestSetupRoute53(t *testing.T) {
f := func(credential *credentials.Credentials) route53iface.Route53API {
return mockedRoute53{}
}
c := caddy.NewTestController("dns", `route53`)
if err := setup(c, f); err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 :`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org:12345678`)
if err := setup(c, f); err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `route53 example.org:12345678 {
aws_access_key
}`)
if err := setup(c, f); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
}