sigs.k8s.io/external-dns@v0.14.1/provider/pdns/pdns.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package pdns
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"encoding/json"
    24  	"errors"
    25  	"math"
    26  	"net"
    27  	"net/http"
    28  	"sort"
    29  	"strings"
    30  	"time"
    31  
    32  	pgo "github.com/ffledgling/pdns-go"
    33  	log "github.com/sirupsen/logrus"
    34  
    35  	"sigs.k8s.io/external-dns/endpoint"
    36  	"sigs.k8s.io/external-dns/pkg/tlsutils"
    37  	"sigs.k8s.io/external-dns/plan"
    38  	"sigs.k8s.io/external-dns/provider"
    39  )
    40  
    41  type pdnsChangeType string
    42  
    43  const (
    44  	apiBase = "/api/v1"
    45  
    46  	// Unless we use something like pdnsproxy (discontinued upstream), this value will _always_ be localhost
    47  	defaultServerID = "localhost"
    48  	defaultTTL      = 300
    49  
    50  	// PdnsDelete and PdnsReplace are effectively an enum for "pgo.RrSet.changetype"
    51  	// TODO: Can we somehow get this from the pgo swagger client library itself?
    52  
    53  	// PdnsDelete : PowerDNS changetype used for deleting rrsets
    54  	// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#rrset (see "changetype")
    55  	PdnsDelete pdnsChangeType = "DELETE"
    56  	// PdnsReplace : PowerDNS changetype for creating, updating and patching rrsets
    57  	PdnsReplace pdnsChangeType = "REPLACE"
    58  	// Number of times to retry failed PDNS requests
    59  	retryLimit = 3
    60  	// time in milliseconds
    61  	retryAfterTime = 250 * time.Millisecond
    62  )
    63  
    64  // PDNSConfig is comprised of the fields necessary to create a new PDNSProvider
    65  type PDNSConfig struct {
    66  	DomainFilter endpoint.DomainFilter
    67  	DryRun       bool
    68  	Server       string
    69  	APIKey       string
    70  	TLSConfig    TLSConfig
    71  }
    72  
    73  // TLSConfig is comprised of the TLS-related fields necessary to create a new PDNSProvider
    74  type TLSConfig struct {
    75  	SkipTLSVerify         bool
    76  	CAFilePath            string
    77  	ClientCertFilePath    string
    78  	ClientCertKeyFilePath string
    79  }
    80  
    81  func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) error {
    82  	log.Debug("Configuring TLS for PDNS Provider.")
    83  	tlsClientConfig, err := tlsutils.NewTLSConfig(
    84  		tlsConfig.ClientCertFilePath,
    85  		tlsConfig.ClientCertKeyFilePath,
    86  		tlsConfig.CAFilePath,
    87  		"",
    88  		tlsConfig.SkipTLSVerify,
    89  		tls.VersionTLS12,
    90  	)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Timeouts taken from net.http.DefaultTransport
    96  	transporter := &http.Transport{
    97  		Proxy: http.ProxyFromEnvironment,
    98  		DialContext: (&net.Dialer{
    99  			Timeout:   30 * time.Second,
   100  			KeepAlive: 30 * time.Second,
   101  			DualStack: true,
   102  		}).DialContext,
   103  		MaxIdleConns:          100,
   104  		IdleConnTimeout:       90 * time.Second,
   105  		TLSHandshakeTimeout:   10 * time.Second,
   106  		ExpectContinueTimeout: 1 * time.Second,
   107  		TLSClientConfig:       tlsClientConfig,
   108  	}
   109  	pdnsClientConfig.HTTPClient = &http.Client{
   110  		Transport: transporter,
   111  	}
   112  
   113  	return nil
   114  }
   115  
   116  // Function for debug printing
   117  func stringifyHTTPResponseBody(r *http.Response) (body string) {
   118  	if r == nil {
   119  		return ""
   120  	}
   121  
   122  	buf := new(bytes.Buffer)
   123  	buf.ReadFrom(r.Body)
   124  	body = buf.String()
   125  	return body
   126  }
   127  
   128  // PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as
   129  // well as mock APIClients used in testing
   130  type PDNSAPIProvider interface {
   131  	ListZones() ([]pgo.Zone, *http.Response, error)
   132  	PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone)
   133  	ListZone(zoneID string) (pgo.Zone, *http.Response, error)
   134  	PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error)
   135  }
   136  
   137  // PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details
   138  type PDNSAPIClient struct {
   139  	dryRun       bool
   140  	authCtx      context.Context
   141  	client       *pgo.APIClient
   142  	domainFilter endpoint.DomainFilter
   143  }
   144  
   145  // ListZones : Method returns all enabled zones from PowerDNS
   146  // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones
   147  func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) {
   148  	for i := 0; i < retryLimit; i++ {
   149  		zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, defaultServerID)
   150  		if err != nil {
   151  			log.Debugf("Unable to fetch zones %v", err)
   152  			log.Debugf("Retrying ListZones() ... %d", i)
   153  			time.Sleep(retryAfterTime * (1 << uint(i)))
   154  			continue
   155  		}
   156  		return zones, resp, err
   157  	}
   158  
   159  	log.Errorf("Unable to fetch zones. %v", err)
   160  	return zones, resp, err
   161  }
   162  
   163  // 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
   164  func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) {
   165  	if c.domainFilter.IsConfigured() {
   166  		for _, zone := range zones {
   167  			if c.domainFilter.Match(zone.Name) {
   168  				filteredZones = append(filteredZones, zone)
   169  			} else {
   170  				residualZones = append(residualZones, zone)
   171  			}
   172  		}
   173  	} else {
   174  		filteredZones = zones
   175  	}
   176  	return filteredZones, residualZones
   177  }
   178  
   179  // ListZone : Method returns the details of a specific zone from PowerDNS
   180  // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id
   181  func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) {
   182  	for i := 0; i < retryLimit; i++ {
   183  		zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, defaultServerID, zoneID)
   184  		if err != nil {
   185  			log.Debugf("Unable to fetch zone %v", err)
   186  			log.Debugf("Retrying ListZone() ... %d", i)
   187  			time.Sleep(retryAfterTime * (1 << uint(i)))
   188  			continue
   189  		}
   190  		return zone, resp, err
   191  	}
   192  
   193  	log.Errorf("Unable to list zone. %v", err)
   194  	return zone, resp, err
   195  }
   196  
   197  // PatchZone : Method used to update the contents of a particular zone from PowerDNS
   198  // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id
   199  func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) {
   200  	for i := 0; i < retryLimit; i++ {
   201  		resp, err = c.client.ZonesApi.PatchZone(c.authCtx, defaultServerID, zoneID, zoneStruct)
   202  		if err != nil {
   203  			log.Debugf("Unable to patch zone %v", err)
   204  			log.Debugf("Retrying PatchZone() ... %d", i)
   205  			time.Sleep(retryAfterTime * (1 << uint(i)))
   206  			continue
   207  		}
   208  		return resp, err
   209  	}
   210  
   211  	log.Errorf("Unable to patch zone. %v", err)
   212  	return resp, err
   213  }
   214  
   215  // PDNSProvider is an implementation of the Provider interface for PowerDNS
   216  type PDNSProvider struct {
   217  	provider.BaseProvider
   218  	client PDNSAPIProvider
   219  }
   220  
   221  // NewPDNSProvider initializes a new PowerDNS based Provider.
   222  func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, error) {
   223  	// Do some input validation
   224  
   225  	if config.APIKey == "" {
   226  		return nil, errors.New("missing API Key for PDNS. Specify using --pdns-api-key=")
   227  	}
   228  
   229  	// We do not support dry running, exit safely instead of surprising the user
   230  	// TODO: Add Dry Run support
   231  	if config.DryRun {
   232  		return nil, errors.New("PDNS Provider does not currently support dry-run")
   233  	}
   234  
   235  	if config.Server == "localhost" {
   236  		log.Warnf("PDNS Server is set to localhost, this may not be what you want. Specify using --pdns-server=")
   237  	}
   238  
   239  	pdnsClientConfig := pgo.NewConfiguration()
   240  	pdnsClientConfig.BasePath = config.Server + apiBase
   241  	if err := config.TLSConfig.setHTTPClient(pdnsClientConfig); err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	provider := &PDNSProvider{
   246  		client: &PDNSAPIClient{
   247  			dryRun:       config.DryRun,
   248  			authCtx:      context.WithValue(ctx, pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}),
   249  			client:       pgo.NewAPIClient(pdnsClientConfig),
   250  			domainFilter: config.DomainFilter,
   251  		},
   252  	}
   253  	return provider, nil
   254  }
   255  
   256  func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
   257  	endpoints = []*endpoint.Endpoint{}
   258  	targets := []string{}
   259  	rrType_ := rr.Type_
   260  
   261  	for _, record := range rr.Records {
   262  		// If a record is "Disabled", it's not supposed to be "visible"
   263  		if !record.Disabled {
   264  			targets = append(targets, record.Content)
   265  		}
   266  	}
   267  	if rr.Type_ == "ALIAS" {
   268  		rrType_ = "CNAME"
   269  	}
   270  	endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rrType_, endpoint.TTL(rr.Ttl), targets...))
   271  	return endpoints, nil
   272  }
   273  
   274  // ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
   275  func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
   276  	zonelist = []pgo.Zone{}
   277  	endpoints := make([]*endpoint.Endpoint, len(eps))
   278  	copy(endpoints, eps)
   279  
   280  	// Sort the endpoints array so we have deterministic inserts
   281  	sort.SliceStable(endpoints,
   282  		func(i, j int) bool {
   283  			// We only care about sorting endpoints with the same dnsname
   284  			if endpoints[i].DNSName == endpoints[j].DNSName {
   285  				return endpoints[i].RecordType < endpoints[j].RecordType
   286  			}
   287  			return endpoints[i].DNSName < endpoints[j].DNSName
   288  		})
   289  
   290  	zones, _, err := p.client.ListZones()
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  	filteredZones, residualZones := p.client.PartitionZones(zones)
   295  
   296  	// Sort the zone by length of the name in descending order, we use this
   297  	// property later to ensure we add a record to the longest matching zone
   298  
   299  	sort.SliceStable(filteredZones, func(i, j int) bool { return len(filteredZones[i].Name) > len(filteredZones[j].Name) })
   300  
   301  	// NOTE: Complexity of this loop is O(FilteredZones*Endpoints).
   302  	// A possibly faster implementation would be a search of the reversed
   303  	// DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not
   304  	// necessary.
   305  	for _, zone := range filteredZones {
   306  		zone.Rrsets = []pgo.RrSet{}
   307  		for i := 0; i < len(endpoints); {
   308  			ep := endpoints[i]
   309  			dnsname := provider.EnsureTrailingDot(ep.DNSName)
   310  			if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
   311  				// The assumption here is that there will only ever be one target
   312  				// per (ep.DNSName, ep.RecordType) tuple, which holds true for
   313  				// external-dns v5.0.0-alpha onwards
   314  				records := []pgo.Record{}
   315  				RecordType_ := ep.RecordType
   316  				for _, t := range ep.Targets {
   317  					if ep.RecordType == "CNAME" || ep.RecordType == "ALIAS" {
   318  						t = provider.EnsureTrailingDot(t)
   319  					}
   320  					records = append(records, pgo.Record{Content: t})
   321  				}
   322  
   323  				if dnsname == zone.Name && ep.RecordType == "CNAME" {
   324  					log.Debugf("Converting APEX record %s from CNAME to ALIAS", dnsname)
   325  					RecordType_ = "ALIAS"
   326  				}
   327  
   328  				rrset := pgo.RrSet{
   329  					Name:       dnsname,
   330  					Type_:      RecordType_,
   331  					Records:    records,
   332  					Changetype: string(changetype),
   333  				}
   334  
   335  				// DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
   336  				if changetype == PdnsReplace {
   337  					if int64(ep.RecordTTL) > int64(math.MaxInt32) {
   338  						return nil, errors.New("value of record TTL overflows, limited to int32")
   339  					}
   340  					if ep.RecordTTL == 0 {
   341  						// No TTL was specified for the record, we use the default
   342  						rrset.Ttl = int32(defaultTTL)
   343  					} else {
   344  						rrset.Ttl = int32(ep.RecordTTL)
   345  					}
   346  				}
   347  
   348  				zone.Rrsets = append(zone.Rrsets, rrset)
   349  
   350  				// "pop" endpoint if it's matched
   351  				endpoints = append(endpoints[0:i], endpoints[i+1:]...)
   352  			} else {
   353  				// If we didn't pop anything, we move to the next item in the list
   354  				i++
   355  			}
   356  		}
   357  		if len(zone.Rrsets) > 0 {
   358  			zonelist = append(zonelist, zone)
   359  		}
   360  	}
   361  
   362  	// residualZones is unsorted by name length like its counterpart
   363  	// since we only care to remove endpoints that do not match domain filter
   364  	for _, zone := range residualZones {
   365  		for i := 0; i < len(endpoints); {
   366  			ep := endpoints[i]
   367  			dnsname := provider.EnsureTrailingDot(ep.DNSName)
   368  			if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) {
   369  				// "pop" endpoint if it's matched to a residual zone... essentially a no-op
   370  				log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname)
   371  				endpoints = append(endpoints[0:i], endpoints[i+1:]...)
   372  			} else {
   373  				i++
   374  			}
   375  		}
   376  	}
   377  	// If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them
   378  	// We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
   379  	if len(endpoints) > 0 {
   380  		log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints)
   381  	}
   382  
   383  	log.Debugf("Zone List generated from Endpoints: %+v", zonelist)
   384  
   385  	return zonelist, nil
   386  }
   387  
   388  // mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype
   389  func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error {
   390  	zonelist, err := p.ConvertEndpointsToZones(endpoints, changetype)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	for _, zone := range zonelist {
   395  		jso, err := json.Marshal(zone)
   396  		if err != nil {
   397  			log.Errorf("JSON Marshal for zone struct failed!")
   398  		} else {
   399  			log.Debugf("Struct for PatchZone:\n%s", string(jso))
   400  		}
   401  		resp, err := p.client.PatchZone(zone.Id, zone)
   402  		if err != nil {
   403  			log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp))
   404  			return err
   405  		}
   406  	}
   407  	return nil
   408  }
   409  
   410  // Records returns all DNS records controlled by the configured PDNS server (for all zones)
   411  func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) {
   412  	zones, _, err := p.client.ListZones()
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  	filteredZones, _ := p.client.PartitionZones(zones)
   417  
   418  	for _, zone := range filteredZones {
   419  		z, _, err := p.client.ListZone(zone.Id)
   420  		if err != nil {
   421  			log.Warnf("Unable to fetch Records")
   422  			return nil, err
   423  		}
   424  
   425  		for _, rr := range z.Rrsets {
   426  			e, err := p.convertRRSetToEndpoints(rr)
   427  			if err != nil {
   428  				return nil, err
   429  			}
   430  			endpoints = append(endpoints, e...)
   431  		}
   432  	}
   433  
   434  	log.Debugf("Records fetched:\n%+v", endpoints)
   435  	return endpoints, nil
   436  }
   437  
   438  // ApplyChanges takes a list of changes (endpoints) and updates the PDNS server
   439  // by sending the correct HTTP PATCH requests to a matching zone
   440  func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   441  	startTime := time.Now()
   442  
   443  	// Create
   444  	for _, change := range changes.Create {
   445  		log.Infof("CREATE: %+v", change)
   446  	}
   447  	// We only attempt to mutate records if there are any to mutate.  A
   448  	// call to mutate records with an empty list of endpoints is still a
   449  	// valid call and a no-op, but we might as well not make the call to
   450  	// prevent unnecessary logging
   451  	if len(changes.Create) > 0 {
   452  		// "Replacing" non-existent records creates them
   453  		err := p.mutateRecords(changes.Create, PdnsReplace)
   454  		if err != nil {
   455  			return err
   456  		}
   457  	}
   458  
   459  	// Update
   460  	for _, change := range changes.UpdateOld {
   461  		// Since PDNS "Patches", we don't need to specify the "old"
   462  		// record. The Update New change type will automatically take
   463  		// care of replacing the old RRSet with the new one We simply
   464  		// leave this logging here for information
   465  		log.Debugf("UPDATE-OLD (ignored): %+v", change)
   466  	}
   467  
   468  	for _, change := range changes.UpdateNew {
   469  		log.Infof("UPDATE-NEW: %+v", change)
   470  	}
   471  	if len(changes.UpdateNew) > 0 {
   472  		err := p.mutateRecords(changes.UpdateNew, PdnsReplace)
   473  		if err != nil {
   474  			return err
   475  		}
   476  	}
   477  
   478  	// Delete
   479  	for _, change := range changes.Delete {
   480  		log.Infof("DELETE: %+v", change)
   481  	}
   482  	if len(changes.Delete) > 0 {
   483  		err := p.mutateRecords(changes.Delete, PdnsDelete)
   484  		if err != nil {
   485  			return err
   486  		}
   487  	}
   488  	log.Infof("Changes pushed out to PowerDNS in %s\n", time.Since(startTime))
   489  	return nil
   490  }