Add Google Cloud DNS plugin (#3011)

Signed-off-by: Palash Nigam <npalash25@gmail.com>

Closes: #2822
This commit is contained in:
Palash Nigam 2019-08-18 02:29:09 +05:30 committed by Yong Tang
parent bde393096f
commit 194b0f95b4
13 changed files with 825 additions and 0 deletions

17
.vscode/launch.json vendored Normal file
View 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": []
}
]
}

View file

@ -37,6 +37,7 @@ var Directives = []string{
"hosts",
"route53",
"azure",
"clouddns",
"federation",
"k8s_external",
"kubernetes",

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
View 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" }

View 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
View 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
}

View 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
View 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
}

View 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)
}
}
}