Unverified Commit b4a8c14c authored by Nick Jüttner's avatar Nick Jüttner Committed by GitHub
Browse files

Merge pull request #373 from ffledgling/363-pdns

Add PDNS as a Provider
parents e499a737 ec822d7b
master add-infoblox-maintainers bugfix/style-faq changelog-for-v0.7.3 cloudflare-ttl doc/aws-rbac-nodes eval-target-health go-1.12.7 gometalinter-timeout google-panic-assignment incubator-kep insensitive-compare ipv6 labeler linki-patch-2 linki-patch-3 linki-patch-4 linki-patch-6 njuettner-patch-1 njuettner/go_modules/github.com/akamai/AkamaiOPEN-edgegrid-golang-0.9.11 njuettner/go_modules/github.com/alecthomas/kingpin-2.2.6incompatible njuettner/go_modules/github.com/digitalocean/godo-1.34.0 njuettner/go_modules/github.com/pkg/errors-0.9.1 njuettner/go_modules/github.com/prometheus/client_golang-1.5.1 pagination-cloudflare-zones pagination-cloudflare-zones-patch plural-provider pr/531 pr/624 pr/674 pr/675 pr/697 pr/702 provider-specific provider-specific2 raffo/add-kustomize-base raffo/arm32v7 raffo/bump-cloudbuild-timeout raffo/bump-kustomize raffo/bump-kustomize-version-0.7.5 raffo/drop-the-changelog raffo/e2e-aws raffo/edit-infoblox-maintainers raffo/fix-1820 raffo/fix-1936 raffo/fix-ns-deletion raffo/fix-that-typo raffo/goarm raffo/gpr-docker-image raffo/kustomize-endpoints raffo/multiarch raffo/multiarch-docs raffo/new-ingress-resource raffo/new-maintainers raffo/provider-structure-refactor raffo/release-conventions raffo/release-script raffo/release-v0.7.2 raffo/remove-incubator-readme raffo/remove-masters raffo/split-sources raffo/use-actions raffo/v0.7.6 ratelimit revert-736-fix-domainfilter revert-963-ns1-provider-ammended sagor999/infoblox-multiple-A-records setup-semaphore stability-matrix test-things travis-test update-changelog v0.5.15 v0.5.17 v0.5.9-changelog v0.7.6 v0.7.5 v0.7.4 v0.7.3 v0.7.2 v0.7.1 v0.7.0 v0.6.0 v0.5.18 v0.5.17 v0.5.16 v0.5.15 v0.5.14 v0.5.13 v0.5.12 v0.5.11 v0.5.10 v0.5.9 v0.5.8 v0.5.7 v0.5.6 v0.5.5 v0.5.4 v0.5.3 v0.5.2 v0.5.1 v0.5.0 v0.5.0-alpha.3 v0.5.0-alpha.2
No related merge requests found
Showing with 991 additions and 1 deletion
+991 -1
......@@ -166,6 +166,12 @@
]
revision = "09691a3b6378b740595c1002f40c34dd5f218a22"
[[projects]]
branch = "master"
name = "github.com/ffledgling/pdns-go"
packages = ["."]
revision = "524e7daccd99651cdb56426eb15b7d61f9597a5c"
[[projects]]
name = "github.com/ghodss/yaml"
packages = ["."]
......
......@@ -136,6 +136,8 @@ func main() {
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
case "designate":
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
case "pdns":
p, err = provider.NewPDNSProvider(cfg.PDNSServer, cfg.PDNSAPIKey, domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}
......
......@@ -64,6 +64,8 @@ type Config struct {
DynPassword string
DynMinTTLSeconds int
InMemoryZones []string
PDNSServer string
PDNSAPIKey string
Policy string
Registry string
TXTOwnerID string
......@@ -100,6 +102,8 @@ var defaultConfig = &Config{
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
......@@ -159,7 +163,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, dyn, designate, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "desginate", "inmemory")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, inmemory, pdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "desginate", "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("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
......@@ -179,6 +183,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
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")
......
......@@ -50,6 +50,8 @@ var (
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
......@@ -84,6 +86,8 @@ var (
InfobloxWapiVersion: "2.6.1",
InfobloxSSLVerify: false,
InMemoryZones: []string{"example.org", "company.com"},
PDNSServer: "http://ns.example.com:8081",
PDNSAPIKey: "some-secret-key",
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
......@@ -135,6 +139,8 @@ func TestParseFlags(t *testing.T) {
"--infoblox-wapi-version=2.6.1",
"--inmemory-zone=example.org",
"--inmemory-zone=company.com",
"--pdns-server=http://ns.example.com:8081",
"--pdns-api-key=some-secret-key",
"--no-infoblox-ssl-verify",
"--domain-filter=example.org",
"--domain-filter=company.com",
......@@ -178,6 +184,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_POLICY": "upsert-only",
......
/*
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 provider
import (
"bytes"
"context"
"encoding/json"
"errors"
"math"
"net/http"
"sort"
"strings"
"time"
log "github.com/sirupsen/logrus"
pgo "github.com/ffledgling/pdns-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
type pdnsChangeType string
const (
apiBase = "/api/v1"
// Unless we use something like pdnsproxy (discontinued upsteam), 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
)
// 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)
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
}
// 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
}
// 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 {
client PDNSAPIProvider
}
// NewPDNSProvider initializes a new PowerDNS based Provider.
func NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error) {
// Do some input validation
if apikey == "" {
return nil, errors.New("Missing API Key for PDNS. Specify using --pdns-api-key=")
}
// The default for when no --domain-filter is passed is [""], instead of [], so we check accordingly.
if len(domainFilter.filters) != 1 && domainFilter.filters[0] != "" {
return nil, errors.New("PDNS Provider does not support domain filter")
}
// We do not support dry running, exit safely instead of surprising the user
// TODO: Add Dry Run support
if dryRun {
return nil, errors.New("PDNS Provider does not currently support dry-run")
}
if server == "localhost" {
log.Warnf("PDNS Server is set to localhost, this may not be what you want. Specify using --pdns-server=")
}
cfg := pgo.NewConfiguration()
cfg.Host = server
cfg.BasePath = server + apiBase
provider := &PDNSProvider{
client: &PDNSAPIClient{
dryRun: dryRun,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: apikey}),
client: pgo.NewAPIClient(cfg),
},
}
return provider, nil
}
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
endpoints = []*endpoint.Endpoint{}
for _, record := range rr.Records {
// If a record is "Disabled", it's not supposed to be "visible"
if !record.Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, record.Content, rr.Type_, endpoint.TTL(rr.Ttl)))
}
}
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
}
// 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(zones, func(i, j int) bool { return len(zones[i].Name) > len(zones[j].Name) })
// NOTE: Complexity of this loop is O(Zones*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 zones {
zone.Rrsets = []pgo.RrSet{}
for i := 0; i < len(endpoints); {
ep := endpoints[0]
dnsname := ensureTrailingDot(ep.DNSName)
if 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 {
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 sepecified 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)
}
}
// If we still have some endpoints left, it means we couldn't find a matching zone 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() (endpoints []*endpoint.Endpoint, _ error) {
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
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(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-existant 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
}
/*
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 provider
import (
"errors"
//"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
pgo "github.com/ffledgling/pdns-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
// FIXME: What do we do about labels?
var (
// Simple RRSets that contain 1 A record and 1 TXT record
RRSetSimpleARecord = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
},
}
RRSetSimpleTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false},
},
}
RRSetLongARecord = pgo.RrSet{
Name: "a.very.long.domainname.example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
},
}
RRSetLongTXTRecord = pgo.RrSet{
Name: "a.very.long.domainname.example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false},
},
}
// RRSet with one record disabled
RRSetDisabledRecord = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
{Content: "8.8.4.4", Disabled: true, SetPtr: false},
},
}
RRSetCNAMERecord = pgo.RrSet{
Name: "cname.example.com.",
Type_: "CNAME",
Ttl: 300,
Records: []pgo.Record{
{Content: "example.by.any.other.name.com", Disabled: false, SetPtr: false},
},
}
RRSetTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "'would smell as sweet'", Disabled: false, SetPtr: false},
},
}
// Multiple PDNS records in an RRSet of a single type
RRSetMultipleRecords = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
{Content: "8.8.4.4", Disabled: false, SetPtr: false},
{Content: "4.4.4.4", Disabled: false, SetPtr: false},
},
}
endpointsDisabledRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
}
endpointsSimpleRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "\"heritage=external-dns,external-dns/owner=tower-pdns\"", endpoint.RecordTypeTXT, endpoint.TTL(300)),
}
endpointsLongRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", "\"heritage=external-dns,external-dns/owner=tower-pdns\"", endpoint.RecordTypeTXT, endpoint.TTL(300)),
}
endpointsNonexistantZone = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("does.not.exist.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("does.not.exist.com", "\"heritage=external-dns,external-dns/owner=tower-pdns\"", endpoint.RecordTypeTXT, endpoint.TTL(300)),
}
endpointsMultipleRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "4.4.4.4", endpoint.RecordTypeA, endpoint.TTL(300)),
}
endpointsMixedRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("cname.example.com", "example.by.any.other.name.com", endpoint.RecordTypeCNAME, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "'would smell as sweet'", endpoint.RecordTypeTXT, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "4.4.4.4", endpoint.RecordTypeA, endpoint.TTL(300)),
}
endpointsMultipleZones = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("example.com", "\"heritage=external-dns,external-dns/owner=tower-pdns\"", endpoint.RecordTypeTXT, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("mock.test", "9.9.9.9", endpoint.RecordTypeA, endpoint.TTL(300)),
endpoint.NewEndpointWithTTL("mock.test", "\"heritage=external-dns,external-dns/owner=tower-pdns\"", endpoint.RecordTypeTXT, endpoint.TTL(300)),
}
ZoneEmpty = pgo.Zone{
// Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs.
Id: "example.com.",
// Name of the zone (e.g. “example.com.”) MUST have a trailing dot
Name: "example.com.",
// Set to “Zone”
Type_: "Zone",
// API endpoint for this zone
Url: "/api/v1/servers/localhost/zones/example.com.",
// Zone kind, one of “Native”, “Master”, “Slave”
Kind: "Native",
// RRSets in this zone
Rrsets: []pgo.RrSet{},
}
ZoneEmptyLong = pgo.Zone{
Id: "long.domainname.example.com.",
Name: "long.domainname.example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/long.domainname.example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{},
}
ZoneEmpty2 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{},
}
ZoneMixed = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords},
}
ZoneEmptyToSimplePatch = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToLongPatch = pgo.Zone{
Id: "long.domainname.example.com.",
Name: "long.domainname.example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/long.domainname.example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "a.very.long.domainname.example.com.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "a.very.long.domainname.example.com.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimplePatch2 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "mock.test.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "mock.test.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimpleDelete = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "example.com.",
Type_: "A",
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: "TXT",
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
)
/******************************************************************************/
// API that returns a zone with multiple record types
type PDNSAPIClientStub struct {
}
func (c *PDNSAPIClientStub) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneMixed}, nil, nil
}
func (c *PDNSAPIClientStub) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return ZoneMixed, nil, nil
}
func (c *PDNSAPIClientStub) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
return nil, nil
}
/******************************************************************************/
// API that returns a zones with no records
type PDNSAPIClientStubEmptyZones struct {
// Keep track of all zones we recieve via PatchZone
patchedZones []pgo.Zone
}
func (c *PDNSAPIClientStubEmptyZones) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2}, nil, nil
}
func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
if strings.Contains(zoneID, "example.com") {
return ZoneEmpty, nil, nil
} else if strings.Contains(zoneID, "mock.test") {
return ZoneEmpty2, nil, nil
} else if strings.Contains(zoneID, "long.domainname.example.com") {
return ZoneEmpty2, nil, nil
}
return pgo.Zone{}, nil, nil
}
func (c *PDNSAPIClientStubEmptyZones) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
c.patchedZones = append(c.patchedZones, zoneStruct)
return nil, nil
}
/******************************************************************************/
// API that returns error on PatchZone()
type PDNSAPIClientStubPatchZoneFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the PatchZone method to introduce a failure
func (c *PDNSAPIClientStubPatchZoneFailure) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
return nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
// API that returns error on ListZone()
type PDNSAPIClientStubListZoneFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the ListZone method to introduce a failure
func (c *PDNSAPIClientStubListZoneFailure) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return pgo.Zone{}, nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
// API that returns error on ListZones() (Zones - plural)
type PDNSAPIClientStubListZonesFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the ListZones method to introduce a failure
func (c *PDNSAPIClientStubListZonesFailure) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{}, nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
type NewPDNSProviderTestSuite struct {
suite.Suite
}
func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreate() {
// Function definition: NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error)
_, err := NewPDNSProvider("http://localhost:8081", "", NewDomainFilter([]string{""}), false)
assert.Error(suite.T(), err, "--pdns-api-key should be specified")
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{"example.com", "example.org"}), false)
assert.Error(suite.T(), err, "--domainfilter should raise an error")
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{""}), true)
assert.Error(suite.T(), err, "--dry-run should raise an error")
// This is our "regular" code path, no error should be thrown
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{""}), false)
assert.Nil(suite.T(), err, "Regular case should raise no error")
}
func (suite *NewPDNSProviderTestSuite) TestPDNSRRSetToEndpoints() {
// Function definition: convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStub{},
}
/* given an RRSet with three records, we test:
- We correctly create corresponding endpoints
*/
eps, err := p.convertRRSetToEndpoints(RRSetMultipleRecords)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsMultipleRecords, eps)
/* Given an RRSet with two records, one of which is disabled, we test:
- We can correctly convert the RRSet into a list of valid endpoints
- We correctly discard/ignore the disabled record.
*/
eps, err = p.convertRRSetToEndpoints(RRSetDisabledRecord)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsDisabledRecord, eps)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSRecords() {
// Function definition: Records() (endpoints []*endpoint.Endpoint, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStub{},
}
/* We test that endpoints are returned correctly for a Zone when Records() is called
*/
eps, err := p.Records()
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsMixedRecords, eps)
// Test failures are handled correctly
// Create a new provider to run tests against
p = &PDNSProvider{
client: &PDNSAPIClientStubListZoneFailure{},
}
eps, err = p.Records()
assert.NotNil(suite.T(), err)
p = &PDNSProvider{
client: &PDNSAPIClientStubListZonesFailure{},
}
eps, err = p.Records()
assert.NotNil(suite.T(), err)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() {
// Function definition: ConvertEndpointsToZones(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStubEmptyZones{},
}
// Check inserting endpoints from a single zone
zlist, err := p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check deleting endpoints from a single zone
zlist, err = p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsDelete)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimpleDelete}, zlist)
// Check endpoints from multiple zones
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch, ZoneEmptyToSimplePatch2}, zlist)
// Check endpoints from a zone that does not exist
zlist, err = p.ConvertEndpointsToZones(endpointsNonexistantZone, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{}, zlist)
// Check endpoints that match multiple zones (one longer than other), is assigned to the right zone
zlist, err = p.ConvertEndpointsToZones(endpointsLongRecord, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToLongPatch}, zlist)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
// Function definition: mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error
// Create a new provider to run tests against
c := &PDNSAPIClientStubEmptyZones{}
p := &PDNSProvider{
client: c,
}
// Check inserting endpoints from a single zone
err := p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("REPLACE"))
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, c.patchedZones)
// Reset the "patchedZones"
c.patchedZones = []pgo.Zone{}
// Check deleting endpoints from a single zone
err = p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("DELETE"))
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimpleDelete}, c.patchedZones)
// Check we fail correctly when patching fails for whatever reason
p = &PDNSProvider{
client: &PDNSAPIClientStubPatchZoneFailure{},
}
// Check inserting endpoints from a single zone
err = p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("REPLACE"))
assert.NotNil(suite.T(), err)
}
func TestNewPDNSProviderTestSuite(t *testing.T) {
suite.Run(t, new(NewPDNSProviderTestSuite))
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment