Add Google Cloud DNS plugin (#3011)
Signed-off-by: Palash Nigam <npalash25@gmail.com> Closes: #2822
This commit is contained in:
parent
bde393096f
commit
194b0f95b4
13 changed files with 825 additions and 0 deletions
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}",
|
||||
"env": {},
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -37,6 +37,7 @@ var Directives = []string{
|
|||
"hosts",
|
||||
"route53",
|
||||
"azure",
|
||||
"clouddns",
|
||||
"federation",
|
||||
"k8s_external",
|
||||
"kubernetes",
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
_ "github.com/coredns/coredns/plugin/cache"
|
||||
_ "github.com/coredns/coredns/plugin/cancel"
|
||||
_ "github.com/coredns/coredns/plugin/chaos"
|
||||
_ "github.com/coredns/coredns/plugin/clouddns"
|
||||
_ "github.com/coredns/coredns/plugin/debug"
|
||||
_ "github.com/coredns/coredns/plugin/dnssec"
|
||||
_ "github.com/coredns/coredns/plugin/dnstap"
|
||||
|
|
1
go.mod
1
go.mod
|
@ -58,6 +58,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
|
||||
google.golang.org/api v0.7.0
|
||||
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df // indirect
|
||||
google.golang.org/grpc v1.22.0
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.16.1
|
||||
|
|
1
go.sum
1
go.sum
|
@ -145,6 +145,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
|||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
|
|
|
@ -45,7 +45,11 @@ autopath:autopath
|
|||
template:template
|
||||
hosts:hosts
|
||||
route53:route53
|
||||
<<<<<<< 6a6e9a9b33731656b51655072951649d9e716613
|
||||
azure:azure
|
||||
=======
|
||||
clouddns:clouddns
|
||||
>>>>>>> Add Google Cloud DNS plugin
|
||||
federation:federation
|
||||
k8s_external:k8s_external
|
||||
kubernetes:kubernetes
|
||||
|
|
67
plugin/clouddns/README.md
Normal file
67
plugin/clouddns/README.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
# clouddns
|
||||
|
||||
## Name
|
||||
|
||||
*clouddns* - enables serving zone data from GCP clouddns.
|
||||
|
||||
## Description
|
||||
|
||||
The clouddns plugin is useful for serving zones from resource record
|
||||
sets in GCP clouddns. This plugin supports all [Google Cloud DNS records](https://cloud.google.com/dns/docs/overview#supported_dns_record_types).
|
||||
The clouddns plugin can be used when coredns is deployed on GCP or elsewhere.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
clouddns [ZONE:PROJECT_NAME:HOSTED_ZONE_NAME...] {
|
||||
credentials [FILENAME]
|
||||
fallthrough [ZONES...]
|
||||
}
|
||||
~~~
|
||||
|
||||
* **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_NAME** the name of the hosted zone that contains the resource record sets to be
|
||||
accessed.
|
||||
|
||||
* `credentials` is used for reading the credential file.
|
||||
|
||||
* **FILENAME** GCP credentials file path.
|
||||
|
||||
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
|
||||
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin is
|
||||
authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then
|
||||
only queries for those zones will be subject to fallthrough.
|
||||
|
||||
* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
|
||||
|
||||
## Examples
|
||||
|
||||
Enable clouddns with implicit GCP credentials and resolve CNAMEs via 10.0.0.1:
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
clouddns example.org.:gcp-example-project:example-zone
|
||||
forward . 10.0.0.1
|
||||
}
|
||||
~~~
|
||||
|
||||
Enable clouddns with fallthrough:
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
clouddns example.org.:gcp-example-project:example-zone clouddns example.com.:gcp-example-project:example-zone-2 {
|
||||
fallthrough example.gov.
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Enable clouddns with multiple hosted zones with the same domain:
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
clouddns example.org.:gcp-example-project:example-zone example.com.:gcp-example-project:other-example-zone
|
||||
}
|
||||
~~~
|
222
plugin/clouddns/clouddns.go
Normal file
222
plugin/clouddns/clouddns.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
// Package clouddns implements a plugin that returns resource records
|
||||
// from GCP Cloud DNS.
|
||||
package clouddns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/file"
|
||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
gcp "google.golang.org/api/dns/v1"
|
||||
)
|
||||
|
||||
// CloudDNS is a plugin that returns RR from GCP Cloud DNS.
|
||||
type CloudDNS struct {
|
||||
Next plugin.Handler
|
||||
Fall fall.F
|
||||
|
||||
zoneNames []string
|
||||
client gcpDNS
|
||||
upstream *upstream.Upstream
|
||||
|
||||
zMu sync.RWMutex
|
||||
zones zones
|
||||
}
|
||||
|
||||
type zone struct {
|
||||
projectName string
|
||||
zoneName string
|
||||
z *file.Zone
|
||||
dns string
|
||||
}
|
||||
|
||||
type zones map[string][]*zone
|
||||
|
||||
// New reads from the keys map which uses domain names as its key and a colon separated
|
||||
// string of project name and hosted zone name lists as its values, validates
|
||||
// that each domain name/zone id pair does exist, and returns a new *CloudDNS.
|
||||
// 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 gcpDNS, keys map[string][]string, up *upstream.Upstream) (*CloudDNS, error) {
|
||||
zones := make(map[string][]*zone, len(keys))
|
||||
zoneNames := make([]string, 0, len(keys))
|
||||
for dnsName, hostedZoneDetails := range keys {
|
||||
for _, hostedZone := range hostedZoneDetails {
|
||||
ss := strings.SplitN(hostedZone, ":", 2)
|
||||
if len(ss) != 2 {
|
||||
return nil, errors.New("either project or zone name missing")
|
||||
}
|
||||
err := c.zoneExists(ss[0], ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fqdnDNSName := dns.Fqdn(dnsName)
|
||||
if _, ok := zones[fqdnDNSName]; !ok {
|
||||
zoneNames = append(zoneNames, fqdnDNSName)
|
||||
}
|
||||
zones[fqdnDNSName] = append(zones[fqdnDNSName], &zone{projectName: ss[0], zoneName: ss[1], dns: fqdnDNSName, z: file.NewZone(fqdnDNSName, "")})
|
||||
}
|
||||
}
|
||||
return &CloudDNS{
|
||||
client: c,
|
||||
zoneNames: zoneNames,
|
||||
zones: zones,
|
||||
upstream: up,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run executes first update, spins up an update forever-loop.
|
||||
// Returns error if first update fails.
|
||||
func (h *CloudDNS) Run(ctx context.Context) error {
|
||||
if err := h.updateZones(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Infof("Breaking out of CloudDNS update loop: %v", ctx.Err())
|
||||
return
|
||||
case <-time.After(1 * time.Minute):
|
||||
if err := h.updateZones(ctx); err != nil && ctx.Err() == nil /* Don't log error if ctx expired. */ {
|
||||
log.Errorf("Failed to update zones: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (h *CloudDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
qname := state.Name()
|
||||
|
||||
zName := plugin.Zones(h.zoneNames).Matches(qname)
|
||||
if zName == "" {
|
||||
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
z, ok := h.zones[zName] // ok true if we are authoritive for the zone
|
||||
if !ok || z == nil {
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative = true
|
||||
var result file.Result
|
||||
|
||||
for _, hostedZone := range z {
|
||||
h.zMu.RLock()
|
||||
m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname)
|
||||
h.zMu.RUnlock()
|
||||
|
||||
// Take the answer if it's non-empty OR if there is another
|
||||
// record type exists for this name (NODATA).
|
||||
if len(m.Answer) != 0 || result == file.NoData {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) {
|
||||
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case file.Success:
|
||||
case file.NoData:
|
||||
case file.NameError:
|
||||
m.Rcode = dns.RcodeNameError
|
||||
case file.Delegation:
|
||||
m.Authoritative = false
|
||||
case file.ServerFailure:
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
func updateZoneFromRRS(rrs *gcp.ResourceRecordSetsListResponse, z *file.Zone) error {
|
||||
for _, rr := range rrs.Rrsets {
|
||||
var rfc1035 string
|
||||
var r dns.RR
|
||||
var err error
|
||||
for _, value := range rr.Rrdatas {
|
||||
if rr.Type == "CNAME" || rr.Type == "PTR" {
|
||||
value = dns.Fqdn(value)
|
||||
}
|
||||
|
||||
// Assemble RFC 1035 conforming record to pass into dns scanner.
|
||||
rfc1035 = fmt.Sprintf("%s %d IN %s %s", dns.Fqdn(rr.Name), rr.Ttl, rr.Type, value)
|
||||
r, err = dns.NewRR(rfc1035)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse resource record: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
z.Insert(r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateZones re-queries resource record sets for each zone and updates the
|
||||
// zone object.
|
||||
// Returns error if any zones error'ed out, but waits for other zones to
|
||||
// complete first.
|
||||
func (h *CloudDNS) updateZones(ctx context.Context) error {
|
||||
errc := make(chan error)
|
||||
defer close(errc)
|
||||
for zName, z := range h.zones {
|
||||
go func(zName string, z []*zone) {
|
||||
var err error
|
||||
var rrListResponse *gcp.ResourceRecordSetsListResponse
|
||||
defer func() {
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
for i, hostedZone := range z {
|
||||
newZ := file.NewZone(zName, "")
|
||||
newZ.Upstream = h.upstream
|
||||
rrListResponse, err = h.client.listRRSets(hostedZone.projectName, hostedZone.zoneName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list resource records for %v:%v:%v from gcp: %v", zName, hostedZone.projectName, hostedZone.zoneName, err)
|
||||
return
|
||||
}
|
||||
updateZoneFromRRS(rrListResponse, newZ)
|
||||
|
||||
h.zMu.Lock()
|
||||
(*z[i]).z = newZ
|
||||
h.zMu.Unlock()
|
||||
}
|
||||
|
||||
}(zName, z)
|
||||
}
|
||||
// Collect errors (if any). This will also sync on all zones updates
|
||||
// completion.
|
||||
var errs []string
|
||||
for i := 0; i < len(h.zones); i++ {
|
||||
err := <-errc
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("errors updating zones: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (h *CloudDNS) Name() string { return "clouddns" }
|
316
plugin/clouddns/clouddns_test.go
Normal file
316
plugin/clouddns/clouddns_test.go
Normal file
|
@ -0,0 +1,316 @@
|
|||
package clouddns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
crequest "github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
gcp "google.golang.org/api/dns/v1"
|
||||
)
|
||||
|
||||
type fakeGCPClient struct {
|
||||
*gcp.Service
|
||||
}
|
||||
|
||||
func (c fakeGCPClient) zoneExists(projectName, hostedZoneName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c fakeGCPClient) listRRSets(projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error) {
|
||||
if projectName == "bad-project" || hostedZoneName == "bad-zone" {
|
||||
return nil, errors.New("the 'parameters.managedZone' resource named 'bad-zone' does not exist")
|
||||
}
|
||||
|
||||
var rr []*gcp.ResourceRecordSet
|
||||
|
||||
if hostedZoneName == "sample-zone-1" {
|
||||
rr = []*gcp.ResourceRecordSet{
|
||||
{
|
||||
Name: "example.org.",
|
||||
Ttl: 300,
|
||||
Type: "A",
|
||||
Rrdatas: []string{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Name: "www.example.org",
|
||||
Ttl: 300,
|
||||
Type: "A",
|
||||
Rrdatas: []string{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Name: "*.www.example.org",
|
||||
Ttl: 300,
|
||||
Type: "CNAME",
|
||||
Rrdatas: []string{"www.example.org"},
|
||||
},
|
||||
{
|
||||
Name: "example.org.",
|
||||
Ttl: 300,
|
||||
Type: "AAAA",
|
||||
Rrdatas: []string{"2001:db8:85a3::8a2e:370:7334"},
|
||||
},
|
||||
{
|
||||
Name: "sample.example.org",
|
||||
Ttl: 300,
|
||||
Type: "CNAME",
|
||||
Rrdatas: []string{"example.org"},
|
||||
},
|
||||
{
|
||||
Name: "example.org.",
|
||||
Ttl: 300,
|
||||
Type: "PTR",
|
||||
Rrdatas: []string{"ptr.example.org."},
|
||||
},
|
||||
{
|
||||
Name: "org.",
|
||||
Ttl: 300,
|
||||
Type: "SOA",
|
||||
Rrdatas: []string{"ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
{
|
||||
Name: "com.",
|
||||
Ttl: 300,
|
||||
Type: "NS",
|
||||
Rrdatas: []string{"ns-cloud-c4.googledomains.com."},
|
||||
},
|
||||
{
|
||||
Name: "split-example.gov.",
|
||||
Ttl: 300,
|
||||
Type: "A",
|
||||
Rrdatas: []string{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Name: "swag.",
|
||||
Ttl: 300,
|
||||
Type: "YOLO",
|
||||
Rrdatas: []string{"foobar"},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
rr = []*gcp.ResourceRecordSet{
|
||||
{
|
||||
Name: "split-example.org.",
|
||||
Ttl: 300,
|
||||
Type: "A",
|
||||
Rrdatas: []string{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
Name: "other-example.org.",
|
||||
Ttl: 300,
|
||||
Type: "A",
|
||||
Rrdatas: []string{"3.5.7.9"},
|
||||
},
|
||||
{
|
||||
Name: "org.",
|
||||
Ttl: 300,
|
||||
Type: "SOA",
|
||||
Rrdatas: []string{"ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &gcp.ResourceRecordSetsListResponse{Rrsets: rr}, nil
|
||||
}
|
||||
|
||||
func TestCloudDNS(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
r, err := New(ctx, fakeGCPClient{}, map[string][]string{"bad.": {"bad-project:bad-zone"}}, &upstream.Upstream{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Cloud DNS: %v", err)
|
||||
}
|
||||
if err = r.Run(ctx); err == nil {
|
||||
t.Fatalf("Expected errors for zone bad.")
|
||||
}
|
||||
|
||||
r, err = New(ctx, fakeGCPClient{}, map[string][]string{"org.": {"sample-project-1:sample-zone-2", "sample-project-1:sample-zone-1"}, "gov.": {"sample-project-1:sample-zone-2", "sample-project-1:sample-zone-1"}}, &upstream.Upstream{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Cloud DNS: %v", err)
|
||||
}
|
||||
r.Fall = fall.Zero
|
||||
r.Fall.SetZonesFromArgs([]string{"gov."})
|
||||
r.Next = test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := crequest.Request{W: w, Req: r}
|
||||
qname := state.Name()
|
||||
m := new(dns.Msg)
|
||||
rcode := dns.RcodeServerFailure
|
||||
if qname == "example.gov." {
|
||||
m.SetReply(r)
|
||||
rr, err := dns.NewRR("example.gov. 300 IN A 2.4.6.8")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Resource Record: %v", err)
|
||||
}
|
||||
m.Answer = []dns.RR{rr}
|
||||
|
||||
m.Authoritative = true
|
||||
rcode = dns.RcodeSuccess
|
||||
|
||||
}
|
||||
|
||||
m.SetRcode(r, rcode)
|
||||
w.WriteMsg(m)
|
||||
return rcode, nil
|
||||
})
|
||||
err = r.Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize Cloud DNS: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
qname string
|
||||
qtype uint16
|
||||
wantRetCode int
|
||||
wantAnswer []string // ownernames for the records in the additional section.
|
||||
wantMsgRCode int
|
||||
wantNS []string
|
||||
expectedErr error
|
||||
}{
|
||||
// 0. example.org A found - success.
|
||||
{
|
||||
qname: "example.org",
|
||||
qtype: dns.TypeA,
|
||||
wantAnswer: []string{"example.org. 300 IN A 1.2.3.4"},
|
||||
},
|
||||
// 1. example.org AAAA found - success.
|
||||
{
|
||||
qname: "example.org",
|
||||
qtype: dns.TypeAAAA,
|
||||
wantAnswer: []string{"example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334"},
|
||||
},
|
||||
// 2. exampled.org PTR found - success.
|
||||
{
|
||||
qname: "example.org",
|
||||
qtype: dns.TypePTR,
|
||||
wantAnswer: []string{"example.org. 300 IN PTR ptr.example.org."},
|
||||
},
|
||||
// 3. sample.example.org points to example.org CNAME.
|
||||
// Query must return both CNAME and A recs.
|
||||
{
|
||||
qname: "sample.example.org",
|
||||
qtype: dns.TypeA,
|
||||
wantAnswer: []string{
|
||||
"sample.example.org. 300 IN CNAME example.org.",
|
||||
"example.org. 300 IN A 1.2.3.4",
|
||||
},
|
||||
},
|
||||
// 4. Explicit CNAME query for sample.example.org.
|
||||
// Query must return just CNAME.
|
||||
{
|
||||
qname: "sample.example.org",
|
||||
qtype: dns.TypeCNAME,
|
||||
wantAnswer: []string{"sample.example.org. 300 IN CNAME example.org."},
|
||||
},
|
||||
// 5. Explicit SOA query for example.org.
|
||||
{
|
||||
qname: "example.org",
|
||||
qtype: dns.TypeSOA,
|
||||
wantAnswer: []string{"org. 300 IN SOA ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
// 6. Explicit SOA query for example.org.
|
||||
{
|
||||
qname: "example.org",
|
||||
qtype: dns.TypeNS,
|
||||
wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
// 7. AAAA query for split-example.org must return NODATA.
|
||||
{
|
||||
qname: "split-example.gov",
|
||||
qtype: dns.TypeAAAA,
|
||||
wantRetCode: dns.RcodeSuccess,
|
||||
wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
// 8. Zone not configured.
|
||||
{
|
||||
qname: "badexample.com",
|
||||
qtype: dns.TypeA,
|
||||
wantRetCode: dns.RcodeServerFailure,
|
||||
wantMsgRCode: dns.RcodeServerFailure,
|
||||
},
|
||||
// 9. No record found. Return SOA record.
|
||||
{
|
||||
qname: "bad.org",
|
||||
qtype: dns.TypeA,
|
||||
wantRetCode: dns.RcodeSuccess,
|
||||
wantMsgRCode: dns.RcodeNameError,
|
||||
wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
// 10. No record found. Fallthrough.
|
||||
{
|
||||
qname: "example.gov",
|
||||
qtype: dns.TypeA,
|
||||
wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"},
|
||||
},
|
||||
// 11. other-zone.example.org is stored in a different hosted zone. success
|
||||
{
|
||||
qname: "other-example.org",
|
||||
qtype: dns.TypeA,
|
||||
wantAnswer: []string{"other-example.org. 300 IN A 3.5.7.9"},
|
||||
},
|
||||
// 12. split-example.org only has A record. Expect NODATA.
|
||||
{
|
||||
qname: "split-example.org",
|
||||
qtype: dns.TypeAAAA,
|
||||
wantNS: []string{"org. 300 IN SOA ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"},
|
||||
},
|
||||
// 13. *.www.example.org is a wildcard CNAME to www.example.org.
|
||||
{
|
||||
qname: "a.www.example.org",
|
||||
qtype: dns.TypeA,
|
||||
wantAnswer: []string{
|
||||
"a.www.example.org. 300 IN CNAME www.example.org.",
|
||||
"www.example.org. 300 IN A 1.2.3.4",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for ti, 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.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err)
|
||||
}
|
||||
if code != int(tc.wantRetCode) {
|
||||
t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code])
|
||||
}
|
||||
|
||||
if tc.wantMsgRCode != rec.Msg.Rcode {
|
||||
t.Errorf("Test %d: Unexpected msg status code. Want: %s, got: %s", ti, dns.RcodeToString[tc.wantMsgRCode], dns.RcodeToString[rec.Msg.Rcode])
|
||||
}
|
||||
|
||||
if len(tc.wantAnswer) != len(rec.Msg.Answer) {
|
||||
t.Errorf("Test %d: Unexpected number of Answers. Want: %d, got: %d", ti, len(tc.wantAnswer), len(rec.Msg.Answer))
|
||||
} else {
|
||||
for i, gotAnswer := range rec.Msg.Answer {
|
||||
if gotAnswer.String() != tc.wantAnswer[i] {
|
||||
t.Errorf("Test %d: Unexpected answer.\nWant:\n\t%s\nGot:\n\t%s", ti, tc.wantAnswer[i], gotAnswer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.wantNS) != len(rec.Msg.Ns) {
|
||||
t.Errorf("Test %d: Unexpected NS number. Want: %d, got: %d", ti, len(tc.wantNS), len(rec.Msg.Ns))
|
||||
} else {
|
||||
for i, ns := range rec.Msg.Ns {
|
||||
got, ok := ns.(*dns.SOA)
|
||||
if !ok {
|
||||
t.Errorf("Test %d: Unexpected NS type. Want: SOA, got: %v", ti, reflect.TypeOf(got))
|
||||
}
|
||||
if got.String() != tc.wantNS[i] {
|
||||
t.Errorf("Test %d: Unexpected NS.\nWant: %v\nGot: %v", ti, tc.wantNS[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
plugin/clouddns/gcp.go
Normal file
32
plugin/clouddns/gcp.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package clouddns
|
||||
|
||||
import gcp "google.golang.org/api/dns/v1"
|
||||
|
||||
type gcpDNS interface {
|
||||
zoneExists(projectName, hostedZoneName string) error
|
||||
listRRSets(projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error)
|
||||
}
|
||||
|
||||
type gcpClient struct {
|
||||
*gcp.Service
|
||||
}
|
||||
|
||||
// zoneExists is a wrapper method around `gcp.Service.ManagedZones.Get`
|
||||
// it checks if the provided zone name for a given project exists.
|
||||
func (c gcpClient) zoneExists(projectName, hostedZoneName string) error {
|
||||
_, err := c.ManagedZones.Get(projectName, hostedZoneName).Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listRRSets is a wrapper method around `gcp.Service.ResourceRecordSets.List`
|
||||
// it fetches and returns the record sets for a hosted zone.
|
||||
func (c gcpClient) listRRSets(projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error) {
|
||||
rr, err := c.ResourceRecordSets.List(projectName, hostedZoneName).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rr, nil
|
||||
}
|
5
plugin/clouddns/log_test.go
Normal file
5
plugin/clouddns/log_test.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package clouddns
|
||||
|
||||
import clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
|
||||
func init() { clog.Discard() }
|
110
plugin/clouddns/setup.go
Normal file
110
plugin/clouddns/setup.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package clouddns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
gcp "google.golang.org/api/dns/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
var log = clog.NewWithPlugin("clouddns")
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("clouddns", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: func(c *caddy.Controller) error {
|
||||
f := func(ctx context.Context, opt option.ClientOption) (gcpDNS, error) {
|
||||
var err error
|
||||
var client *gcp.Service
|
||||
if opt != nil {
|
||||
client, err = gcp.NewService(ctx, opt)
|
||||
} else {
|
||||
// if credentials file is not provided in the Corefile
|
||||
// authenticate the client using env variables
|
||||
client, err = gcp.NewService(ctx)
|
||||
}
|
||||
return gcpClient{client}, err
|
||||
}
|
||||
return setup(c, f)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller, f func(ctx context.Context, opt option.ClientOption) (gcpDNS, error)) error {
|
||||
for c.Next() {
|
||||
keyPairs := map[string]struct{}{}
|
||||
keys := map[string][]string{}
|
||||
|
||||
var fall fall.F
|
||||
up := upstream.New()
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
parts := strings.SplitN(args[i], ":", 3)
|
||||
if len(parts) != 3 {
|
||||
return c.Errf("invalid zone '%s'", args[i])
|
||||
}
|
||||
dnsName, projectName, hostedZone := parts[0], parts[1], parts[2]
|
||||
if dnsName == "" || projectName == "" || hostedZone == "" {
|
||||
return c.Errf("invalid zone '%s'", args[i])
|
||||
}
|
||||
if _, ok := keyPairs[args[i]]; ok {
|
||||
return c.Errf("conflict zone '%s'", args[i])
|
||||
}
|
||||
|
||||
keyPairs[args[i]] = struct{}{}
|
||||
keys[dnsName] = append(keys[dnsName], projectName+":"+hostedZone)
|
||||
}
|
||||
|
||||
var opt option.ClientOption
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "upstream":
|
||||
c.RemainingArgs() // eats args
|
||||
// if filepath is provided in the Corefile use it to authenticate the dns client
|
||||
case "credentials":
|
||||
if c.NextArg() {
|
||||
opt = option.WithCredentialsFile(c.Val())
|
||||
} else {
|
||||
return c.ArgErr()
|
||||
}
|
||||
case "fallthrough":
|
||||
fall.SetZonesFromArgs(c.RemainingArgs())
|
||||
default:
|
||||
return c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := f(ctx, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := New(ctx, client, keys, up)
|
||||
if err != nil {
|
||||
return c.Errf("failed to create Cloud DNS plugin: %v", err)
|
||||
}
|
||||
h.Fall = fall
|
||||
|
||||
if err := h.Run(ctx); err != nil {
|
||||
return c.Errf("failed to initialize Cloud DNS plugin: %v", err)
|
||||
}
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
h.Next = next
|
||||
return h
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
48
plugin/clouddns/setup_test.go
Normal file
48
plugin/clouddns/setup_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package clouddns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
func TestSetupCloudDNS(t *testing.T) {
|
||||
f := func(ctx context.Context, opt option.ClientOption) (gcpDNS, error) {
|
||||
return fakeGCPClient{}, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
body string
|
||||
expectedError bool
|
||||
}{
|
||||
{`clouddns`, false},
|
||||
{`clouddns :`, true},
|
||||
{`clouddns ::`, true},
|
||||
{`clouddns example.org.:example-project:zone-name`, false},
|
||||
{`clouddns example.org.:example-project:zone-name { }`, false},
|
||||
{`clouddns example.org.:example-project: { }`, true},
|
||||
{`clouddns example.org.:example-project:zone-name { }`, false},
|
||||
{`clouddns example.org.:example-project:zone-name { wat
|
||||
}`, true},
|
||||
{`clouddns example.org.:example-project:zone-name {
|
||||
fallthrough
|
||||
}`, false},
|
||||
{`clouddns example.org.:example-project:zone-name {
|
||||
credentials
|
||||
}`, true},
|
||||
{`clouddns example.org.:example-project:zone-name example.org.:example-project:zone-name {
|
||||
}`, true},
|
||||
|
||||
{`clouddns example.org {
|
||||
}`, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.body)
|
||||
if err := setup(c, f); (err == nil) == test.expectedError {
|
||||
t.Errorf("Unexpected errors: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue