Skip to content

Commit

Permalink
Add Google Cloud DNS plugin (coredns#3011)
Browse files Browse the repository at this point in the history
Signed-off-by: Palash Nigam <npalash25@gmail.com>

Closes: coredns#2822
  • Loading branch information
palash25 authored and yongtang committed Aug 17, 2019
1 parent bde3930 commit 194b0f9
Show file tree
Hide file tree
Showing 13 changed files with 825 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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": []
}
]
}
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var Directives = []string{
"hosts",
"route53",
"azure",
"clouddns",
"federation",
"k8s_external",
"kubernetes",
Expand Down
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 4 additions & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions plugin/clouddns/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 222 additions & 0 deletions plugin/clouddns/clouddns.go
Original file line number Diff line number Diff line change
@@ -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" }
Loading

0 comments on commit 194b0f9

Please sign in to comment.