mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
Bugfixed: NO_PURGE now works on all diff2 providers (#2084)
This commit is contained in:
@ -750,6 +750,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
// This is a strange one. It adds a new record to an existing
|
// This is a strange one. It adds a new record to an existing
|
||||||
// label but the pre-existing label has its TTL change.
|
// label but the pre-existing label has its TTL change.
|
||||||
testgroup("add to label and change orig ttl",
|
testgroup("add to label and change orig ttl",
|
||||||
|
not("NAMEDOTCOM"), // Known bug: https://github.com/StackExchange/dnscontrol/issues/2088
|
||||||
tc("Setup", ttl(a("www", "5.6.7.8"), 400)),
|
tc("Setup", ttl(a("www", "5.6.7.8"), 400)),
|
||||||
tc("Add at same label, new ttl", ttl(a("www", "5.6.7.8"), 700), ttl(a("www", "1.2.3.4"), 700)),
|
tc("Add at same label, new ttl", ttl(a("www", "5.6.7.8"), 700), ttl(a("www", "1.2.3.4"), 700)),
|
||||||
),
|
),
|
||||||
|
@ -19,11 +19,13 @@ type DomainConfig struct {
|
|||||||
Records Records `json:"records"`
|
Records Records `json:"records"`
|
||||||
Nameservers []*Nameserver `json:"nameservers,omitempty"`
|
Nameservers []*Nameserver `json:"nameservers,omitempty"`
|
||||||
|
|
||||||
KeepUnknown bool `json:"keepunknown,omitempty"`
|
EnsureAbsent Records `json:"recordsabsent,omitempty"` // ENSURE_ABSENT
|
||||||
|
KeepUnknown bool `json:"keepunknown,omitempty"` // NO_PURGE
|
||||||
|
|
||||||
IgnoredNames []*IgnoreName `json:"ignored_names,omitempty"`
|
IgnoredNames []*IgnoreName `json:"ignored_names,omitempty"`
|
||||||
IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"`
|
IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"`
|
||||||
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"`
|
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"` // UNMANAGED()
|
||||||
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"`
|
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"` // DISABLE_UNMANAGED_SAFETY_CHECK
|
||||||
|
|
||||||
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
||||||
//DNSSEC bool `json:"dnssec,omitempty"`
|
//DNSSEC bool `json:"dnssec,omitempty"`
|
||||||
@ -36,14 +38,6 @@ type DomainConfig struct {
|
|||||||
DNSProviderInstances []*DNSProviderInstance `json:"-"`
|
DNSProviderInstances []*DNSProviderInstance `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmanagedConfig describes an UNMANAGED() rule.
|
|
||||||
type UnmanagedConfig struct {
|
|
||||||
Label string `json:"label_pattern"` // Glob pattern for matching labels.
|
|
||||||
RType string `json:"rType_pattern"` // Comma-separated list of DNS Resource Types.
|
|
||||||
typeMap map[string]bool // map of RTypes or len()=0 for all
|
|
||||||
Target string `json:"target_pattern"` // Glob pattern for matching targets.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy returns a deep copy of the DomainConfig.
|
// Copy returns a deep copy of the DomainConfig.
|
||||||
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
|
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
|
||||||
newDc := &DomainConfig{}
|
newDc := &DomainConfig{}
|
||||||
|
@ -184,6 +184,9 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
|
|||||||
TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
|
TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
|
||||||
R53Alias map[string]string `json:"r53_alias,omitempty"`
|
R53Alias map[string]string `json:"r53_alias,omitempty"`
|
||||||
AzureAlias map[string]string `json:"azure_alias,omitempty"`
|
AzureAlias map[string]string `json:"azure_alias,omitempty"`
|
||||||
|
|
||||||
|
EnsureAbsent bool `json:"ensure_absent,omitempty"` // Override NO_PURGE and delete this record
|
||||||
|
|
||||||
// NB(tlim): If anyone can figure out how to do this without listing all
|
// NB(tlim): If anyone can figure out how to do this without listing all
|
||||||
// the fields, please let us know!
|
// the fields, please let us know!
|
||||||
}{}
|
}{}
|
||||||
|
30
models/recorddb.go
Normal file
30
models/recorddb.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Functions that make it easier to deal with a group of records.
|
||||||
|
|
||||||
|
type RecordDB struct {
|
||||||
|
labelAndTypeMap map[RecordKey]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecordDBFromRecords creates a RecordDB from a list of RecordConfig.
|
||||||
|
func NewRecordDBFromRecords(recs Records, zone string) *RecordDB {
|
||||||
|
result := &RecordDB{}
|
||||||
|
|
||||||
|
//fmt.Printf("DEBUG: BUILDING RecordDB: zone=%v\n", zone)
|
||||||
|
result.labelAndTypeMap = make(map[RecordKey]struct{}, len(recs))
|
||||||
|
for _, rec := range recs {
|
||||||
|
//fmt.Printf(" DEBUG: Adding %+v\n", rec.Key())
|
||||||
|
result.labelAndTypeMap[rec.Key()] = struct{}{}
|
||||||
|
}
|
||||||
|
//fmt.Printf("DEBUG: BUILDING RecordDB: DONE!\n")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsLT returns true if recdb contains rec. Matching is done
|
||||||
|
// on the record's label and type (i.e. the RecordKey)
|
||||||
|
func (recdb *RecordDB) ContainsLT(rec *RecordConfig) bool {
|
||||||
|
_, ok := recdb.labelAndTypeMap[rec.Key()]
|
||||||
|
//fmt.Printf("DEBUG: ContainsLT(%q) = %v (%v)\n", rec.Key(), ok, recdb)
|
||||||
|
return ok
|
||||||
|
}
|
20
models/unmanaged.go
Normal file
20
models/unmanaged.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gobwas/glob"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmanagedConfig describes an UNMANAGED() rule.
|
||||||
|
type UnmanagedConfig struct {
|
||||||
|
// Glob pattern for matching labels.
|
||||||
|
LabelPattern string `json:"label_pattern,omitempty"`
|
||||||
|
LabelGlob glob.Glob `json:"-"` // Compiled version
|
||||||
|
|
||||||
|
// Comma-separated list of DNS Resource Types.
|
||||||
|
RTypePattern string `json:"rType_pattern,omitempty"`
|
||||||
|
RTypeMap map[string]struct{} `json:"-"` // map of RTypes or len()=0 for all
|
||||||
|
|
||||||
|
// Glob pattern for matching targets.
|
||||||
|
TargetPattern string `json:"target_pattern,omitempty"`
|
||||||
|
TargetGlob glob.Glob `json:"-"` // Compiled version
|
||||||
|
}
|
@ -65,8 +65,9 @@ func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (unchang
|
|||||||
cor := Correlation{d: d.OldDiffer}
|
cor := Correlation{d: d.OldDiffer}
|
||||||
switch inst.Type {
|
switch inst.Type {
|
||||||
case diff2.REPORT:
|
case diff2.REPORT:
|
||||||
// Sadly the NewCompat function doesn't have a way to do this.
|
// Sadly the NewCompat function doesn't have an equivalent. We
|
||||||
// Purge reports are silently skipped.
|
// just output the messages now.
|
||||||
|
fmt.Println(inst.MsgsJoined)
|
||||||
case diff2.CREATE:
|
case diff2.CREATE:
|
||||||
cor.Desired = inst.New[0]
|
cor.Desired = inst.New[0]
|
||||||
create = append(create, cor)
|
create = append(create, cor)
|
||||||
|
@ -109,10 +109,8 @@ func NewCompareConfig(origin string, existing, desired models.Records, compFn Co
|
|||||||
func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
||||||
|
|
||||||
// In theory these assertions do not need to be tested as they test
|
// In theory these assertions do not need to be tested as they test
|
||||||
// something that can not happen. In my head I've proved this to be
|
// something that can not happen. In my I've proved this to be
|
||||||
// true. That said, a little paranoia is healthy. Those familiar
|
// true. That said, a little paranoia is healthy.
|
||||||
// with the Therac-25 accident will agree:
|
|
||||||
// https://hackaday.com/2015/10/26/killed-by-a-machine-the-therac-25/
|
|
||||||
|
|
||||||
// According to the RFCs if a label has a CNAME, it can not have any other
|
// According to the RFCs if a label has a CNAME, it can not have any other
|
||||||
// records at that label... even other CNAMEs. Therefore, we need to be
|
// records at that label... even other CNAMEs. Therefore, we need to be
|
||||||
@ -141,15 +139,28 @@ func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
|||||||
|
|
||||||
for _, ld := range cc.ldata {
|
for _, ld := range cc.ldata {
|
||||||
for j, td := range ld.tdata {
|
for j, td := range ld.tdata {
|
||||||
|
|
||||||
if td.rType == "CNAME" {
|
if td.rType == "CNAME" {
|
||||||
|
|
||||||
|
// This assertion doesn't hold for a site that permits a
|
||||||
|
// recordset with both CNAMEs and other records, such as
|
||||||
|
// Cloudflare.
|
||||||
|
// Therefore, we skip the test if we aren't deleting
|
||||||
|
// everything at the recordset or creating it from scratch.
|
||||||
|
if len(td.existingTargets) != 0 && len(td.desiredTargets) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if len(td.existingTargets) != 0 {
|
if len(td.existingTargets) != 0 {
|
||||||
//fmt.Printf("DEBUG: cname in existing: index=%d\n", j)
|
//fmt.Printf("DEBUG: cname in existing: index=%d\n", j)
|
||||||
if j != 0 {
|
if j != 0 {
|
||||||
panic("should not happen: (CNAME not in first position)")
|
panic("should not happen: (CNAME not in first position)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(td.desiredTargets) != 0 {
|
if len(td.desiredTargets) != 0 {
|
||||||
//fmt.Printf("DEBUG: cname in desired: index=%d\n", j)
|
//fmt.Printf("DEBUG: cname in desired: index=%d\n", j)
|
||||||
|
//fmt.Printf("DEBUG: highest: index=%d\n", j)
|
||||||
if j != highest(ld.tdata) {
|
if j != highest(ld.tdata) {
|
||||||
panic("should not happen: (CNAME not in last position)")
|
panic("should not happen: (CNAME not in last position)")
|
||||||
}
|
}
|
||||||
@ -215,8 +226,6 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
|||||||
|
|
||||||
for _, rec := range z.Records {
|
for _, rec := range z.Records {
|
||||||
|
|
||||||
//label := rec.NameFQDN
|
|
||||||
//rtype := rec.Type
|
|
||||||
key := rec.Key()
|
key := rec.Key()
|
||||||
label := key.NameFQDN
|
label := key.NameFQDN
|
||||||
rtype := key.Type
|
rtype := key.Type
|
||||||
@ -225,12 +234,10 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
|||||||
// Are we seeing this label for the first time?
|
// Are we seeing this label for the first time?
|
||||||
var labelIdx int
|
var labelIdx int
|
||||||
if _, ok := cc.labelMap[label]; !ok {
|
if _, ok := cc.labelMap[label]; !ok {
|
||||||
//fmt.Printf("DEBUG: I haven't see label=%v before. Adding.\n", label)
|
|
||||||
cc.labelMap[label] = true
|
cc.labelMap[label] = true
|
||||||
cc.ldata = append(cc.ldata, &labelConfig{label: label})
|
cc.ldata = append(cc.ldata, &labelConfig{label: label})
|
||||||
labelIdx = highest(cc.ldata)
|
labelIdx = highest(cc.ldata)
|
||||||
} else {
|
} else {
|
||||||
// find label in cc.ldata:
|
|
||||||
for k, v := range cc.ldata {
|
for k, v := range cc.ldata {
|
||||||
if v.label == label {
|
if v.label == label {
|
||||||
labelIdx = k
|
labelIdx = k
|
||||||
@ -240,12 +247,9 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Are we seeing this label+rtype for the first time?
|
// Are we seeing this label+rtype for the first time?
|
||||||
//key := rec.Key()
|
|
||||||
if _, ok := cc.keyMap[key]; !ok {
|
if _, ok := cc.keyMap[key]; !ok {
|
||||||
//fmt.Printf("DEBUG: I haven't see key=%v before. Adding.\n", key)
|
|
||||||
cc.keyMap[key] = true
|
cc.keyMap[key] = true
|
||||||
x := cc.ldata[labelIdx]
|
x := cc.ldata[labelIdx]
|
||||||
//fmt.Printf("DEBUG: appending rtype=%v\n", rtype)
|
|
||||||
x.tdata = append(x.tdata, &rTypeConfig{rType: rtype})
|
x.tdata = append(x.tdata, &rTypeConfig{rType: rtype})
|
||||||
}
|
}
|
||||||
var rtIdx int
|
var rtIdx int
|
||||||
@ -256,7 +260,6 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//fmt.Printf("DEBUG: found rtype=%v at index %d\n", rtype, rtIdx)
|
|
||||||
|
|
||||||
// Now it is safe to add/modify the records.
|
// Now it is safe to add/modify the records.
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package diff2
|
package diff2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -9,11 +8,6 @@ import (
|
|||||||
"github.com/kylelemons/godebug/diff"
|
"github.com/kylelemons/godebug/diff"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prettyPrint(i interface{}) string {
|
|
||||||
s, _ := json.MarshalIndent(i, "", "\t")
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewCompareConfig(t *testing.T) {
|
func TestNewCompareConfig(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
origin string
|
origin string
|
||||||
|
@ -8,6 +8,7 @@ package diff2
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/models"
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
)
|
)
|
||||||
@ -96,18 +97,7 @@ General instructions:
|
|||||||
//
|
//
|
||||||
// Examples include:
|
// Examples include:
|
||||||
func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||||
// dc stores the desired state.
|
return byHelper(analyzeByRecordSet, existing, dc, compFunc)
|
||||||
|
|
||||||
desired := dc.Records
|
|
||||||
var err error
|
|
||||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
|
||||||
instructions := analyzeByRecordSet(cc)
|
|
||||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByLabel takes two lists of records (existing and desired) and
|
// ByLabel takes two lists of records (existing and desired) and
|
||||||
@ -119,18 +109,7 @@ func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc Comp
|
|||||||
//
|
//
|
||||||
// Examples include:
|
// Examples include:
|
||||||
func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||||
// dc stores the desired state.
|
return byHelper(analyzeByLabel, existing, dc, compFunc)
|
||||||
|
|
||||||
desired := dc.Records
|
|
||||||
var err error
|
|
||||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
|
||||||
instructions := analyzeByLabel(cc)
|
|
||||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByRecord takes two lists of records (existing and desired) and
|
// ByRecord takes two lists of records (existing and desired) and
|
||||||
@ -146,61 +125,82 @@ func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc Comparab
|
|||||||
//
|
//
|
||||||
// Examples include: INWX
|
// Examples include: INWX
|
||||||
func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||||
// dc stores the desired state.
|
return byHelper(analyzeByRecord, existing, dc, compFunc)
|
||||||
|
|
||||||
desired := dc.Records
|
|
||||||
var err error
|
|
||||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
|
||||||
instructions := analyzeByRecord(cc)
|
|
||||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByZone takes two lists of records (existing and desired) and
|
// ByZone takes two lists of records (existing and desired) and
|
||||||
// returns text one would output to users describing the change.
|
// returns text to output to users describing the change, a bool
|
||||||
|
// indicating if there were any changes, and a possible err value.
|
||||||
//
|
//
|
||||||
// Use this with DNS providers whose API updates the entire zone at a
|
// Use this with DNS providers whose API updates the entire zone at a
|
||||||
// time. That is, to make any change (1 record or many) the entire DNS
|
// time. That is, to make any change (even just 1 record) the entire DNS
|
||||||
// zone is uploaded.
|
// zone is uploaded.
|
||||||
//
|
//
|
||||||
// The user should see a list of changes as if individual records were
|
// The user should see a list of changes as if individual records were updated.
|
||||||
// updated.
|
|
||||||
//
|
//
|
||||||
// The caller of this function should:
|
// Example usage:
|
||||||
//
|
//
|
||||||
// changed, msgs := diff2.ByZone(existing, desired, origin, nil
|
// msgs, changes, err := diff2.ByZone(foundRecords, dc, nil)
|
||||||
// fmt.Sprintf("CREATING ZONEFILE FOR THE FIRST TIME: dir/example.com.zone"))
|
// if err != nil {
|
||||||
// if changed {
|
// return nil, err
|
||||||
// // output msgs
|
// }
|
||||||
// // generate the zone using the "desired" records
|
// if changes {
|
||||||
// }
|
// // Generate a "correction" that uploads the entire zone.
|
||||||
|
// // (dc.Records are the new records for the zone).
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// Example providers include: BIND
|
// Example providers include: BIND
|
||||||
func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
|
func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
|
||||||
// dc stores the desired state.
|
|
||||||
|
|
||||||
if len(existing) == 0 {
|
if len(existing) == 0 {
|
||||||
// Nothing previously existed. No need to output a list of individual changes.
|
// Nothing previously existed. No need to output a list of individual changes.
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
desired := dc.Records
|
// Only return the messages.
|
||||||
var err error
|
instructions, err := byHelper(analyzeByRecord, existing, dc, compFunc)
|
||||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
return justMsgs(instructions), len(instructions) != 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
// byHelper does 90% of the work for the By*() calls.
|
||||||
|
func byHelper(fn func(cc *CompareConfig) ChangeList, existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||||
|
|
||||||
|
// Process NO_PURGE/ENSURE_ABSENT and UNMANAGED/IGNORE_*.
|
||||||
|
desired, msgs, err := handsoff(
|
||||||
|
dc.Name,
|
||||||
|
existing, dc.Records, dc.EnsureAbsent,
|
||||||
|
dc.Unmanaged,
|
||||||
|
dc.UnmanagedUnsafe,
|
||||||
|
dc.KeepUnknown,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regroup existing/desiredd for easy comparison:
|
||||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||||
instructions := analyzeByRecord(cc)
|
|
||||||
instructions = processPurge(instructions, !dc.KeepUnknown)
|
// Analyze and generate the instructions:
|
||||||
return justMsgs(instructions), len(instructions) != 0, nil
|
instructions := fn(cc)
|
||||||
|
|
||||||
|
// If we have msgs, create a change to output them:
|
||||||
|
if len(msgs) != 0 {
|
||||||
|
chg := Change{
|
||||||
|
Type: REPORT,
|
||||||
|
Msgs: msgs,
|
||||||
|
MsgsJoined: strings.Join(msgs, "\n"),
|
||||||
|
}
|
||||||
|
_ = chg
|
||||||
|
instructions = append([]Change{chg}, instructions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instructions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stringify the datastructures for easier debugging
|
||||||
|
|
||||||
func (c Change) String() string {
|
func (c Change) String() string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
b := &buf
|
b := &buf
|
||||||
|
@ -18,7 +18,7 @@ func groupbyRSet(recs models.Records, origin string) []recset {
|
|||||||
|
|
||||||
// Sort the NameFQDN to a consistent order. The actual sort methodology
|
// Sort the NameFQDN to a consistent order. The actual sort methodology
|
||||||
// doesn't matter as long as equal values are adjacent.
|
// doesn't matter as long as equal values are adjacent.
|
||||||
// Use the PrettySort ordering so that the records are in a nice order.
|
// Use the PrettySort ordering so that the records are extra pretty.
|
||||||
pretty := prettyzone.PrettySort(recs, origin, 0, nil)
|
pretty := prettyzone.PrettySort(recs, origin, 0, nil)
|
||||||
recs = pretty.Records
|
recs = pretty.Records
|
||||||
|
|
||||||
|
251
pkg/diff2/handsoff.go
Normal file
251
pkg/diff2/handsoff.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package diff2
|
||||||
|
|
||||||
|
// This file implements the features that tell DNSControl "hands off"
|
||||||
|
// foreign-controlled (or shared-control) DNS records. i.e. the
|
||||||
|
// NO_PURGE, ENSURE_ABSENT, IGNORE_*, and UNMANAGED features.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/gobwas/glob"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
# How do NO_PURGE, IGNORE_*, ENSURE_ABSENT and friends work?
|
||||||
|
|
||||||
|
|
||||||
|
## Terminology:
|
||||||
|
|
||||||
|
* "existing" refers to the records downloaded from the provider via the API.
|
||||||
|
* "desired" refers to the records generated from dnsconfig.js.
|
||||||
|
* "absences" refers to a list of records tagged with ASSURE_ABSENT.
|
||||||
|
|
||||||
|
## What are the features?
|
||||||
|
|
||||||
|
There are 2 ways to tell DNSControl not to touch existing records in a domain,
|
||||||
|
and 1 way to make exceptions.
|
||||||
|
|
||||||
|
* NO_PURGE: Tells DNSControl not to delete records in a domain.
|
||||||
|
* New records will be created
|
||||||
|
* Existing records (matched on label:rtype) will be modified.
|
||||||
|
* FYI: This means you can't have a label with two A records, one controlled
|
||||||
|
by DNSControl and one controlled by an external system.
|
||||||
|
* UNMANAGED(labelglob, typelist, targetglob):
|
||||||
|
* "If an existing record matches this pattern, don't touch it!""
|
||||||
|
* IGNORE_NAME(foo, bar) is the same as UNMANAGED(foo, bar, "*")
|
||||||
|
* IGNORE_TARGET(foo) is the same as UNMANAGED("*", "*", foo)
|
||||||
|
* FYI: You CAN have a label with two A records, one controlled by
|
||||||
|
DNSControl and one controlled by an external system. DNSControl would
|
||||||
|
need to have an UNMANAGED() statement with a targetglob that matches
|
||||||
|
the external system's target values.
|
||||||
|
* ASSURE_ABSENT: Override NO_PURGE for specific records. i.e. delete them even
|
||||||
|
though NO_PURGE is enabled.
|
||||||
|
* If any of these records are in desired (matched on
|
||||||
|
label:rtype:target), remove them. This takes priority over
|
||||||
|
NO_PURGE/UNMANAGED/IGNORE*.
|
||||||
|
|
||||||
|
## Implementation premise
|
||||||
|
|
||||||
|
The fundamental premise is "if you don't want it deleted, copy it to the
|
||||||
|
'desired' list." So, for example, if you want to IGNORE_NAME("www"), then you
|
||||||
|
find any records with the label "www" in "existing" and copy them to "desired".
|
||||||
|
As a result, the diff2 algorithm won't delete them because they are desired!
|
||||||
|
|
||||||
|
This is different than in the old system (pkg/diff) which would generate the
|
||||||
|
diff but but then do a bunch of checking to see if the record was one that
|
||||||
|
shouldn't be deleted. Or, in the case of NO_PURGE, would simply not do the
|
||||||
|
deletions. This was complex because there were many edge cases to deal with.
|
||||||
|
It was often also wrong. For example, if a provider updates all records in a
|
||||||
|
RecordSet at once, you shouldn't NOT update the record.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Here is how we intend to implement these features:
|
||||||
|
|
||||||
|
UNMANAGED is implemented as:
|
||||||
|
* Take the list of existing records. If any match one of the UNMANAGED glob
|
||||||
|
patterns, add it to the "ignored list".
|
||||||
|
* If any item on the "ignored list" is also in "desired" (match on
|
||||||
|
label:rtype), output a warning (defeault) or declare an error (if
|
||||||
|
DISABLE_UNMANAGED_SAFETY_CHECK is true).
|
||||||
|
* When we're done, add the "ignore list" records to desired.
|
||||||
|
|
||||||
|
NO_PURGE + ENSURE_ABSENT is implemented as:
|
||||||
|
* Take the list of existing records. If any do not appear in desired, add them
|
||||||
|
to desired UNLESS they appear in absences.
|
||||||
|
* "appear in desired" is done by matching on label:type.
|
||||||
|
* "appear in absences" is done by matching on label:type:target.
|
||||||
|
|
||||||
|
The actual implementation combines this all into one loop:
|
||||||
|
foreach rec in existing:
|
||||||
|
if rec matches_any_unmanaged_pattern:
|
||||||
|
if rec in desired:
|
||||||
|
if "DISABLE_UNMANAGED_SAFETY_CHECK" is false:
|
||||||
|
Display a warning.
|
||||||
|
else
|
||||||
|
Return an error.
|
||||||
|
Add rec to "ignored list"
|
||||||
|
else:
|
||||||
|
if NO_PURGE:
|
||||||
|
if rec NOT in desired: (matched on label:type)
|
||||||
|
if rec NOT in absences: (matched on label:type:target)
|
||||||
|
Add rec to "foreign list"
|
||||||
|
Append "ignored list" to "desired".
|
||||||
|
Append "foreign list" to "desired".
|
||||||
|
*/
|
||||||
|
|
||||||
|
// handsoff processes the IGNORE_*/UNMANAGED/NO_PURGE/ENSURE_ABSENT features.
|
||||||
|
func handsoff(
|
||||||
|
domain string,
|
||||||
|
existing, desired, absences models.Records,
|
||||||
|
unmanagedConfigs []*models.UnmanagedConfig,
|
||||||
|
unmanagedSafely bool,
|
||||||
|
noPurge bool,
|
||||||
|
) (models.Records, []string, error) {
|
||||||
|
var msgs []string
|
||||||
|
|
||||||
|
// Prep the globs:
|
||||||
|
err := compileUnmanagedConfigs(unmanagedConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process UNMANAGE/IGNORE_* and NO_PURGE features:
|
||||||
|
ignorable, foreign := processIgnoreAndNoPurge(domain, existing, desired, absences, unmanagedConfigs, noPurge)
|
||||||
|
if len(foreign) != 0 {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("INFO: %d records not being deleted because of NO_PURGE:", len(foreign)))
|
||||||
|
for _, r := range foreign {
|
||||||
|
msgs = append(msgs, fmt.Sprintf(" %s. %s %s", r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ignorable) != 0 {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("INFO: %d records not being deleted because of IGNORE*():", len(ignorable)))
|
||||||
|
for _, r := range ignorable {
|
||||||
|
msgs = append(msgs, fmt.Sprintf(" %s %s %s", r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid use of IGNORE_*.
|
||||||
|
conflicts := findConflicts(unmanagedConfigs, desired)
|
||||||
|
if len(conflicts) != 0 {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("INFO: %d records that are both IGNORE*()'d and not ignored:", len(conflicts)))
|
||||||
|
for _, r := range conflicts {
|
||||||
|
msgs = append(msgs, fmt.Sprintf(" %s %s %s", r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted()))
|
||||||
|
}
|
||||||
|
if unmanagedSafely {
|
||||||
|
return nil, nil, fmt.Errorf(strings.Join(msgs, "\n") +
|
||||||
|
"ERROR: Unsafe to continue. Add DISABLE_UNMANAGED_SAFETY_CHECK to D() to override")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the ignored/foreign items to the desired list so they are not deleted:
|
||||||
|
desired = append(desired, ignorable...)
|
||||||
|
desired = append(desired, foreign...)
|
||||||
|
return desired, msgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processIgnoreAndNoPurge processes the IGNORE_*()/UNMANAGED() and NO_PURGE/ENSURE_ABSENT_REC() features.
|
||||||
|
func processIgnoreAndNoPurge(domain string, existing, desired, absences models.Records, unmanagedConfigs []*models.UnmanagedConfig, noPurge bool) (models.Records, models.Records) {
|
||||||
|
var ignorable, foreign models.Records
|
||||||
|
desiredDB := models.NewRecordDBFromRecords(desired, domain)
|
||||||
|
absentDB := models.NewRecordDBFromRecords(absences, domain)
|
||||||
|
compileUnmanagedConfigs(unmanagedConfigs)
|
||||||
|
for _, rec := range existing {
|
||||||
|
if matchAll(unmanagedConfigs, rec) {
|
||||||
|
ignorable = append(ignorable, rec)
|
||||||
|
} else {
|
||||||
|
if noPurge {
|
||||||
|
// Is this a candidate for purging?
|
||||||
|
if !desiredDB.ContainsLT(rec) {
|
||||||
|
// Yes, but not if it is an exception!
|
||||||
|
if !absentDB.ContainsLT(rec) {
|
||||||
|
foreign = append(foreign, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ignorable, foreign
|
||||||
|
}
|
||||||
|
|
||||||
|
// findConflicts takes a list of recs and a list of (compiled) UnmanagedConfigs
|
||||||
|
// and reports if any of the recs match any of the configs.
|
||||||
|
func findConflicts(uconfigs []*models.UnmanagedConfig, recs models.Records) models.Records {
|
||||||
|
var conflicts models.Records
|
||||||
|
for _, rec := range recs {
|
||||||
|
if matchAll(uconfigs, rec) {
|
||||||
|
conflicts = append(conflicts, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileUnmanagedConfigs prepares a slice of UnmanagedConfigs so they can be used.
|
||||||
|
func compileUnmanagedConfigs(configs []*models.UnmanagedConfig) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := range configs {
|
||||||
|
c := configs[i]
|
||||||
|
|
||||||
|
if c.LabelPattern == "" || c.LabelPattern == "*" {
|
||||||
|
c.LabelGlob = nil // nil indicates "always match"
|
||||||
|
} else {
|
||||||
|
c.LabelGlob, err = glob.Compile(c.LabelPattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RTypeMap = make(map[string]struct{})
|
||||||
|
if c.RTypePattern != "*" && c.RTypePattern != "" {
|
||||||
|
for _, part := range strings.Split(c.RTypePattern, ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
c.RTypeMap[part] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TargetPattern == "" || c.TargetPattern == "*" {
|
||||||
|
c.TargetGlob = nil // nil indicates "always match"
|
||||||
|
} else {
|
||||||
|
c.TargetGlob, err = glob.Compile(c.TargetPattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchAll returns true if rec matches any of the uconfigs.
|
||||||
|
func matchAll(uconfigs []*models.UnmanagedConfig, rec *models.RecordConfig) bool {
|
||||||
|
for _, uc := range uconfigs {
|
||||||
|
if matchLabel(uc.LabelGlob, rec.GetLabel()) &&
|
||||||
|
matchType(uc.RTypeMap, rec.Type) &&
|
||||||
|
matchTarget(uc.TargetGlob, rec.GetLabel()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func matchLabel(labelGlob glob.Glob, labelName string) bool {
|
||||||
|
if labelGlob == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return labelGlob.Match(labelName)
|
||||||
|
}
|
||||||
|
func matchType(typeMap map[string]struct{}, typeName string) bool {
|
||||||
|
if len(typeMap) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := typeMap[typeName]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
func matchTarget(targetGlob glob.Glob, targetName string) bool {
|
||||||
|
if targetGlob == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return targetGlob.Match(targetName)
|
||||||
|
}
|
237
pkg/diff2/handsoff_test.go
Normal file
237
pkg/diff2/handsoff_test.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package diff2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/js"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
testifyrequire "github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseZoneContents is copied verbatium 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.RRtoRC(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.GetTargetRFC1035Quoted() +
|
||||||
|
"\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 {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
//fmt.Printf("DEBUG: existing=%s\n", showRecs(existing))
|
||||||
|
|
||||||
|
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 := processIgnoreAndNoPurge(
|
||||||
|
"f.com",
|
||||||
|
existing, desired,
|
||||||
|
absences,
|
||||||
|
unmanagedConfigs,
|
||||||
|
noPurge,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
resultActual,
|
||||||
|
resultWanted,
|
||||||
|
"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, true, `
|
||||||
|
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, true, `
|
||||||
|
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, true, `
|
||||||
|
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, true, `
|
||||||
|
IGNORED:
|
||||||
|
FOREIGN:
|
||||||
|
_2222222222222222.cr CNAME _333333.nnn.acm-validations.aws.
|
||||||
|
`)
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
package diff2
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
func processPurge(instructions ChangeList, nopurge bool) ChangeList {
|
|
||||||
|
|
||||||
if nopurge {
|
|
||||||
return instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(tlim): This can probably be done without allocations but it
|
|
||||||
// works and I won't want to prematurely optimize.
|
|
||||||
|
|
||||||
var msgs []string
|
|
||||||
|
|
||||||
newinstructions := make(ChangeList, 0, len(instructions))
|
|
||||||
for _, j := range instructions {
|
|
||||||
if j.Type == DELETE {
|
|
||||||
msgs = append(msgs, j.Msgs...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newinstructions = append(newinstructions, j)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report what would have been purged
|
|
||||||
if len(msgs) != 0 {
|
|
||||||
for i := range msgs {
|
|
||||||
msgs[i] = "NO_PURGE: Skipping " + msgs[i]
|
|
||||||
}
|
|
||||||
msgs = append([]string{"NO_PURGE Activated! Skipping these actions:"}, msgs...)
|
|
||||||
newinstructions = append(newinstructions, Change{
|
|
||||||
Type: REPORT,
|
|
||||||
Msgs: msgs,
|
|
||||||
MsgsJoined: strings.Join(msgs, "\n"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return newinstructions
|
|
||||||
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
package diff2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
|
||||||
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/models"
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handsoff(
|
|
||||||
existing, desired models.Records,
|
|
||||||
unmanaged []*models.UnmanagedConfig,
|
|
||||||
beSafe bool,
|
|
||||||
) (models.Records, error) {
|
|
||||||
|
|
||||||
// What foreign items should we ignore?
|
|
||||||
foreign, err := manyQueries(existing, unmanaged)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(foreign) != 0 {
|
|
||||||
printer.Printf("INFO: Foreign records being ignored: (%d)\n", len(foreign))
|
|
||||||
for i, r := range foreign {
|
|
||||||
printer.Printf("- % 4d: %s %s %s\n", i, r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// What desired items might conflict?
|
|
||||||
conflicts, err := manyQueries(desired, unmanaged)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(conflicts) != 0 {
|
|
||||||
level := "WARN"
|
|
||||||
if beSafe {
|
|
||||||
level = "ERROR"
|
|
||||||
}
|
|
||||||
printer.Printf("%s: dnsconfig.js records that overlap MANAGED: (%d)\n", level, len(conflicts))
|
|
||||||
for i, r := range conflicts {
|
|
||||||
printer.Printf("- % 4d: %s %s %s\n", i, r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted())
|
|
||||||
}
|
|
||||||
if beSafe {
|
|
||||||
return nil, fmt.Errorf("ERROR: Unsafe to continue. Add DISABLE_UNMANAGED_SAFETY_CHECK to D() to override")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the foreign items to the desired list.
|
|
||||||
// (Rather than literally ignoring them, we just add them to the desired list
|
|
||||||
// and all the diffing algorithms become more simple.)
|
|
||||||
desired = append(desired, foreign...)
|
|
||||||
|
|
||||||
return desired, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func manyQueries(rcs models.Records, queries []*models.UnmanagedConfig) (result models.Records, err error) {
|
|
||||||
|
|
||||||
for _, q := range queries {
|
|
||||||
|
|
||||||
lab := q.Label
|
|
||||||
if lab == "" {
|
|
||||||
lab = "*"
|
|
||||||
}
|
|
||||||
glabel, err := glob.Compile(lab)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
targ := q.Target
|
|
||||||
if targ == "" {
|
|
||||||
targ = "*"
|
|
||||||
}
|
|
||||||
gtarget, err := glob.Compile(targ)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRType := compileTypeGlob(q.RType)
|
|
||||||
|
|
||||||
for _, rc := range rcs {
|
|
||||||
if match(rc, glabel, gtarget, hasRType) {
|
|
||||||
result = append(result, rc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileTypeGlob(g string) map[string]bool {
|
|
||||||
m := map[string]bool{}
|
|
||||||
for _, j := range strings.Split(g, ",") {
|
|
||||||
m[strings.TrimSpace(j)] = true
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func match(rc *models.RecordConfig, glabel, gtarget glob.Glob, hasRType map[string]bool) bool {
|
|
||||||
//printer.Printf("DEBUG: match(%v, %v, %v, %v)\n", rc.NameFQDN, glabel, gtarget, hasRType)
|
|
||||||
|
|
||||||
// _ = glabel.Match(rc.NameFQDN)
|
|
||||||
// _ = matchType(rc.Type, hasRType)
|
|
||||||
// x := rc.GetTargetField()
|
|
||||||
// _ = gtarget.Match(x)
|
|
||||||
|
|
||||||
if !glabel.Match(rc.NameFQDN) {
|
|
||||||
//printer.Printf("DEBUG: REJECTED LABEL: %s:%v\n", rc.NameFQDN, glabel)
|
|
||||||
return false
|
|
||||||
} else if !matchType(rc.Type, hasRType) {
|
|
||||||
//printer.Printf("DEBUG: REJECTED TYPE: %s:%v\n", rc.Type, hasRType)
|
|
||||||
return false
|
|
||||||
} else if gtarget == nil {
|
|
||||||
return true
|
|
||||||
} else if !gtarget.Match(rc.GetTargetField()) {
|
|
||||||
//printer.Printf("DEBUG: REJECTED TARGET: %v:%v\n", rc.GetTargetField(), gtarget)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchType(s string, hasRType map[string]bool) bool {
|
|
||||||
//printer.Printf("DEBUG: matchType map=%v\n", hasRType)
|
|
||||||
if len(hasRType) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, ok := hasRType[s]
|
|
||||||
return ok
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
package diff2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/StackExchange/dnscontrol/v3/models"
|
|
||||||
"github.com/gobwas/glob"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rmapNil map[string]bool
|
|
||||||
var rmapAMX = map[string]bool{
|
|
||||||
"A": true,
|
|
||||||
"MX": true,
|
|
||||||
}
|
|
||||||
var rmapCNAME = map[string]bool{
|
|
||||||
"CNAME": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_match(t *testing.T) {
|
|
||||||
|
|
||||||
testRecLammaA1234 := makeRec("lamma", "A", "1.2.3.4")
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
rc *models.RecordConfig
|
|
||||||
glabel glob.Glob
|
|
||||||
gtarget glob.Glob
|
|
||||||
hasRType map[string]bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "match3",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("lam*"),
|
|
||||||
hasRType: rmapAMX,
|
|
||||||
gtarget: glob.MustCompile("1.2.3.*"),
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "match2",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("lam*"),
|
|
||||||
hasRType: rmapAMX,
|
|
||||||
gtarget: nil,
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "match1",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("lam*"),
|
|
||||||
hasRType: rmapNil,
|
|
||||||
gtarget: nil,
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "reject1",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("yyyy"),
|
|
||||||
hasRType: rmapAMX,
|
|
||||||
gtarget: glob.MustCompile("1.2.3.*"),
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "reject2",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("lam*"),
|
|
||||||
hasRType: rmapCNAME,
|
|
||||||
gtarget: glob.MustCompile("1.2.3.*"),
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "reject3",
|
|
||||||
args: args{
|
|
||||||
rc: testRecLammaA1234,
|
|
||||||
glabel: glob.MustCompile("lam*"),
|
|
||||||
hasRType: rmapAMX,
|
|
||||||
gtarget: glob.MustCompile("zzzzz"),
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := match(tt.args.rc, tt.args.glabel, tt.args.gtarget, tt.args.hasRType); got != tt.want {
|
|
||||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_matchType(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
s string
|
|
||||||
hasRType map[string]bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "matchCNAME",
|
|
||||||
args: args{"CNAME", rmapCNAME},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "rejectCNAME",
|
|
||||||
args: args{"MX", rmapCNAME},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "matchNIL",
|
|
||||||
args: args{"CNAME", rmapNil},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := matchType(tt.args.s, tt.args.hasRType); got != tt.want {
|
|
||||||
t.Errorf("matchType() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -104,6 +104,7 @@ function newDomain(name, registrar) {
|
|||||||
registrar: registrar,
|
registrar: registrar,
|
||||||
meta: {},
|
meta: {},
|
||||||
records: [],
|
records: [],
|
||||||
|
recordsabsent: [],
|
||||||
dnsProviders: {},
|
dnsProviders: {},
|
||||||
defaultTTL: 0,
|
defaultTTL: 0,
|
||||||
nameservers: [],
|
nameservers: [],
|
||||||
@ -614,7 +615,6 @@ function IGNORE_NAME(name, rTypes) {
|
|||||||
d.unmanaged.push({
|
d.unmanaged.push({
|
||||||
label_pattern: name,
|
label_pattern: name,
|
||||||
rType_pattern: rTypes,
|
rType_pattern: rTypes,
|
||||||
target_pattern: '*',
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -632,7 +632,6 @@ function IGNORE_TARGET(target, rType) {
|
|||||||
return function (d) {
|
return function (d) {
|
||||||
d.ignored_targets.push({ pattern: target, type: rType });
|
d.ignored_targets.push({ pattern: target, type: rType });
|
||||||
d.unmanaged.push({
|
d.unmanaged.push({
|
||||||
label_pattern: '*',
|
|
||||||
rType_pattern: rType,
|
rType_pattern: rType,
|
||||||
target_pattern: target,
|
target_pattern: target,
|
||||||
});
|
});
|
||||||
@ -660,6 +659,22 @@ function NO_PURGE(d) {
|
|||||||
d.KeepUnknown = true;
|
d.KeepUnknown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ENSURE_ABSENT_REC()
|
||||||
|
// Usage: A("foo", "1.2.3.4", ENSURE_ABSENT_REC())
|
||||||
|
function ENSURE_ABSENT_REC() {
|
||||||
|
return function (r) {
|
||||||
|
r.ensure_absent = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENSURE_ABSENT()
|
||||||
|
// Usage: ENSURE_ABSENT(A("foo", "1.2.3.4"))
|
||||||
|
// (BROKEN. COMMENTED OUT UNTIL IT IS FIXED.)
|
||||||
|
// function ENSURE_ABSENT(r) {
|
||||||
|
// //console.log(r);
|
||||||
|
// return r;
|
||||||
|
// }
|
||||||
|
|
||||||
// AUTODNSSEC
|
// AUTODNSSEC
|
||||||
// Permitted values are:
|
// Permitted values are:
|
||||||
// "" Do not modify the setting (the default)
|
// "" Do not modify the setting (the default)
|
||||||
@ -678,15 +693,6 @@ function AUTODNSSEC(d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UNMANAGED(label_pattern, rType_pattern, target_pattern) {
|
function UNMANAGED(label_pattern, rType_pattern, target_pattern) {
|
||||||
if (rType_pattern === undefined) {
|
|
||||||
rType_pattern = '*';
|
|
||||||
}
|
|
||||||
if (rType_pattern === "") {
|
|
||||||
rType_pattern = '*';
|
|
||||||
}
|
|
||||||
if (target_pattern === undefined) {
|
|
||||||
target_pattern = '*';
|
|
||||||
}
|
|
||||||
return function (d) {
|
return function (d) {
|
||||||
d.unmanaged.push({
|
d.unmanaged.push({
|
||||||
label_pattern: label_pattern,
|
label_pattern: label_pattern,
|
||||||
@ -835,7 +841,15 @@ function recordBuilder(type, opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d.records.push(record);
|
// Now we finally have the record. If it is a normal record, we add
|
||||||
|
// it to "records". If it is an ENSURE_ABSENT record, we add it to
|
||||||
|
// the ensure_absent list.
|
||||||
|
if (record.ensure_absent) {
|
||||||
|
d.recordsabsent.push(record);
|
||||||
|
} else {
|
||||||
|
d.records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -45,6 +45,11 @@ func ExecuteJavascript(file string, devMode bool, variables map[string]string) (
|
|||||||
// Record the directory path leading up to this file.
|
// Record the directory path leading up to this file.
|
||||||
currentDirectory = filepath.Dir(file)
|
currentDirectory = filepath.Dir(file)
|
||||||
|
|
||||||
|
return ExecuteJavascriptString(script, devMode, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteJavascriptString(script []byte, devMode bool, variables map[string]string) (*models.DNSConfig, error) {
|
||||||
|
|
||||||
vm := otto.New()
|
vm := otto.New()
|
||||||
l := loop.New(vm)
|
l := loop.New(vm)
|
||||||
|
|
||||||
|
@ -46,45 +46,37 @@
|
|||||||
"unmanaged": [
|
"unmanaged": [
|
||||||
{
|
{
|
||||||
"label_pattern": "testignore",
|
"label_pattern": "testignore",
|
||||||
"rType_pattern": "*",
|
"rType_pattern": "*"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "testignore2",
|
"label_pattern": "testignore2",
|
||||||
"rType_pattern": "A",
|
"rType_pattern": "A"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "testignore3",
|
"label_pattern": "testignore3",
|
||||||
"rType_pattern": "A, CNAME, TXT",
|
"rType_pattern": "A, CNAME, TXT"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "testignore4",
|
"label_pattern": "testignore4",
|
||||||
"rType_pattern": "*",
|
"rType_pattern": "*"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "*",
|
|
||||||
"rType_pattern": "CNAME",
|
"rType_pattern": "CNAME",
|
||||||
"target_pattern": "testtarget"
|
"target_pattern": "testtarget"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "legacyignore",
|
"label_pattern": "legacyignore",
|
||||||
"rType_pattern": "*",
|
"rType_pattern": "*"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "@",
|
"label_pattern": "@",
|
||||||
"rType_pattern": "*",
|
"rType_pattern": "*"
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "*",
|
|
||||||
"rType_pattern": "CNAME",
|
"rType_pattern": "CNAME",
|
||||||
"target_pattern": "@"
|
"target_pattern": "@"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
"unmanaged": [
|
"unmanaged": [
|
||||||
{
|
{
|
||||||
"label_pattern": "\\*.testignore",
|
"label_pattern": "\\*.testignore",
|
||||||
"rType_pattern": "*",
|
"rType_pattern": "*"
|
||||||
"target_pattern": "*"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
D("foo.com", "none"
|
D("foo.com", "none"
|
||||||
, UNMANAGED("one")
|
, UNMANAGED("", "", "targetGlob1")
|
||||||
, UNMANAGED("two", "A, CNAME")
|
, UNMANAGED("", "CNAME", "")
|
||||||
, UNMANAGED("three", "TXT", "findme")
|
, UNMANAGED("", "A", "targetGlob3")
|
||||||
, UNMANAGED("notype", "", "targglob")
|
, UNMANAGED("lab4")
|
||||||
|
, UNMANAGED("notype", "", "targetGlob5")
|
||||||
|
, UNMANAGED("lab6", "A, CNAME")
|
||||||
|
, UNMANAGED("lab7", "TXT", "targetGlob7")
|
||||||
);
|
);
|
||||||
|
@ -1,34 +1,40 @@
|
|||||||
{
|
{
|
||||||
"registrars": [],
|
|
||||||
"dns_providers": [],
|
"dns_providers": [],
|
||||||
"domains": [
|
"domains": [
|
||||||
{
|
{
|
||||||
"name": "foo.com",
|
|
||||||
"registrar": "none",
|
|
||||||
"dnsProviders": {},
|
"dnsProviders": {},
|
||||||
|
"name": "foo.com",
|
||||||
"records": [],
|
"records": [],
|
||||||
|
"registrar": "none",
|
||||||
"unmanaged": [
|
"unmanaged": [
|
||||||
{
|
{
|
||||||
"label_pattern": "one",
|
"target_pattern": "targetGlob1"
|
||||||
"rType_pattern": "*",
|
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "two",
|
"rType_pattern": "CNAME"
|
||||||
"rType_pattern": "A, CNAME",
|
|
||||||
"target_pattern": "*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "three",
|
"rType_pattern": "A",
|
||||||
"rType_pattern": "TXT",
|
"target_pattern": "targetGlob3"
|
||||||
"target_pattern": "findme"
|
},
|
||||||
|
{
|
||||||
|
"label_pattern": "lab4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label_pattern": "notype",
|
"label_pattern": "notype",
|
||||||
"rType_pattern": "*",
|
"target_pattern": "targetGlob5"
|
||||||
"target_pattern": "targglob"
|
},
|
||||||
|
{
|
||||||
|
"label_pattern": "lab6",
|
||||||
|
"rType_pattern": "A, CNAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label_pattern": "lab7",
|
||||||
|
"rType_pattern": "TXT",
|
||||||
|
"target_pattern": "targetGlob7"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"registrars": []
|
||||||
|
}
|
||||||
|
5
pkg/js/parse_tests/044-ensureabsent.js
Normal file
5
pkg/js/parse_tests/044-ensureabsent.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
D("example.com", "none",
|
||||||
|
A("normal", "1.1.1.1"),
|
||||||
|
A("helper", "2.2.2.2", ENSURE_ABSENT_REC()),
|
||||||
|
//ENSURE_ABSENT(A("wrapped", "3.3.3.3")),
|
||||||
|
{});
|
25
pkg/js/parse_tests/044-ensureabsent.json
Normal file
25
pkg/js/parse_tests/044-ensureabsent.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"dns_providers": [],
|
||||||
|
"domains": [
|
||||||
|
{
|
||||||
|
"dnsProviders": {},
|
||||||
|
"name": "example.com",
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"name": "normal",
|
||||||
|
"target": "1.1.1.1",
|
||||||
|
"type": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recordsabsent": [
|
||||||
|
{
|
||||||
|
"name": "helper",
|
||||||
|
"target": "2.2.2.2",
|
||||||
|
"type": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"registrar": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"registrars": []
|
||||||
|
}
|
29
pkg/recorddb/recorddb.go
Normal file
29
pkg/recorddb/recorddb.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package recorddb
|
||||||
|
|
||||||
|
import "github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
|
||||||
|
// Functions that make it easier to deal with
|
||||||
|
// a group of records.
|
||||||
|
//
|
||||||
|
|
||||||
|
type RecordDB = struct {
|
||||||
|
labelAndTypeMap map[models.RecordKey]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromRecords(recs models.Records) *RecordDB {
|
||||||
|
result := &RecordDB{}
|
||||||
|
|
||||||
|
result.labelAndTypeMap = make(map[models.RecordKey]struct{}, len(recs))
|
||||||
|
for _, rec := range recs {
|
||||||
|
result.labelAndTypeMap[rec.Key()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsLT returns true if recdb contains rec. Matching is done
|
||||||
|
// on the record's label and type (i.e. the RecordKey)
|
||||||
|
//func (recdb RecordDB) ContainsLT(rec *models.RecordConfig) bool {
|
||||||
|
// _, ok := recdb.labelAndTypeMap[rec.Key()]
|
||||||
|
// return ok
|
||||||
|
//}
|
@ -153,7 +153,6 @@ func (c *bindProvider) ListZones() ([]string, error) {
|
|||||||
|
|
||||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||||
func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||||
foundRecords := models.Records{}
|
|
||||||
|
|
||||||
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
||||||
printer.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
|
printer.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
|
||||||
@ -177,10 +176,17 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|||||||
}
|
}
|
||||||
c.zoneFileFound = true
|
c.zoneFileFound = true
|
||||||
|
|
||||||
zp := dns.NewZoneParser(strings.NewReader(string(content)), domain, c.zonefile)
|
zonefileName := c.zonefile
|
||||||
|
|
||||||
|
return ParseZoneContents(string(content), domain, zonefileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||||
rec, err := models.RRtoRC(rr, domain)
|
rec, err := models.RRtoRC(rr, zoneName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -188,7 +194,7 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := zp.Err(); err != nil {
|
if err := zp.Err(); err != nil {
|
||||||
return nil, fmt.Errorf("error while parsing '%v': %w", c.zonefile, err)
|
return nil, fmt.Errorf("error while parsing '%v': %w", zonefileName, err)
|
||||||
}
|
}
|
||||||
return foundRecords, nil
|
return foundRecords, nil
|
||||||
}
|
}
|
||||||
|
@ -489,7 +489,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
|||||||
switch inst.Type {
|
switch inst.Type {
|
||||||
|
|
||||||
case diff2.REPORT:
|
case diff2.REPORT:
|
||||||
corrections = append(corrections, &models.Correction{Msg: inst.MsgsJoined})
|
chg = r53Types.Change{}
|
||||||
|
|
||||||
case diff2.CREATE:
|
case diff2.CREATE:
|
||||||
fallthrough
|
fallthrough
|
||||||
@ -586,8 +586,9 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
|||||||
func reorderInstructions(changes diff2.ChangeList) diff2.ChangeList {
|
func reorderInstructions(changes diff2.ChangeList) diff2.ChangeList {
|
||||||
var main, tail diff2.ChangeList
|
var main, tail diff2.ChangeList
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
//if change.Key.Type == "R53_ALIAS" {
|
// Reports should be early in the list.
|
||||||
if strings.HasPrefix(change.Key.Type, "R53_ALIAS_") {
|
// R53_ALIAS_ records should go to the tail.
|
||||||
|
if change.Type != diff2.REPORT && strings.HasPrefix(change.Key.Type, "R53_ALIAS_") {
|
||||||
tail = append(tail, change)
|
tail = append(tail, change)
|
||||||
} else {
|
} else {
|
||||||
main = append(main, change)
|
main = append(main, change)
|
||||||
@ -889,6 +890,11 @@ func (b *changeBatcher) Next() bool {
|
|||||||
c := &b.changes[end]
|
c := &b.changes[end]
|
||||||
|
|
||||||
// Check that we won't exceed 1000 ResourceRecords in the request.
|
// Check that we won't exceed 1000 ResourceRecords in the request.
|
||||||
|
if c.ResourceRecordSet == nil {
|
||||||
|
end++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
rrsetSize := len(c.ResourceRecordSet.ResourceRecords)
|
rrsetSize := len(c.ResourceRecordSet.ResourceRecords)
|
||||||
if c.Action == r53Types.ChangeActionUpsert {
|
if c.Action == r53Types.ChangeActionUpsert {
|
||||||
// "When the value of the Action element is UPSERT, each ResourceRecord element is counted twice."
|
// "When the value of the Action element is UPSERT, each ResourceRecord element is counted twice."
|
||||||
|
Reference in New Issue
Block a user