external-dns/provider/pdns/pdns_test.go
Seena Fallah fd3e82cf9b
feat: add PTR to registry mapper and pdns trailing-dot types (#6281)
Signed-off-by: Seena Fallah <seenafallah@gmail.com>
2026-03-17 19:19:40 +05:30

1462 lines
50 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 (
"context"
"net/http"
"regexp"
"strings"
"testing"
pgo "github.com/ffledgling/pdns-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/provider"
)
// 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_: endpoint.RecordTypeA,
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
},
}
RRSetSimpleTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
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_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
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_: endpoint.RecordTypeCNAME,
Ttl: 300,
Records: []pgo.Record{
{Content: "example.com.", Disabled: false, SetPtr: false},
},
}
RRSetALIASRecord = pgo.RrSet{
Name: "alias.example.com.",
Type_: "ALIAS",
Ttl: 300,
Records: []pgo.Record{
{Content: "example.by.any.other.name.com.", Disabled: false, SetPtr: false},
},
}
RRSetTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
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},
},
}
// RRSet with MX record
RRSetMXRecord = pgo.RrSet{
Name: "example.com.",
Type_: endpoint.RecordTypeMX,
Ttl: 300,
Records: []pgo.Record{
{Content: "10 mailhost1.example.com", Disabled: false, SetPtr: false},
{Content: "10 mailhost2.example.com", Disabled: false, SetPtr: false},
},
}
// RRSet with SRV record
RRSetSRVRecord = pgo.RrSet{
Name: "_service._tls.example.com.",
Type_: endpoint.RecordTypeSRV,
Ttl: 300,
Records: []pgo.Record{
{Content: "100 1 443 service.example.com", Disabled: false, SetPtr: false},
},
}
// RRSet with NS record
RRSetNSRecord = pgo.RrSet{
Name: "sub.example.com.",
Type_: endpoint.RecordTypeNS,
Ttl: 300,
Records: []pgo.Record{
{Content: "ns1.example.com", Disabled: false, SetPtr: false},
{Content: "ns2.example.com", Disabled: false, SetPtr: false},
},
}
// RRSet with PTR record
RRSetPTRRecord = pgo.RrSet{
Name: "4.3.2.1.in-addr.arpa.",
Type_: endpoint.RecordTypePTR,
Ttl: 300,
Records: []pgo.Record{
{Content: "host.example.com", Disabled: false, SetPtr: false},
},
}
endpointsDisabledRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
}
endpointsSimpleRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsLongRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsNonexistantZone = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("does.not.exist.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("does.not.exist.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8", "8.8.4.4", "4.4.4.4"),
}
endpointsMXRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 example.com"),
}
endpointsMXRecordInvalidFormatTooManyArgs = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 example.com abc"),
}
endpointsMultipleMXRecordsWithSingleInvalid = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "abc example.com"),
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "20 backup.example.com"),
}
endpointsMultipleInvalidMXRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "example.com"),
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "backup.example.com"),
}
endpointsMixedRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.com"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "'would smell as sweet'"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8", "8.8.4.4", "4.4.4.4"),
endpoint.NewEndpointWithTTL("alias.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 mailhost1.example.com", "10 mailhost2.example.com"),
endpoint.NewEndpointWithTTL("_service._tls.example.com", endpoint.RecordTypeSRV, endpoint.TTL(300), "100 1 443 service.example.com"),
endpoint.NewEndpointWithTTL("sub.example.com", endpoint.RecordTypeNS, endpoint.TTL(300), "ns1.example.com", "ns2.example.com"),
endpoint.NewEndpointWithTTL("4.3.2.1.in-addr.arpa", endpoint.RecordTypePTR, endpoint.TTL(300), "host.example.com"),
}
endpointsMultipleZones = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("mock.test", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("mock.test", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZones2 = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("abcd.mock.test", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("abcd.mock.test", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZonesWithNoExist = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZonesWithLongRecordNotInDomainFilter = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZonesWithSimilarRecordNotInDomainFilter = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("test.simexample.com", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("test.simexample.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsApexRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"),
}
// Endpoint with alias annotation
endpointWithAliasAnnotation = endpoint.NewEndpointWithTTL("sub.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "target.example.com").WithProviderSpecific("alias", "true")
// Endpoints for preferAlias test
endpointsPreferAlias = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("sub.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "target.example.com"),
}
ZoneEmptyToPreferAliasPatch = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "sub.example.com.",
Type_: "ALIAS",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "target.example.com.",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToCNAMEPatch = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "sub.example.com.",
Type_: endpoint.RecordTypeCNAME,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "target.example.com.",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
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{},
}
ZoneEmptySimilar = pgo.Zone{
Id: "simexample.com.",
Name: "simexample.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/simexample.com.",
Kind: "Native",
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, RRSetALIASRecord, RRSetMXRecord, RRSetSRVRecord, RRSetNSRecord, RRSetPTRRecord},
}
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_: endpoint.RecordTypeA,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimplePatchLongRecordIgnoredInDomainFilter = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "a.very.long.domainname.example.com.",
Type_: endpoint.RecordTypeA,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "a.very.long.domainname.example.com.",
Type_: endpoint.RecordTypeTXT,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: endpoint.RecordTypeA,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
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_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "mock.test.",
Type_: endpoint.RecordTypeTXT,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimplePatch3 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "abcd.mock.test.",
Type_: endpoint.RecordTypeA,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "abcd.mock.test.",
Type_: endpoint.RecordTypeTXT,
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_: endpoint.RecordTypeA,
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToApexPatch = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "cname.example.com.",
Type_: endpoint.RecordTypeCNAME,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "example.by.any.other.name.com.",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "cname.example.com.",
Type_: endpoint.RecordTypeTXT,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: "ALIAS",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "example.by.any.other.name.com.",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: endpoint.RecordTypeTXT,
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
DomainFilterListSingle = endpoint.NewDomainFilter([]string{"example.com"})
DomainFilterListMultiple = endpoint.NewDomainFilter([]string{"example.com", "mock.com"})
DomainFilterListEmpty = endpoint.NewDomainFilter([]string{})
RegexDomainFilter = endpoint.NewRegexDomainFilter(regexp.MustCompile("example.com"), 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(_ string) (pgo.Zone, *http.Response, error) {
return ZoneMixed, nil, nil
}
func (c *PDNSAPIClientStub) PatchZone(_ string, _ pgo.Zone) (*http.Response, error) {
return &http.Response{}, nil
}
/******************************************************************************/
// API that returns a zones with no records
type PDNSAPIClientStubEmptyZones struct {
// Keep track of all zones we receive 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) {
switch {
case strings.Contains(zoneID, "example.com"):
return ZoneEmpty, nil, nil
case strings.Contains(zoneID, "mock.test"):
return ZoneEmpty2, nil, nil
case strings.Contains(zoneID, "long.domainname.example.com"):
return ZoneEmptyLong, nil, nil
}
return pgo.Zone{}, nil, nil
}
func (c *PDNSAPIClientStubEmptyZones) PatchZone(_ string, zoneStruct pgo.Zone) (*http.Response, error) {
c.patchedZones = append(c.patchedZones, zoneStruct)
return &http.Response{}, 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(_ string, _ pgo.Zone) (*http.Response, error) {
return nil, provider.NewSoftErrorf("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(_ string) (pgo.Zone, *http.Response, error) {
return pgo.Zone{}, nil, provider.NewSoftErrorf("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, provider.NewSoftErrorf("Generic PDNS Error")
}
/******************************************************************************/
// API that returns zone partitions given DomainFilter(s)
type PDNSAPIClientStubPartitionZones struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
func (c *PDNSAPIClientStubPartitionZones) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneEmpty, ZoneEmpty2, ZoneEmptySimilar}, nil, nil
}
func (c *PDNSAPIClientStubPartitionZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
switch {
case strings.Contains(zoneID, "example.com"):
return ZoneEmpty, nil, nil
case strings.Contains(zoneID, "mock.test"):
return ZoneEmpty2, nil, nil
case strings.Contains(zoneID, "simexample.com"):
return ZoneEmptySimilar, nil, nil
}
return pgo.Zone{}, nil, nil
}
/******************************************************************************/
// Configurable API stub that performs real domain-filter partitioning.
// Use it to test the intersection logic between ListZones results and the
// provider's domain filter.
type PDNSAPIClientStubConfigurable struct {
zones []pgo.Zone
listErr error
}
func (c *PDNSAPIClientStubConfigurable) ListZones() ([]pgo.Zone, *http.Response, error) {
if c.listErr != nil {
return nil, nil, c.listErr
}
return c.zones, nil, nil
}
func (c *PDNSAPIClientStubConfigurable) ListZone(_ string) (pgo.Zone, *http.Response, error) {
return pgo.Zone{}, nil, nil
}
func (c *PDNSAPIClientStubConfigurable) PatchZone(_ string, _ pgo.Zone) (*http.Response, error) {
return &http.Response{}, nil
}
/******************************************************************************/
type NewPDNSProviderTestSuite struct {
suite.Suite
}
func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreate() {
_, err := newProvider(
context.Background(),
PDNSConfig{
Server: "http://localhost:8081",
DomainFilter: endpoint.NewDomainFilter([]string{""}),
})
suite.Error(err, "--pdns-api-key should be specified")
_, err = newProvider(
context.Background(),
PDNSConfig{
Server: "http://localhost:8081",
APIKey: "foo",
DomainFilter: endpoint.NewDomainFilter([]string{"example.com", "example.org"}),
})
suite.NoError(err, "--domain-filter should raise no error")
_, err = newProvider(
context.Background(),
PDNSConfig{
Server: "http://localhost:8081",
APIKey: "foo",
DomainFilter: endpoint.NewDomainFilter([]string{""}),
DryRun: true,
})
suite.Error(err, "--dry-run should raise an error")
// This is our "regular" code path, no error should be thrown
_, err = newProvider(
context.Background(),
PDNSConfig{
Server: "http://localhost:8081",
APIKey: "foo",
DomainFilter: endpoint.NewDomainFilter([]string{""}),
})
suite.NoError(err, "Regular case should raise no error")
}
func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() {
newProvider := func(TLSConfig TLSConfig) error {
_, err := newProvider(
context.Background(),
PDNSConfig{APIKey: "foo", TLSConfig: TLSConfig})
return err
}
suite.NoError(newProvider(TLSConfig{SkipTLSVerify: true}), "Disabled TLS Config should raise no error")
suite.NoError(newProvider(TLSConfig{
SkipTLSVerify: true,
CAFilePath: "../../internal/testresources/ca.pem",
ClientCertFilePath: "../../internal/testresources/client-cert.pem",
ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem",
}), "Disabled TLS Config with additional flags should raise no error")
suite.NoError(newProvider(TLSConfig{}), "Enabled TLS Config without --tls-ca should raise no error")
suite.NoError(newProvider(TLSConfig{
CAFilePath: "../../internal/testresources/ca.pem",
}), "Enabled TLS Config with --tls-ca should raise no error")
suite.Error(newProvider(TLSConfig{
CAFilePath: "../../internal/testresources/ca.pem",
ClientCertFilePath: "../../internal/testresources/client-cert.pem",
}), "Enabled TLS Config with --tls-client-cert only should raise an error")
suite.Error(newProvider(TLSConfig{
CAFilePath: "../../internal/testresources/ca.pem",
ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem",
}), "Enabled TLS Config with --tls-client-cert-key only should raise an error")
suite.NoError(newProvider(TLSConfig{
CAFilePath: "../../internal/testresources/ca.pem",
ClientCertFilePath: "../../internal/testresources/client-cert.pem",
ClientCertKeyFilePath: "../../internal/testresources/client-cert-key.pem",
}), "Enabled TLS Config with all flags should raise no error")
}
func (suite *NewPDNSProviderTestSuite) TestPDNSHasAliasAnnotation() {
p := &PDNSProvider{}
// Test endpoint without alias annotation
epWithoutAlias := endpoint.NewEndpoint("test.example.com", endpoint.RecordTypeCNAME, "target.example.com")
suite.False(p.hasAliasAnnotation(epWithoutAlias))
// Test endpoint with alias=false
epWithAliasFalse := endpoint.NewEndpoint("test.example.com", endpoint.RecordTypeCNAME, "target.example.com")
epWithAliasFalse.ProviderSpecific = endpoint.ProviderSpecific{
{Name: "alias", Value: "false"},
}
suite.False(p.hasAliasAnnotation(epWithAliasFalse))
// Test endpoint with alias=true
epWithAliasTrue := endpoint.NewEndpoint("test.example.com", endpoint.RecordTypeCNAME, "target.example.com")
epWithAliasTrue.ProviderSpecific = endpoint.ProviderSpecific{
{Name: "alias", Value: "true"},
}
suite.True(p.hasAliasAnnotation(epWithAliasTrue))
// Test endpoint with other provider specific but no alias
epWithOtherPS := endpoint.NewEndpoint("test.example.com", endpoint.RecordTypeCNAME, "target.example.com")
epWithOtherPS.ProviderSpecific = endpoint.ProviderSpecific{
{Name: "other", Value: "value"},
}
suite.False(p.hasAliasAnnotation(epWithOtherPS))
}
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 := p.convertRRSetToEndpoints(RRSetMultipleRecords)
suite.Equal(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 = p.convertRRSetToEndpoints(RRSetDisabledRecord)
suite.Equal(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{},
}
ctx := context.Background()
/* We test that endpoints are returned correctly for a Zone when Records() is called
*/
eps, err := p.Records(ctx)
suite.Require().NoError(err)
suite.Equal(endpointsMixedRecords, eps)
// Test failures are handled correctly
// Create a new provider to run tests against
p = &PDNSProvider{
client: &PDNSAPIClientStubListZoneFailure{},
}
_, err = p.Records(ctx)
suite.Error(err)
suite.ErrorIs(err, provider.SoftError)
p = &PDNSProvider{
client: &PDNSAPIClientStubListZonesFailure{},
}
_, err = p.Records(ctx)
suite.Error(err)
suite.ErrorIs(err, provider.SoftError)
}
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)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check deleting endpoints from a single zone
zlist, err = p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsDelete)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimpleDelete}, zlist)
// Check endpoints from multiple zones #1
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch, ZoneEmptyToSimplePatch2}, zlist)
// Check endpoints from multiple zones #2
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones2, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch, ZoneEmptyToSimplePatch3}, zlist)
// Check endpoints from multiple zones where some endpoints which don't exist
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithNoExist, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check endpoints from a zone that does not exist
zlist, err = p.ConvertEndpointsToZones(endpointsNonexistantZone, PdnsReplace)
suite.NoError(err)
suite.Equal([]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)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToLongPatch}, zlist)
// Check endpoints of type CNAME, ALIAS, MX, SRV, and NS always have their values end with a trailing dot.
zlist, err = p.ConvertEndpointsToZones(endpointsMixedRecords, PdnsReplace)
suite.NoError(err)
trailingTypes := map[string]bool{
endpoint.RecordTypeCNAME: true,
"ALIAS": true,
endpoint.RecordTypeMX: true,
endpoint.RecordTypeSRV: true,
endpoint.RecordTypeNS: true,
endpoint.RecordTypePTR: true,
}
for _, z := range zlist {
for _, rs := range z.Rrsets {
if trailingTypes[rs.Type_] {
for _, r := range rs.Records {
suite.Equal(uint8(0x2e), r.Content[len(r.Content)-1])
}
}
}
}
// Check endpoints of type CNAME are converted to ALIAS on the domain apex
zlist, err = p.ConvertEndpointsToZones(endpointsApexRecords, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToApexPatch}, zlist)
// Check endpoints of type CNAME remain CNAME when no alias annotation is set
zlist, err = p.ConvertEndpointsToZones(endpointsPreferAlias, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToCNAMEPatch}, zlist)
// Check endpoints with alias annotation are converted to ALIAS
// Note: The --prefer-alias flag now works via PostProcessor wrapper which sets the alias annotation
zlist, err = p.ConvertEndpointsToZones([]*endpoint.Endpoint{endpointWithAliasAnnotation}, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToPreferAliasPatch}, zlist)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZonesPartitionZones() {
// Test DomainFilters
p := &PDNSProvider{
client: &PDNSAPIClientStubPartitionZones{},
domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
// Check inserting endpoints from a single zone which is specified in DomainFilter
zlist, err := p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsReplace)
suite.Require().NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check deleting endpoints from a single zone which is specified in DomainFilter
zlist, err = p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsDelete)
suite.Require().NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimpleDelete}, zlist)
// Check endpoints from multiple zones # which one is specified in DomainFilter and one is not
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check endpoints from multiple zones where some endpoints which don't exist and one that does
// and is part of DomainFilter
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithNoExist, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check endpoints from a zone that does not exist
zlist, err = p.ConvertEndpointsToZones(endpointsNonexistantZone, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{}, zlist)
// Check endpoints that match multiple zones (one longer than other), is assigned to the right zone when the longer
// zone is not part of the DomainFilter
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithLongRecordNotInDomainFilter, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatchLongRecordIgnoredInDomainFilter}, zlist)
// Check endpoints that match multiple zones (one longer than other and one is very similar)
// is assigned to the right zone when the similar zone is not part of the DomainFilter
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithSimilarRecordNotInDomainFilter, PdnsReplace)
suite.NoError(err)
suite.Equal([]pgo.Zone{ZoneEmptyToSimplePatch}, 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"))
suite.NoError(err)
suite.Equal([]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"))
suite.NoError(err)
suite.Equal([]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"))
suite.Error(err)
suite.ErrorIs(err, provider.SoftError)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSClientPartitionZones() {
zoneList := []pgo.Zone{
ZoneEmpty,
ZoneEmpty2,
}
partitionResultFilteredEmptyFilter := []pgo.Zone{
ZoneEmpty,
ZoneEmpty2,
}
partitionResultResidualEmptyFilter := ([]pgo.Zone)(nil)
partitionResultFilteredSingleFilter := []pgo.Zone{
ZoneEmpty,
}
partitionResultResidualSingleFilter := []pgo.Zone{
ZoneEmpty2,
}
partitionResultFilteredMultipleFilter := []pgo.Zone{
ZoneEmpty,
}
partitionResultResidualMultipleFilter := []pgo.Zone{
ZoneEmpty2,
}
// Check filtered, residual zones when no domain filter specified
filteredZones, residualZones := partitionZones(zoneList, DomainFilterListEmpty)
suite.Equal(partitionResultFilteredEmptyFilter, filteredZones)
suite.Equal(partitionResultResidualEmptyFilter, residualZones)
// Check filtered, residual zones when a single domain filter specified
filteredZones, residualZones = partitionZones(zoneList, DomainFilterListSingle)
suite.Equal(partitionResultFilteredSingleFilter, filteredZones)
suite.Equal(partitionResultResidualSingleFilter, residualZones)
// Check filtered, residual zones when a multiple domain filter specified
filteredZones, residualZones = partitionZones(zoneList, DomainFilterListMultiple)
suite.Equal(partitionResultFilteredMultipleFilter, filteredZones)
suite.Equal(partitionResultResidualMultipleFilter, residualZones)
filteredZones, residualZones = partitionZones(zoneList, RegexDomainFilter)
suite.Equal(partitionResultFilteredSingleFilter, filteredZones)
suite.Equal(partitionResultResidualSingleFilter, residualZones)
}
// Validate whether invalid endpoints are removed by AdjustEndpoints
func (suite *NewPDNSProviderTestSuite) TestPDNSAdjustEndpoints() {
// Function definition: AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
// Create a new provider to run tests against
p := &PDNSProvider{}
tests := []struct {
description string
endpoints []*endpoint.Endpoint
expected []*endpoint.Endpoint
}{
{
description: "Valid MX endpoint is not removed",
endpoints: endpointsMXRecord,
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "10 example.com"),
},
},
{
description: "Invalid MX endpoint with too many arguments is removed",
endpoints: endpointsMXRecordInvalidFormatTooManyArgs,
expected: []*endpoint.Endpoint([]*endpoint.Endpoint(nil)),
},
{
description: "Invalid MX endpoint is removed among valid endpoints",
endpoints: endpointsMultipleMXRecordsWithSingleInvalid,
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(300), "20 backup.example.com"),
},
},
{
description: "Multiple invalid MX endpoints are removed",
endpoints: endpointsMultipleInvalidMXRecords,
expected: []*endpoint.Endpoint([]*endpoint.Endpoint(nil)),
},
}
for _, tt := range tests {
actual, err := p.AdjustEndpoints(tt.endpoints)
suite.NoError(err)
suite.Equal(tt.expected, actual)
}
}
func (suite *NewPDNSProviderTestSuite) TestPDNSGetDomainFilter() {
allZones := []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2} // example.com., long.domainname.example.com., mock.test.
tests := []struct {
name string
client PDNSAPIProvider
domainFilter *endpoint.DomainFilter
// domains we expect the returned filter to match
shouldMatch []string
// domains we expect the returned filter NOT to match
shouldNotMatch []string
}{
{
name: "no domain filter — all zones from API are in scope",
client: &PDNSAPIClientStubConfigurable{
zones: allZones,
},
domainFilter: nil,
shouldMatch: []string{"example.com", "long.domainname.example.com", "mock.test", "sub.example.com", "sub.mock.test"},
shouldNotMatch: []string{"other.com"},
},
{
name: "domain filter set — all API zones still returned (controller handles intersection with --domain-filter)",
client: &PDNSAPIClientStubConfigurable{
zones: allZones,
},
domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
// GetDomainFilter returns all API zones, not the filtered subset;
// the controller intersects with --domain-filter on its own
shouldMatch: []string{"example.com", "long.domainname.example.com", "mock.test", "sub.example.com", "sub.mock.test"},
shouldNotMatch: []string{"other.com"},
},
{
name: "domain filter excludes all API zones — all zones still returned (no silent fail-open)",
client: &PDNSAPIClientStubConfigurable{
zones: allZones,
},
domainFilter: endpoint.NewDomainFilter([]string{"notexist.org"}),
// All provider-managed zones are returned; when the controller
// intersects with --domain-filter=notexist.org, nothing matches
// and the plan is safely empty
shouldMatch: []string{"example.com", "mock.test", "long.domainname.example.com"},
shouldNotMatch: []string{"notexist.org", "other.com"},
},
{
name: "ListZones error — returns empty filter (fail-open)",
client: &PDNSAPIClientStubConfigurable{
listErr: provider.NewSoftErrorf("API unreachable"),
},
domainFilter: nil,
// empty DomainFilter matches everything
shouldMatch: []string{"anything.com", "example.com"},
shouldNotMatch: []string{},
},
{
name: "API returns single zone — that zone is returned regardless of domain filter",
client: &PDNSAPIClientStubConfigurable{
zones: []pgo.Zone{ZoneEmpty}, // only example.com.
},
domainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
shouldMatch: []string{"example.com", "sub.example.com"},
shouldNotMatch: []string{"mock.test", "other.com"},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
p := &PDNSProvider{
client: tt.client,
domainFilter: tt.domainFilter,
}
df := p.GetDomainFilter()
for _, domain := range tt.shouldMatch {
suite.True(df.Match(domain), "expected filter to match %q", domain)
}
for _, domain := range tt.shouldNotMatch {
suite.False(df.Match(domain), "expected filter NOT to match %q", domain)
}
})
}
}
func TestNewPDNSProviderTestSuite(t *testing.T) {
suite.Run(t, new(NewPDNSProviderTestSuite))
}
// TestPDNSPartitionZonesRegexBehavior compares two regex forms for --domain-filter
// and shows how the choice of regex affects zone partitioning correctness.
func TestPDNSPartitionZonesRegexBehavior(t *testing.T) {
newZone := func(name string) pgo.Zone {
return pgo.Zone{Id: name, Name: name, Type_: "Zone", Kind: "Native", Rrsets: []pgo.RrSet{}}
}
zoneNames := func(zz []pgo.Zone) []string {
names := make([]string, len(zz))
for i, z := range zz {
names[i] = z.Name
}
return names
}
tests := []struct {
name string
zones []pgo.Zone
regex string
regexExclude string
assertions func(t *testing.T, filtered []pgo.Zone, residual []pgo.Zone)
}{
{
// Worst case: no subdomain zone exists at all.
// Both apex zones fail the regex → filtered is empty →
// ConvertEndpointsToZones logs "Ignoring Endpoint" for every record.
//
// "example.com" → no label prefix → residual ← BUG
// "other.com" → no match at all → residual ← BUG
name: "complete wipeout: subdomain-only regex with only apex zones leaves filtered empty",
zones: []pgo.Zone{
newZone("example.com."),
newZone("other.com."),
},
regex: `^[\w-]+\.example\.com$`,
assertions: func(t *testing.T, filtered []pgo.Zone, residual []pgo.Zone) {
assert.Empty(t, filtered,
"no zone matches the subdomain-only regex — every record will be ignored")
assert.ElementsMatch(t, []string{"example.com.", "other.com."}, zoneNames(residual),
"both zones land in residual: records in example.com. silently dropped")
},
},
{
// Partial match: a sub-zone happens to exist, so sub.example.com. is
// managed but the apex example.com. and the deep zone are still lost.
//
// "example.com" → no label at all → residual ← BUG
// "sub.example.com" → one label "sub" → filtered
// "long.domainname.example.com" → [\w-]+ can't span dots → residual ← BUG
// "simexample.com" → no .example.com suffix → residual ✓
// "mock.test" → no match → residual ✓
name: "partial match: subdomain-only regex misses apex and multi-label zones",
zones: []pgo.Zone{
newZone("example.com."),
newZone("sub.example.com."),
newZone("long.domainname.example.com."),
newZone("simexample.com."),
newZone("mock.test."),
},
regex: `^[\w-]+\.example\.com$`,
assertions: func(t *testing.T, filtered []pgo.Zone, residual []pgo.Zone) {
assert.Equal(t, []string{"sub.example.com."}, zoneNames(filtered),
"only the single-label subdomain zone matches")
assert.Contains(t, zoneNames(residual), "example.com.",
"zone apex lands in residual: its records would be ignored")
assert.Contains(t, zoneNames(residual), "long.domainname.example.com.",
"multi-label zone lands in residual: [\\w-]+ cannot span dots")
assert.Contains(t, zoneNames(residual), "simexample.com.")
assert.Contains(t, zoneNames(residual), "mock.test.")
},
},
{
// Exclusion regex takes priority: zones matching regexExclusion are
// rejected before the inclusion regex is checked.
//
// "example.com" → inclusion matches, exclusion does not → filtered ✓
// "staging.example.com" → inclusion matches, exclusion matches → residual ✓
// "prod.example.com" → inclusion matches, exclusion does not → filtered ✓
// "mock.test" → inclusion does not match → residual ✓
name: "exclusion regex overrides inclusion: staging zones are excluded",
regexExclude: `^staging\.`,
zones: []pgo.Zone{
newZone("example.com."),
newZone("staging.example.com."),
newZone("prod.example.com."),
newZone("mock.test."),
},
regex: `^([\w-]+\.)*example\.com$`,
assertions: func(t *testing.T, filtered []pgo.Zone, residual []pgo.Zone) {
assert.ElementsMatch(t, []string{"example.com.", "prod.example.com."}, zoneNames(filtered),
"only non-excluded example.com zones must be filtered")
assert.ElementsMatch(t, []string{"staging.example.com.", "mock.test."}, zoneNames(residual),
"staging zone is excluded by regexExclusion; mock.test does not match inclusion")
},
},
{
// ([\w-]+\.)* with zero repetitions matches the apex; one or more
// repetitions match subdomain zones at any depth.
// Suffix similarity (simexample.com) is rejected by the dot-boundary.
//
// "example.com" → 0 repetitions → filtered ✓
// "sub.example.com" → 1 repetition "sub." → filtered ✓
// "long.domainname.example.com" → 2 repetitions → filtered ✓
// "simexample.com" → no dot-boundary match → residual ✓
// "mock.test" → no match → residual ✓
name: "zone-aware regex (* quantifier) matches apex and all subdomain depths",
zones: []pgo.Zone{
newZone("example.com."),
newZone("sub.example.com."),
newZone("long.domainname.example.com."),
newZone("simexample.com."),
newZone("mock.test."),
},
regex: `^([\w-]+\.)*example\.com$`,
assertions: func(t *testing.T, filtered []pgo.Zone, residual []pgo.Zone) {
assert.ElementsMatch(t,
[]string{"example.com.", "sub.example.com.", "long.domainname.example.com."},
zoneNames(filtered),
"apex and all subdomain zones must be filtered")
assert.ElementsMatch(t, []string{"simexample.com.", "mock.test."}, zoneNames(residual),
"only truly unrelated zones must be residual")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var exclusion *regexp.Regexp
if tt.regexExclude != "" {
exclusion = regexp.MustCompile(tt.regexExclude)
}
df := endpoint.NewRegexDomainFilter(regexp.MustCompile(tt.regex), exclusion)
filtered, residual := partitionZones(tt.zones, df)
tt.assertions(t, filtered, residual)
})
}
}