mirror of
https://github.com/coredns/coredns.git
synced 2026-10-17 15:55:47 -04:00
Brings clouddns to parity with route53, which already exposes a refresh directive to control how often zones are re-synced. clouddns currently hardcodes this to 1 minute (delay := 1 * time.Minute in Run) with no way to change it. Adds a refresh field to the CloudDNS struct, threads it through New(), and has Run() use h.refresh instead of the hardcoded delay. Parses the refresh directive in setup() mirroring route53's exact validation (accepts a plain number as seconds or a duration string, must be greater than 0). Extends TestSetupCloudDNS with refresh coverage (valid duration, valid plain number, missing arg, non-numeric, zero, negative). Verified it fails without the setup.go change (unknown property error) and passes with it. Signed-off-by: stackedbyaradhya <aradhyaj736@gmail.com>
230 lines
6.2 KiB
Go
230 lines
6.2 KiB
Go
// 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
|
|
refresh time.Duration
|
|
|
|
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.
|
|
// refresh sets how often the zones are refreshed from Cloud DNS.
|
|
// 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, refresh time.Duration) (*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,
|
|
refresh: refresh,
|
|
}, 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() {
|
|
timer := time.NewTimer(h.refresh)
|
|
defer timer.Stop()
|
|
for {
|
|
timer.Reset(h.refresh)
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debugf("Breaking out of CloudDNS update loop for %v: %v", h.zoneNames, ctx.Err())
|
|
return
|
|
case <-timer.C:
|
|
if err := h.updateZones(ctx); err != nil && ctx.Err() == nil /* Don't log error if ctx expired. */ {
|
|
log.Errorf("Failed to update zones %v: %v", h.zoneNames, 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 authoritative 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)
|
|
}
|
|
|
|
err = z.Insert(r)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert record: %v", err)
|
|
}
|
|
}
|
|
}
|
|
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(ctx, 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 range len(h.zones) {
|
|
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" }
|