mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 05:36:27 +08:00
440 lines
10 KiB
Go
440 lines
10 KiB
Go
package diff2
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/js"
|
|
"github.com/miekg/dns"
|
|
testifyrequire "github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// parseZoneContents is copied verbatim from providers/bind/bindProvider.go
|
|
// because import cycles and... tests shouldn't depend on huge modules.
|
|
func parseZoneContents(content string, zoneName string, zonefileName string) (models.Records, error) {
|
|
zp := dns.NewZoneParser(strings.NewReader(content), zoneName, zonefileName)
|
|
|
|
foundRecords := models.Records{}
|
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
|
rec, err := models.RRtoRCTxtBug(rr, zoneName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
foundRecords = append(foundRecords, &rec)
|
|
}
|
|
|
|
if err := zp.Err(); err != nil {
|
|
return nil, fmt.Errorf("error while parsing '%v': %w", zonefileName, err)
|
|
}
|
|
return foundRecords, nil
|
|
}
|
|
|
|
func showRecs(recs models.Records) string {
|
|
result := ""
|
|
for _, rec := range recs {
|
|
result += (rec.GetLabel() +
|
|
" " + rec.Type +
|
|
" " + rec.GetTargetCombined() +
|
|
"\n")
|
|
}
|
|
return result
|
|
}
|
|
|
|
func handsoffHelper(t *testing.T, existingZone, desiredJs string, noPurge bool, resultWanted string) {
|
|
t.Helper()
|
|
|
|
existing, err := parseZoneContents(existingZone, "f.com", "no_file_name")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dnsconfig, err := js.ExecuteJavascriptString([]byte(desiredJs), false, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
dc := dnsconfig.FindDomain("f.com")
|
|
desired := dc.Records
|
|
absences := dc.EnsureAbsent
|
|
unmanagedConfigs := dc.Unmanaged
|
|
// BUG(tlim): For some reason ExecuteJavascriptString() isn't setting the NameFQDN on records.
|
|
// This fixes up the records. It is a crass workaround. We should find the real
|
|
// cause and fix it.
|
|
for i, j := range desired {
|
|
desired[i].SetLabel(j.GetLabel(), "f.com")
|
|
}
|
|
for i, j := range absences {
|
|
absences[i].SetLabel(j.GetLabel(), "f.com")
|
|
}
|
|
|
|
ignored, purged, err := processIgnoreAndNoPurge(
|
|
"f.com",
|
|
existing, desired,
|
|
absences,
|
|
unmanagedConfigs,
|
|
noPurge,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ignoredRecs := showRecs(ignored)
|
|
purgedRecs := showRecs(purged)
|
|
resultActual := "IGNORED:\n" + ignoredRecs + "FOREIGN:\n" + purgedRecs
|
|
resultWanted = strings.TrimSpace(resultWanted) + "\n"
|
|
resultActual = strings.TrimSpace(resultActual) + "\n"
|
|
|
|
existingTxt := showRecs(existing)
|
|
desiredTxt := showRecs(desired)
|
|
debugTxt := "EXISTING:\n" + existingTxt + "DESIRED:\n" + desiredTxt
|
|
|
|
if resultWanted != resultActual {
|
|
testifyrequire.Equal(t,
|
|
resultWanted,
|
|
resultActual,
|
|
"GOT =\n```\n%s```\nWANT=\n```%s```\nINPUTS=\n```\n%s\n```\n",
|
|
resultActual,
|
|
resultWanted,
|
|
debugTxt)
|
|
}
|
|
}
|
|
|
|
func Test_purge_empty(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
func Test_purge_1(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
foo3 IN A 2.2.2.2
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
func Test_nopurge_1(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
foo3 IN A 3.3.3.3
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, true, `
|
|
IGNORED:
|
|
FOREIGN:
|
|
foo3 A 3.3.3.3
|
|
`)
|
|
}
|
|
|
|
func Test_absent_1(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
foo3 IN A 3.3.3.3
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
A("foo3", "3.3.3.3", ENSURE_ABSENT_REC()),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
func Test_ignore_lab(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
foo3 IN A 3.3.3.3
|
|
foo3 IN MX 10 mymx.example.com.
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
IGNORE_NAME("foo3"),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
foo3 A 3.3.3.3
|
|
foo3 MX 10 mymx.example.com.
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
func Test_ignore_labAndType(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
foo3 IN A 3.3.3.3
|
|
foo3 IN MX 10 mymx.example.com.
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
A("foo3", "3.3.3.3"),
|
|
IGNORE_NAME("foo3", "MX"),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
foo3 MX 10 mymx.example.com.
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
func Test_ignore_target(t *testing.T) {
|
|
existingZone := `
|
|
foo1 IN A 1.1.1.1
|
|
foo2 IN A 2.2.2.2
|
|
_2222222222222222.cr IN CNAME _333333.nnn.acm-validations.aws.
|
|
`
|
|
desiredJs := `
|
|
D("f.com", "none",
|
|
A("foo1", "1.1.1.1"),
|
|
A("foo2", "2.2.2.2"),
|
|
MX("foo3", 10, "mymx.example.com."),
|
|
IGNORE_TARGET('**.acm-validations.aws.', 'CNAME'),
|
|
{})
|
|
`
|
|
handsoffHelper(t, existingZone, desiredJs, false, `
|
|
IGNORED:
|
|
_2222222222222222.cr CNAME _333333.nnn.acm-validations.aws.
|
|
FOREIGN:
|
|
`)
|
|
}
|
|
|
|
// Test_ignore_external_dns tests the IGNORE_EXTERNAL_DNS feature
|
|
// using the full handsoff() function.
|
|
func Test_ignore_external_dns(t *testing.T) {
|
|
domain := "f.com"
|
|
|
|
// Existing zone has external-dns managed records
|
|
existing := models.Records{
|
|
// External-dns TXT ownership record
|
|
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
|
// The A record managed by external-dns
|
|
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
|
// Static record not managed by external-dns
|
|
makeTestRecord("static", "A", "1.2.3.4", domain),
|
|
// Another external-dns managed record
|
|
makeTestRecord("cname-api", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
|
makeTestRecord("api", "CNAME", "myapp.f.com.", domain),
|
|
}
|
|
|
|
// Desired only has the static record
|
|
desired := models.Records{
|
|
makeTestRecord("static", "A", "1.2.3.4", domain),
|
|
}
|
|
|
|
// Call handsoff with IGNORE_EXTERNAL_DNS enabled
|
|
result, msgs, err := handsoff(
|
|
domain,
|
|
existing,
|
|
desired,
|
|
nil, // absences
|
|
nil, // unmanagedConfigs
|
|
false, // unmanagedSafely
|
|
false, // noPurge
|
|
true, // ignoreExternalDNS
|
|
"", // externalDNSPrefix (empty = default)
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check that external-dns records are in the result (so they won't be deleted)
|
|
foundMyappA := false
|
|
foundMyappTXT := false
|
|
foundApiCNAME := false
|
|
foundApiTXT := false
|
|
foundStatic := false
|
|
|
|
for _, rec := range result {
|
|
switch {
|
|
case rec.GetLabel() == "myapp" && rec.Type == "A":
|
|
foundMyappA = true
|
|
case rec.GetLabel() == "a-myapp" && rec.Type == "TXT":
|
|
foundMyappTXT = true
|
|
case rec.GetLabel() == "api" && rec.Type == "CNAME":
|
|
foundApiCNAME = true
|
|
case rec.GetLabel() == "cname-api" && rec.Type == "TXT":
|
|
foundApiTXT = true
|
|
case rec.GetLabel() == "static" && rec.Type == "A":
|
|
foundStatic = true
|
|
}
|
|
}
|
|
|
|
if !foundMyappA {
|
|
t.Error("Expected myapp A record to be preserved")
|
|
}
|
|
if !foundMyappTXT {
|
|
t.Error("Expected a-myapp TXT record to be preserved")
|
|
}
|
|
if !foundApiCNAME {
|
|
t.Error("Expected api CNAME record to be preserved")
|
|
}
|
|
if !foundApiTXT {
|
|
t.Error("Expected cname-api TXT record to be preserved")
|
|
}
|
|
if !foundStatic {
|
|
t.Error("Expected static A record to be preserved")
|
|
}
|
|
|
|
// Check that we got a message about external-dns records
|
|
foundMsg := false
|
|
for _, msg := range msgs {
|
|
if strings.Contains(msg, "IGNORE_EXTERNAL_DNS") {
|
|
foundMsg = true
|
|
break
|
|
}
|
|
}
|
|
if !foundMsg {
|
|
t.Error("Expected message about IGNORE_EXTERNAL_DNS records")
|
|
}
|
|
}
|
|
|
|
// Test_ignore_external_dns_custom_prefix tests IGNORE_EXTERNAL_DNS with custom prefix
|
|
func Test_ignore_external_dns_custom_prefix(t *testing.T) {
|
|
domain := "f.com"
|
|
|
|
// Existing zone has external-dns managed records with custom prefix "extdns-"
|
|
existing := models.Records{
|
|
// External-dns TXT ownership record with custom prefix
|
|
makeTestRecord("extdns-www", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
|
// The A record managed by external-dns
|
|
makeTestRecord("www", "A", "10.0.0.1", domain),
|
|
// Static record
|
|
makeTestRecord("static", "A", "1.2.3.4", domain),
|
|
}
|
|
|
|
// Desired only has the static record
|
|
desired := models.Records{
|
|
makeTestRecord("static", "A", "1.2.3.4", domain),
|
|
}
|
|
|
|
// Call handsoff with custom prefix
|
|
result, _, err := handsoff(
|
|
domain,
|
|
existing,
|
|
desired,
|
|
nil, // absences
|
|
nil, // unmanagedConfigs
|
|
false, // unmanagedSafely
|
|
false, // noPurge
|
|
true, // ignoreExternalDNS
|
|
"extdns-", // externalDNSPrefix
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check that external-dns records with custom prefix are preserved
|
|
foundWwwA := false
|
|
foundWwwTXT := false
|
|
|
|
for _, rec := range result {
|
|
switch {
|
|
case rec.GetLabel() == "www" && rec.Type == "A":
|
|
foundWwwA = true
|
|
case rec.GetLabel() == "extdns-www" && rec.Type == "TXT":
|
|
foundWwwTXT = true
|
|
}
|
|
}
|
|
|
|
if !foundWwwA {
|
|
t.Error("Expected www A record to be preserved with custom prefix")
|
|
}
|
|
if !foundWwwTXT {
|
|
t.Error("Expected extdns-www TXT record to be preserved with custom prefix")
|
|
}
|
|
}
|
|
|
|
// Test_ignore_external_dns_conflict tests conflict detection
|
|
func Test_ignore_external_dns_conflict(t *testing.T) {
|
|
domain := "f.com"
|
|
|
|
// Existing zone has external-dns managed record
|
|
existing := models.Records{
|
|
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
|
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
|
}
|
|
|
|
// Desired ALSO has myapp - this is a conflict!
|
|
desired := models.Records{
|
|
makeTestRecord("myapp", "A", "192.168.1.1", domain), // Different IP
|
|
}
|
|
|
|
// Call handsoff with IGNORE_EXTERNAL_DNS enabled
|
|
result, msgs, err := handsoff(
|
|
domain,
|
|
existing,
|
|
desired,
|
|
nil, // absences
|
|
nil, // unmanagedConfigs
|
|
false, // unmanagedSafely
|
|
false, // noPurge
|
|
true, // ignoreExternalDNS
|
|
"", // externalDNSPrefix
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should get a warning about the conflict
|
|
foundConflictWarning := false
|
|
for _, msg := range msgs {
|
|
if strings.Contains(msg, "WARNING") && strings.Contains(msg, "external-dns") {
|
|
foundConflictWarning = true
|
|
break
|
|
}
|
|
}
|
|
if !foundConflictWarning {
|
|
t.Error("Expected warning about conflict between desired and external-dns records")
|
|
}
|
|
|
|
// The desired record should be in result (not duplicated)
|
|
myappCount := 0
|
|
for _, rec := range result {
|
|
if rec.GetLabel() == "myapp" && rec.Type == "A" {
|
|
myappCount++
|
|
}
|
|
}
|
|
if myappCount != 1 {
|
|
t.Errorf("Expected exactly 1 myapp A record in result, got %d", myappCount)
|
|
}
|
|
}
|