Skip to content

Commit

Permalink
Add PDNS Provider
Browse files Browse the repository at this point in the history
- Adds PowerDNS Authoritative Server as a provider
- Adds `--pdns-server` and `--pdns-api-key` flags
- Implements the PDNS provider
  • Loading branch information
ffledgling committed Nov 6, 2017
1 parent 8829fb7 commit fdedf6d
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 1 deletion.
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func main() {
)
case "inmemory":
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
case "pdns":
p, err = provider.NewPDNSProvider(cfg.PDNSServer, cfg.PDNSAPIKey, domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type Config struct {
InfobloxWapiVersion string
InfobloxSSLVerify bool
InMemoryZones []string
PDNSServer string
PDNSAPIKey string
Policy string
Registry string
TXTOwnerID string
Expand Down Expand Up @@ -85,6 +87,8 @@ var defaultConfig = &Config{
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
Expand Down Expand Up @@ -129,7 +133,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)

// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "inmemory")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "inmemory", "pdns")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
Expand All @@ -143,6 +147,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("infoblox-wapi-version", "When using the Infoblox provider, specify the WAPI version (default: 2.3.1)").Default(defaultConfig.InfobloxWapiVersion).StringVar(&cfg.InfobloxWapiVersion)
app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify)
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)

// Flags related to policies
app.Flag("policy", "Modify how DNS records are sychronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only")
Expand Down
290 changes: 290 additions & 0 deletions provider/pdns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package provider

import (
//"strings"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"strings"

log "github.com/sirupsen/logrus"

"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
pgo "github.com/kubernetes-incubator/external-dns/provider/internal/pdns-go"
)

type PDNSChangeType string

const (
API_BASE = "/api/v1"

// Unless we use something like pdnsproxy (discontinued upsteam), this value will _always_ be localhost
DEFAULT_SERVER_ID = "localhost"
DEFAULT_TTL = 300

MAX_UINT32 = ^uint32(0)
MAX_INT32 = MAX_UINT32 >> 1

// This is effectively an enum for "pgo.RrSet.changetype"
// TODO: Can we somehow get this from the pgo swagger client library itself?
PDNSDelete PDNSChangeType = "DELETE"
PDNSReplace PDNSChangeType = "REPLACE"
)

type PDNSProvider struct {
//client Route53API
dryRun bool
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
// filter hosted zones by type (e.g. private or public)
zoneTypeFilter ZoneTypeFilter

// Swagger API Client
client *pgo.APIClient

// Auth context to be passed to client requests, contains API keys etc.
auth_ctx context.Context
}

// This function is just for debugging
func printHTTPResponseBody(r *http.Response) (body string) {

buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
body = buf.String()
return body

}

func NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error) {

// Do some input validation

// We do not support dry running, exit safely instead of surprising the user
// TODO: Add Dry Run support
if dryRun {
log.Fatalf("PDNS Provider does not currently support dry-run, stopping.")
}

if server == "localhost" {
log.Warnf("PDNS Server is set to localhost, this is likely not what you want. Specify using --pdns-server=")
}

if apikey == "" {
log.Warnf("API Key for PDNS is empty. Specify using --pdns-api-key=")
}
if len(domainFilter.filters) == 0 {
log.Warnf("Domain Filter is not supported by PDNS. It will be ignored.")
}

provider := &PDNSProvider{}

cfg := pgo.NewConfiguration()
cfg.Host = server
cfg.BasePath = server + API_BASE

// Initialize a single client that we can use for all requests
provider.client = pgo.NewAPIClient(cfg)

// Configure PDNS API Key, which is sent via X-API-Key header to pdns server
provider.auth_ctx = context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: apikey})

return provider, nil
}

func convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
endpoints = []*endpoint.Endpoint{}

for _, record := range rr.Records {
//func NewEndpointWithTTL(dnsName, target, recordType string, ttl TTL) *Endpoint
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, record.Content, rr.Type_, endpoint.TTL(rr.Ttl)))
}

return endpoints, nil
}

func (p *PDNSProvider) Zones() (zones []pgo.Zone, _ error) {
zones, _, err := p.client.ZonesApi.ListZones(p.auth_ctx, DEFAULT_SERVER_ID)
if err != nil {
log.Warnf("Unable to fetch zones. %v", err)
return nil, err
}

return zones, nil

}

func (p *PDNSProvider) EndpointsToZones(endpoints []*endpoint.Endpoint, changetype PDNSChangeType) (zonelist []pgo.Zone, _ error) {
/* eg of mastermap
{ "example.com":
{ "app.example.com":
{ "A": ["192.168.0.1", "8.8.8.8"] }
{ "TXT": ["\"heritage=external-dns,external-dns/owner=example\""] }
}
}
*/
mastermap := make(map[string]map[string]map[string][]*endpoint.Endpoint)
zoneNameStructMap := map[string]pgo.Zone{}

zones, err := p.Zones()

if err != nil {
return nil, err
}
// Identify zones we control
for _, z := range zones {
mastermap[z.Name] = make(map[string]map[string][]*endpoint.Endpoint)
zoneNameStructMap[z.Name] = z
}

for _, ep := range endpoints {
// Identify which zone an endpoint belongs to
dnsname := ensureTrailingDot(ep.DNSName)
zname := ""
for z := range mastermap {
if strings.HasSuffix(dnsname, z) && len(dnsname) > len(zname) {
zname = z
}
}

// We can encounter a DNS name multiple times (different record types), we only create a map the first time
if _, ok := mastermap[zname][dnsname]; !ok {
mastermap[zname][dnsname] = make(map[string][]*endpoint.Endpoint)
}

// We can get multiple targets for the same record type (eg. Multiple A records for a service)
if _, ok := mastermap[zname][dnsname][ep.RecordType]; !ok {
mastermap[zname][dnsname][ep.RecordType] = make([]*endpoint.Endpoint, 0)
}

mastermap[zname][dnsname][ep.RecordType] = append(mastermap[zname][dnsname][ep.RecordType], ep)

}

//log.Debugf("Conversion Map: %+v", mastermap)

for zname := range mastermap {

zone := zoneNameStructMap[zname]
zone.Rrsets = []pgo.RrSet{}
for rrname := range mastermap[zname] {
for rtype := range mastermap[zname][rrname] {
rrset := pgo.RrSet{}
rrset.Name = rrname
rrset.Type_ = rtype
rrset.Changetype = string(changetype)
rttl := mastermap[zname][rrname][rtype][0].RecordTTL
if int64(rttl) > int64(MAX_INT32) {
return nil, errors.New("Value of record TTL overflows, limited to int32")
}
rrset.Ttl = int32(rttl)
records := []pgo.Record{}
for _, e := range mastermap[zname][rrname][rtype] {
records = append(records, pgo.Record{Content: e.Target})

}
rrset.Records = records
zone.Rrsets = append(zone.Rrsets, rrset)
}

}

// Skip the empty zones (likely ones we don't control)
if len(zone.Rrsets) > 0 {
zonelist = append(zonelist, zone)
}

}

log.Debugf("Zone List generated from Endpoints: %+v", zonelist)

return zonelist, nil
}

func (p *PDNSProvider) MutateRecords(endpoints []*endpoint.Endpoint, changetype PDNSChangeType) error {
if zonelist, err := p.EndpointsToZones(endpoints, changetype); err != nil {
return err
} else {
for _, zone := range zonelist {
jso, _ := json.Marshal(zone)
log.Debugf("Struct for PatchZone:\n%s", string(jso))

resp, err := p.client.ZonesApi.PatchZone(p.auth_ctx, DEFAULT_SERVER_ID, zone.Id, zone)
if err != nil {
log.Debugf("PDNS API response: %s", printHTTPResponseBody(resp))
return err
}

}
}
return nil
}

// Records returns the list of records in a given hosted zone.
func (p *PDNSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {

zones, err := p.Zones()
if err != nil {
return nil, err
}

for _, zone := range zones {
z, _, err := p.client.ZonesApi.ListZone(p.auth_ctx, DEFAULT_SERVER_ID, zone.Id)
if err != nil {
log.Warnf("Unable to fetch data for %v. %v", zone.Id, err)
return nil, err
}

for _, rr := range z.Rrsets {
e, err := convertRRSetToEndpoints(rr)
if err != nil {
return nil, err
}
endpoints = append(endpoints, e...)
}
}

log.Debugf("Records fetched:\n%+v", endpoints)
return endpoints, nil
}

func (p *PDNSProvider) ApplyChanges(changes *plan.Changes) error {

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-existant records creates them
p.MutateRecords(changes.Create, PDNSReplace)
}

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 {
p.MutateRecords(changes.UpdateNew, PDNSReplace)
}

for _, change := range changes.Delete {
log.Debugf("DELETE: %+v", change)
}
if len(changes.Delete) > 0 {
p.MutateRecords(changes.Delete, PDNSDelete)
}
return nil
}

0 comments on commit fdedf6d

Please sign in to comment.