mirror of
				https://github.com/kubernetes-sigs/external-dns.git
				synced 2025-11-04 04:31:00 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			484 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			484 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2018 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package pdns
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"math"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	pgo "github.com/ffledgling/pdns-go"
 | 
						|
	log "github.com/sirupsen/logrus"
 | 
						|
 | 
						|
	"sigs.k8s.io/external-dns/endpoint"
 | 
						|
	"sigs.k8s.io/external-dns/pkg/tlsutils"
 | 
						|
	"sigs.k8s.io/external-dns/plan"
 | 
						|
	"sigs.k8s.io/external-dns/provider"
 | 
						|
)
 | 
						|
 | 
						|
type pdnsChangeType string
 | 
						|
 | 
						|
const (
 | 
						|
	apiBase = "/api/v1"
 | 
						|
 | 
						|
	// Unless we use something like pdnsproxy (discontinued upstream), this value will _always_ be localhost
 | 
						|
	defaultServerID = "localhost"
 | 
						|
	defaultTTL      = 300
 | 
						|
 | 
						|
	// PdnsDelete and PdnsReplace are effectively an enum for "pgo.RrSet.changetype"
 | 
						|
	// TODO: Can we somehow get this from the pgo swagger client library itself?
 | 
						|
 | 
						|
	// PdnsDelete : PowerDNS changetype used for deleting rrsets
 | 
						|
	// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#rrset (see "changetype")
 | 
						|
	PdnsDelete pdnsChangeType = "DELETE"
 | 
						|
	// PdnsReplace : PowerDNS changetype for creating, updating and patching rrsets
 | 
						|
	PdnsReplace pdnsChangeType = "REPLACE"
 | 
						|
	// Number of times to retry failed PDNS requests
 | 
						|
	retryLimit = 3
 | 
						|
	// time in milliseconds
 | 
						|
	retryAfterTime = 250 * time.Millisecond
 | 
						|
)
 | 
						|
 | 
						|
// PDNSConfig is comprised of the fields necessary to create a new PDNSProvider
 | 
						|
type PDNSConfig struct {
 | 
						|
	DomainFilter endpoint.DomainFilter
 | 
						|
	DryRun       bool
 | 
						|
	Server       string
 | 
						|
	APIKey       string
 | 
						|
	TLSConfig    TLSConfig
 | 
						|
}
 | 
						|
 | 
						|
// TLSConfig is comprised of the TLS-related fields necessary to create a new PDNSProvider
 | 
						|
type TLSConfig struct {
 | 
						|
	TLSEnabled            bool
 | 
						|
	CAFilePath            string
 | 
						|
	ClientCertFilePath    string
 | 
						|
	ClientCertKeyFilePath string
 | 
						|
}
 | 
						|
 | 
						|
func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) error {
 | 
						|
	if !tlsConfig.TLSEnabled {
 | 
						|
		log.Debug("Skipping TLS for PDNS Provider.")
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug("Configuring TLS for PDNS Provider.")
 | 
						|
	if tlsConfig.CAFilePath == "" {
 | 
						|
		return errors.New("certificate authority file path must be specified if TLS is enabled")
 | 
						|
	}
 | 
						|
 | 
						|
	tlsClientConfig, err := tlsutils.NewTLSConfig(tlsConfig.ClientCertFilePath, tlsConfig.ClientCertKeyFilePath, tlsConfig.CAFilePath, "", false, tls.VersionTLS12)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Timeouts taken from net.http.DefaultTransport
 | 
						|
	transporter := &http.Transport{
 | 
						|
		Proxy: http.ProxyFromEnvironment,
 | 
						|
		DialContext: (&net.Dialer{
 | 
						|
			Timeout:   30 * time.Second,
 | 
						|
			KeepAlive: 30 * time.Second,
 | 
						|
			DualStack: true,
 | 
						|
		}).DialContext,
 | 
						|
		MaxIdleConns:          100,
 | 
						|
		IdleConnTimeout:       90 * time.Second,
 | 
						|
		TLSHandshakeTimeout:   10 * time.Second,
 | 
						|
		ExpectContinueTimeout: 1 * time.Second,
 | 
						|
		TLSClientConfig:       tlsClientConfig,
 | 
						|
	}
 | 
						|
	pdnsClientConfig.HTTPClient = &http.Client{
 | 
						|
		Transport: transporter,
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Function for debug printing
 | 
						|
func stringifyHTTPResponseBody(r *http.Response) (body string) {
 | 
						|
	if r == nil {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	buf := new(bytes.Buffer)
 | 
						|
	buf.ReadFrom(r.Body)
 | 
						|
	body = buf.String()
 | 
						|
	return body
 | 
						|
}
 | 
						|
 | 
						|
// PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as
 | 
						|
// well as mock APIClients used in testing
 | 
						|
type PDNSAPIProvider interface {
 | 
						|
	ListZones() ([]pgo.Zone, *http.Response, error)
 | 
						|
	PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone)
 | 
						|
	ListZone(zoneID string) (pgo.Zone, *http.Response, error)
 | 
						|
	PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error)
 | 
						|
}
 | 
						|
 | 
						|
// PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details
 | 
						|
type PDNSAPIClient struct {
 | 
						|
	dryRun       bool
 | 
						|
	authCtx      context.Context
 | 
						|
	client       *pgo.APIClient
 | 
						|
	domainFilter endpoint.DomainFilter
 | 
						|
}
 | 
						|
 | 
						|
// ListZones : Method returns all enabled zones from PowerDNS
 | 
						|
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones
 | 
						|
func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) {
 | 
						|
	for i := 0; i < retryLimit; i++ {
 | 
						|
		zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, defaultServerID)
 | 
						|
		if err != nil {
 | 
						|
			log.Debugf("Unable to fetch zones %v", err)
 | 
						|
			log.Debugf("Retrying ListZones() ... %d", i)
 | 
						|
			time.Sleep(retryAfterTime * (1 << uint(i)))
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		return zones, resp, err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Errorf("Unable to fetch zones. %v", err)
 | 
						|
	return zones, resp, err
 | 
						|
}
 | 
						|
 | 
						|
// PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter
 | 
						|
func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) {
 | 
						|
	if c.domainFilter.IsConfigured() {
 | 
						|
		for _, zone := range zones {
 | 
						|
			if c.domainFilter.Match(zone.Name) || c.domainFilter.MatchParent(zone.Name) {
 | 
						|
				filteredZones = append(filteredZones, zone)
 | 
						|
			} else {
 | 
						|
				residualZones = append(residualZones, zone)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		filteredZones = zones
 | 
						|
	}
 | 
						|
	return filteredZones, residualZones
 | 
						|
}
 | 
						|
 | 
						|
// ListZone : Method returns the details of a specific zone from PowerDNS
 | 
						|
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id
 | 
						|
func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) {
 | 
						|
	for i := 0; i < retryLimit; i++ {
 | 
						|
		zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, defaultServerID, zoneID)
 | 
						|
		if err != nil {
 | 
						|
			log.Debugf("Unable to fetch zone %v", err)
 | 
						|
			log.Debugf("Retrying ListZone() ... %d", i)
 | 
						|
			time.Sleep(retryAfterTime * (1 << uint(i)))
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		return zone, resp, err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Errorf("Unable to list zone. %v", err)
 | 
						|
	return zone, resp, err
 | 
						|
}
 | 
						|
 | 
						|
// PatchZone : Method used to update the contents of a particular zone from PowerDNS
 | 
						|
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id
 | 
						|
func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) {
 | 
						|
	for i := 0; i < retryLimit; i++ {
 | 
						|
		resp, err = c.client.ZonesApi.PatchZone(c.authCtx, defaultServerID, zoneID, zoneStruct)
 | 
						|
		if err != nil {
 | 
						|
			log.Debugf("Unable to patch zone %v", err)
 | 
						|
			log.Debugf("Retrying PatchZone() ... %d", i)
 | 
						|
			time.Sleep(retryAfterTime * (1 << uint(i)))
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		return resp, err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Errorf("Unable to patch zone. %v", err)
 | 
						|
	return resp, err
 | 
						|
}
 | 
						|
 | 
						|
// PDNSProvider is an implementation of the Provider interface for PowerDNS
 | 
						|
type PDNSProvider struct {
 | 
						|
	provider.BaseProvider
 | 
						|
	client PDNSAPIProvider
 | 
						|
}
 | 
						|
 | 
						|
// NewPDNSProvider initializes a new PowerDNS based Provider.
 | 
						|
func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) {
 | 
						|
	// Do some input validation
 | 
						|
 | 
						|
	if config.APIKey == "" {
 | 
						|
		return nil, errors.New("missing API Key for PDNS. Specify using --pdns-api-key=")
 | 
						|
	}
 | 
						|
 | 
						|
	// We do not support dry running, exit safely instead of surprising the user
 | 
						|
	// TODO: Add Dry Run support
 | 
						|
	if config.DryRun {
 | 
						|
		return nil, errors.New("PDNS Provider does not currently support dry-run")
 | 
						|
	}
 | 
						|
 | 
						|
	if config.Server == "localhost" {
 | 
						|
		log.Warnf("PDNS Server is set to localhost, this may not be what you want. Specify using --pdns-server=")
 | 
						|
	}
 | 
						|
 | 
						|
	pdnsClientConfig := pgo.NewConfiguration()
 | 
						|
	pdnsClientConfig.BasePath = config.Server + apiBase
 | 
						|
	if err := config.TLSConfig.setHTTPClient(pdnsClientConfig); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	provider := &PDNSProvider{
 | 
						|
		client: &PDNSAPIClient{
 | 
						|
			dryRun:       config.DryRun,
 | 
						|
			authCtx:      context.WithValue(ctx, pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}),
 | 
						|
			client:       pgo.NewAPIClient(pdnsClientConfig),
 | 
						|
			domainFilter: config.DomainFilter,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	return provider, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
 | 
						|
	endpoints = []*endpoint.Endpoint{}
 | 
						|
	var targets = []string{}
 | 
						|
 | 
						|
	for _, record := range rr.Records {
 | 
						|
		// If a record is "Disabled", it's not supposed to be "visible"
 | 
						|
		if !record.Disabled {
 | 
						|
			targets = append(targets, record.Content)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), targets...))
 | 
						|
	return endpoints, nil
 | 
						|
}
 | 
						|
 | 
						|
// ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
 | 
						|
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
 | 
						|
	zonelist = []pgo.Zone{}
 | 
						|
	endpoints := make([]*endpoint.Endpoint, len(eps))
 | 
						|
	copy(endpoints, eps)
 | 
						|
 | 
						|
	// Sort the endpoints array so we have deterministic inserts
 | 
						|
	sort.SliceStable(endpoints,
 | 
						|
		func(i, j int) bool {
 | 
						|
			// We only care about sorting endpoints with the same dnsname
 | 
						|
			if endpoints[i].DNSName == endpoints[j].DNSName {
 | 
						|
				return endpoints[i].RecordType < endpoints[j].RecordType
 | 
						|
			}
 | 
						|
			return endpoints[i].DNSName < endpoints[j].DNSName
 | 
						|
		})
 | 
						|
 | 
						|
	zones, _, err := p.client.ListZones()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	filteredZones, residualZones := p.client.PartitionZones(zones)
 | 
						|
 | 
						|
	// Sort the zone by length of the name in descending order, we use this
 | 
						|
	// property later to ensure we add a record to the longest matching zone
 | 
						|
 | 
						|
	sort.SliceStable(filteredZones, func(i, j int) bool { return len(filteredZones[i].Name) > len(filteredZones[j].Name) })
 | 
						|
 | 
						|
	// NOTE: Complexity of this loop is O(FilteredZones*Endpoints).
 | 
						|
	// A possibly faster implementation would be a search of the reversed
 | 
						|
	// DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not
 | 
						|
	// necessary.
 | 
						|
	for _, zone := range filteredZones {
 | 
						|
		zone.Rrsets = []pgo.RrSet{}
 | 
						|
		for i := 0; i < len(endpoints); {
 | 
						|
			ep := endpoints[i]
 | 
						|
			dnsname := provider.EnsureTrailingDot(ep.DNSName)
 | 
						|
			if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
 | 
						|
				// The assumption here is that there will only ever be one target
 | 
						|
				// per (ep.DNSName, ep.RecordType) tuple, which holds true for
 | 
						|
				// external-dns v5.0.0-alpha onwards
 | 
						|
				records := []pgo.Record{}
 | 
						|
				for _, t := range ep.Targets {
 | 
						|
					if ep.RecordType == "CNAME" {
 | 
						|
						t = provider.EnsureTrailingDot(t)
 | 
						|
					}
 | 
						|
 | 
						|
					records = append(records, pgo.Record{Content: t})
 | 
						|
				}
 | 
						|
				rrset := pgo.RrSet{
 | 
						|
					Name:       dnsname,
 | 
						|
					Type_:      ep.RecordType,
 | 
						|
					Records:    records,
 | 
						|
					Changetype: string(changetype),
 | 
						|
				}
 | 
						|
 | 
						|
				// DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
 | 
						|
				if changetype == PdnsReplace {
 | 
						|
					if int64(ep.RecordTTL) > int64(math.MaxInt32) {
 | 
						|
						return nil, errors.New("value of record TTL overflows, limited to int32")
 | 
						|
					}
 | 
						|
					if ep.RecordTTL == 0 {
 | 
						|
						// No TTL was specified for the record, we use the default
 | 
						|
						rrset.Ttl = int32(defaultTTL)
 | 
						|
					} else {
 | 
						|
						rrset.Ttl = int32(ep.RecordTTL)
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				zone.Rrsets = append(zone.Rrsets, rrset)
 | 
						|
 | 
						|
				// "pop" endpoint if it's matched
 | 
						|
				endpoints = append(endpoints[0:i], endpoints[i+1:]...)
 | 
						|
			} else {
 | 
						|
				// If we didn't pop anything, we move to the next item in the list
 | 
						|
				i++
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if len(zone.Rrsets) > 0 {
 | 
						|
			zonelist = append(zonelist, zone)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// residualZones is unsorted by name length like its counterpart
 | 
						|
	// since we only care to remove endpoints that do not match domain filter
 | 
						|
	for _, zone := range residualZones {
 | 
						|
		for i := 0; i < len(endpoints); {
 | 
						|
			ep := endpoints[i]
 | 
						|
			dnsname := provider.EnsureTrailingDot(ep.DNSName)
 | 
						|
			if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
 | 
						|
				// "pop" endpoint if it's matched to a residual zone... essentially a no-op
 | 
						|
				log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname)
 | 
						|
				endpoints = append(endpoints[0:i], endpoints[i+1:]...)
 | 
						|
			} else {
 | 
						|
				i++
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them
 | 
						|
	// We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
 | 
						|
	if len(endpoints) > 0 {
 | 
						|
		log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints)
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Zone List generated from Endpoints: %+v", zonelist)
 | 
						|
 | 
						|
	return zonelist, nil
 | 
						|
}
 | 
						|
 | 
						|
// mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype
 | 
						|
func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error {
 | 
						|
	zonelist, err := p.ConvertEndpointsToZones(endpoints, changetype)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	for _, zone := range zonelist {
 | 
						|
		jso, err := json.Marshal(zone)
 | 
						|
		if err != nil {
 | 
						|
			log.Errorf("JSON Marshal for zone struct failed!")
 | 
						|
		} else {
 | 
						|
			log.Debugf("Struct for PatchZone:\n%s", string(jso))
 | 
						|
		}
 | 
						|
		resp, err := p.client.PatchZone(zone.Id, zone)
 | 
						|
		if err != nil {
 | 
						|
			log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp))
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Records returns all DNS records controlled by the configured PDNS server (for all zones)
 | 
						|
func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
 | 
						|
	zones, _, err := p.client.ListZones()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	filteredZones, _ := p.client.PartitionZones(zones)
 | 
						|
 | 
						|
	for _, zone := range filteredZones {
 | 
						|
		z, _, err := p.client.ListZone(zone.Id)
 | 
						|
		if err != nil {
 | 
						|
			log.Warnf("Unable to fetch Records")
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		for _, rr := range z.Rrsets {
 | 
						|
			e, err := p.convertRRSetToEndpoints(rr)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			endpoints = append(endpoints, e...)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Records fetched:\n%+v", endpoints)
 | 
						|
	return endpoints, nil
 | 
						|
}
 | 
						|
 | 
						|
// ApplyChanges takes a list of changes (endpoints) and updates the PDNS server
 | 
						|
// by sending the correct HTTP PATCH requests to a matching zone
 | 
						|
func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
 | 
						|
	startTime := time.Now()
 | 
						|
 | 
						|
	// Create
 | 
						|
	for _, change := range changes.Create {
 | 
						|
		log.Debugf("CREATE: %+v", change)
 | 
						|
	}
 | 
						|
	// We only attempt to mutate records if there are any to mutate.  A
 | 
						|
	// call to mutate records with an empty list of endpoints is still a
 | 
						|
	// valid call and a no-op, but we might as well not make the call to
 | 
						|
	// prevent unnecessary logging
 | 
						|
	if len(changes.Create) > 0 {
 | 
						|
		// "Replacing" non-existent records creates them
 | 
						|
		err := p.mutateRecords(changes.Create, PdnsReplace)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Update
 | 
						|
	for _, change := range changes.UpdateOld {
 | 
						|
		// Since PDNS "Patches", we don't need to specify the "old"
 | 
						|
		// record. The Update New change type will automatically take
 | 
						|
		// care of replacing the old RRSet with the new one We simply
 | 
						|
		// leave this logging here for information
 | 
						|
		log.Debugf("UPDATE-OLD (ignored): %+v", change)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, change := range changes.UpdateNew {
 | 
						|
		log.Debugf("UPDATE-NEW: %+v", change)
 | 
						|
	}
 | 
						|
	if len(changes.UpdateNew) > 0 {
 | 
						|
		err := p.mutateRecords(changes.UpdateNew, PdnsReplace)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Delete
 | 
						|
	for _, change := range changes.Delete {
 | 
						|
		log.Debugf("DELETE: %+v", change)
 | 
						|
	}
 | 
						|
	if len(changes.Delete) > 0 {
 | 
						|
		err := p.mutateRecords(changes.Delete, PdnsDelete)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime))
 | 
						|
	return nil
 | 
						|
}
 |