aboutsummaryrefslogtreecommitdiff
path: root/plugin/clouddns
diff options
context:
space:
mode:
authorGravatar Palash Nigam <npalash25@gmail.com> 2019-08-18 02:29:09 +0530
committerGravatar Yong Tang <yong.tang.github@outlook.com> 2019-08-17 13:59:09 -0700
commit194b0f95b459a593deb6c2d1e048e020070a841a (patch)
tree3f8a6fed86cf61ffe62cafe3c9e8ee00238836fc /plugin/clouddns
parentbde393096f76dc02752f71f9deff5934353e4eb8 (diff)
downloadcoredns-194b0f95b459a593deb6c2d1e048e020070a841a.tar.gz
coredns-194b0f95b459a593deb6c2d1e048e020070a841a.tar.zst
coredns-194b0f95b459a593deb6c2d1e048e020070a841a.zip
Add Google Cloud DNS plugin (#3011)
Signed-off-by: Palash Nigam <npalash25@gmail.com> Closes: #2822
Diffstat (limited to 'plugin/clouddns')
-rw-r--r--plugin/clouddns/README.md67
-rw-r--r--plugin/clouddns/clouddns.go222
-rw-r--r--plugin/clouddns/clouddns_test.go316
-rw-r--r--plugin/clouddns/gcp.go32
-rw-r--r--plugin/clouddns/log_test.go5
-rw-r--r--plugin/clouddns/setup.go110
-rw-r--r--plugin/clouddns/setup_test.go48
7 files changed, 800 insertions, 0 deletions
diff --git a/plugin/clouddns/README.md b/plugin/clouddns/README.md
new file mode 100644
index 000000000..4aa5f04fb
--- /dev/null
+++ b/plugin/clouddns/README.md
@@ -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
+}
+~~~
diff --git a/plugin/clouddns/clouddns.go b/plugin/clouddns/clouddns.go
new file mode 100644
index 000000000..ab04e5f75
--- /dev/null
+++ b/plugin/clouddns/clouddns.go
@@ -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" }
diff --git a/plugin/clouddns/clouddns_test.go b/plugin/clouddns/clouddns_test.go
new file mode 100644
index 000000000..dafd65bba
--- /dev/null
+++ b/plugin/clouddns/clouddns_test.go
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/clouddns/gcp.go b/plugin/clouddns/gcp.go
new file mode 100644
index 000000000..6d9d85d43
--- /dev/null
+++ b/plugin/clouddns/gcp.go
@@ -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
+}
diff --git a/plugin/clouddns/log_test.go b/plugin/clouddns/log_test.go
new file mode 100644
index 000000000..148635b4b
--- /dev/null
+++ b/plugin/clouddns/log_test.go
@@ -0,0 +1,5 @@
+package clouddns
+
+import clog "github.com/coredns/coredns/plugin/pkg/log"
+
+func init() { clog.Discard() }
diff --git a/plugin/clouddns/setup.go b/plugin/clouddns/setup.go
new file mode 100644
index 000000000..732c240f7
--- /dev/null
+++ b/plugin/clouddns/setup.go
@@ -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
+}
diff --git a/plugin/clouddns/setup_test.go b/plugin/clouddns/setup_test.go
new file mode 100644
index 000000000..be9c51d92
--- /dev/null
+++ b/plugin/clouddns/setup_test.go
@@ -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)
+ }
+ }
+}