aboutsummaryrefslogtreecommitdiff
path: root/plugin/clouddns/clouddns.go
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/clouddns.go
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/clouddns.go')
-rw-r--r--plugin/clouddns/clouddns.go222
1 files changed, 222 insertions, 0 deletions
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" }