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
|
||||
// label but the pre-existing label has its TTL change.
|
||||
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("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"`
|
||||
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"`
|
||||
IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"`
|
||||
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"`
|
||||
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"`
|
||||
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"` // UNMANAGED()
|
||||
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"` // DISABLE_UNMANAGED_SAFETY_CHECK
|
||||
|
||||
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
||||
//DNSSEC bool `json:"dnssec,omitempty"`
|
||||
@ -36,14 +38,6 @@ type DomainConfig struct {
|
||||
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.
|
||||
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
|
||||
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.
|
||||
R53Alias map[string]string `json:"r53_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
|
||||
// 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}
|
||||
switch inst.Type {
|
||||
case diff2.REPORT:
|
||||
// Sadly the NewCompat function doesn't have a way to do this.
|
||||
// Purge reports are silently skipped.
|
||||
// Sadly the NewCompat function doesn't have an equivalent. We
|
||||
// just output the messages now.
|
||||
fmt.Println(inst.MsgsJoined)
|
||||
case diff2.CREATE:
|
||||
cor.Desired = inst.New[0]
|
||||
create = append(create, cor)
|
||||
|
@ -109,10 +109,8 @@ func NewCompareConfig(origin string, existing, desired models.Records, compFn Co
|
||||
func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
||||
|
||||
// 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
|
||||
// true. That said, a little paranoia is healthy. Those familiar
|
||||
// with the Therac-25 accident will agree:
|
||||
// https://hackaday.com/2015/10/26/killed-by-a-machine-the-therac-25/
|
||||
// something that can not happen. In my I've proved this to be
|
||||
// true. That said, a little paranoia is healthy.
|
||||
|
||||
// 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
|
||||
@ -141,15 +139,28 @@ func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
||||
|
||||
for _, ld := range cc.ldata {
|
||||
for j, td := range ld.tdata {
|
||||
|
||||
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 {
|
||||
//fmt.Printf("DEBUG: cname in existing: index=%d\n", j)
|
||||
if j != 0 {
|
||||
panic("should not happen: (CNAME not in first position)")
|
||||
}
|
||||
}
|
||||
|
||||
if len(td.desiredTargets) != 0 {
|
||||
//fmt.Printf("DEBUG: cname in desired: index=%d\n", j)
|
||||
//fmt.Printf("DEBUG: highest: index=%d\n", j)
|
||||
if j != highest(ld.tdata) {
|
||||
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 {
|
||||
|
||||
//label := rec.NameFQDN
|
||||
//rtype := rec.Type
|
||||
key := rec.Key()
|
||||
label := key.NameFQDN
|
||||
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?
|
||||
var labelIdx int
|
||||
if _, ok := cc.labelMap[label]; !ok {
|
||||
//fmt.Printf("DEBUG: I haven't see label=%v before. Adding.\n", label)
|
||||
cc.labelMap[label] = true
|
||||
cc.ldata = append(cc.ldata, &labelConfig{label: label})
|
||||
labelIdx = highest(cc.ldata)
|
||||
} else {
|
||||
// find label in cc.ldata:
|
||||
for k, v := range cc.ldata {
|
||||
if v.label == label {
|
||||
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?
|
||||
//key := rec.Key()
|
||||
if _, ok := cc.keyMap[key]; !ok {
|
||||
//fmt.Printf("DEBUG: I haven't see key=%v before. Adding.\n", key)
|
||||
cc.keyMap[key] = true
|
||||
x := cc.ldata[labelIdx]
|
||||
//fmt.Printf("DEBUG: appending rtype=%v\n", rtype)
|
||||
x.tdata = append(x.tdata, &rTypeConfig{rType: rtype})
|
||||
}
|
||||
var rtIdx int
|
||||
@ -256,7 +260,6 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
||||
break
|
||||
}
|
||||
}
|
||||
//fmt.Printf("DEBUG: found rtype=%v at index %d\n", rtype, rtIdx)
|
||||
|
||||
// Now it is safe to add/modify the records.
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package diff2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -9,11 +8,6 @@ import (
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
)
|
||||
|
||||
func prettyPrint(i interface{}) string {
|
||||
s, _ := json.MarshalIndent(i, "", "\t")
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func TestNewCompareConfig(t *testing.T) {
|
||||
type args struct {
|
||||
origin string
|
||||
|
@ -8,6 +8,7 @@ package diff2
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
)
|
||||
@ -96,18 +97,7 @@ General instructions:
|
||||
//
|
||||
// Examples include:
|
||||
func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
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
|
||||
return byHelper(analyzeByRecordSet, existing, dc, compFunc)
|
||||
}
|
||||
|
||||
// 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:
|
||||
func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
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
|
||||
return byHelper(analyzeByLabel, existing, dc, compFunc)
|
||||
}
|
||||
|
||||
// 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
|
||||
func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
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
|
||||
return byHelper(analyzeByRecord, existing, dc, compFunc)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// The user should see a list of changes as if individual records were
|
||||
// updated.
|
||||
// The user should see a list of changes as if individual records were updated.
|
||||
//
|
||||
// The caller of this function should:
|
||||
// Example usage:
|
||||
//
|
||||
// changed, msgs := diff2.ByZone(existing, desired, origin, nil
|
||||
// fmt.Sprintf("CREATING ZONEFILE FOR THE FIRST TIME: dir/example.com.zone"))
|
||||
// if changed {
|
||||
// // output msgs
|
||||
// // generate the zone using the "desired" records
|
||||
// }
|
||||
// msgs, changes, err := diff2.ByZone(foundRecords, dc, nil)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if changes {
|
||||
// // Generate a "correction" that uploads the entire zone.
|
||||
// // (dc.Records are the new records for the zone).
|
||||
// }
|
||||
//
|
||||
// Example providers include: BIND
|
||||
func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
if len(existing) == 0 {
|
||||
// Nothing previously existed. No need to output a list of individual changes.
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
desired := dc.Records
|
||||
var err error
|
||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
||||
// Only return the messages.
|
||||
instructions, err := byHelper(analyzeByRecord, existing, dc, compFunc)
|
||||
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 {
|
||||
return nil, false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Regroup existing/desiredd for easy comparison:
|
||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||
instructions := analyzeByRecord(cc)
|
||||
instructions = processPurge(instructions, !dc.KeepUnknown)
|
||||
return justMsgs(instructions), len(instructions) != 0, nil
|
||||
|
||||
// Analyze and generate the instructions:
|
||||
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 {
|
||||
var buf bytes.Buffer
|
||||
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
|
||||
// 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)
|
||||
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,
|
||||
meta: {},
|
||||
records: [],
|
||||
recordsabsent: [],
|
||||
dnsProviders: {},
|
||||
defaultTTL: 0,
|
||||
nameservers: [],
|
||||
@ -614,7 +615,6 @@ function IGNORE_NAME(name, rTypes) {
|
||||
d.unmanaged.push({
|
||||
label_pattern: name,
|
||||
rType_pattern: rTypes,
|
||||
target_pattern: '*',
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -632,7 +632,6 @@ function IGNORE_TARGET(target, rType) {
|
||||
return function (d) {
|
||||
d.ignored_targets.push({ pattern: target, type: rType });
|
||||
d.unmanaged.push({
|
||||
label_pattern: '*',
|
||||
rType_pattern: rType,
|
||||
target_pattern: target,
|
||||
});
|
||||
@ -660,6 +659,22 @@ function NO_PURGE(d) {
|
||||
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
|
||||
// Permitted values are:
|
||||
// "" Do not modify the setting (the default)
|
||||
@ -678,15 +693,6 @@ function AUTODNSSEC(d) {
|
||||
}
|
||||
|
||||
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) {
|
||||
d.unmanaged.push({
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
@ -45,6 +45,11 @@ func ExecuteJavascript(file string, devMode bool, variables map[string]string) (
|
||||
// Record the directory path leading up to this 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()
|
||||
l := loop.New(vm)
|
||||
|
||||
|
@ -46,45 +46,37 @@
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "testignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore2",
|
||||
"rType_pattern": "A",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "A"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore3",
|
||||
"rType_pattern": "A, CNAME, TXT",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "A, CNAME, TXT"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore4",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "*",
|
||||
"rType_pattern": "CNAME",
|
||||
"target_pattern": "testtarget"
|
||||
},
|
||||
{
|
||||
"label_pattern": "legacyignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "@",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "*",
|
||||
"rType_pattern": "CNAME",
|
||||
"target_pattern": "@"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,9 @@
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "\\*.testignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
D("foo.com", "none"
|
||||
, UNMANAGED("one")
|
||||
, UNMANAGED("two", "A, CNAME")
|
||||
, UNMANAGED("three", "TXT", "findme")
|
||||
, UNMANAGED("notype", "", "targglob")
|
||||
, UNMANAGED("", "", "targetGlob1")
|
||||
, UNMANAGED("", "CNAME", "")
|
||||
, UNMANAGED("", "A", "targetGlob3")
|
||||
, UNMANAGED("lab4")
|
||||
, UNMANAGED("notype", "", "targetGlob5")
|
||||
, UNMANAGED("lab6", "A, CNAME")
|
||||
, UNMANAGED("lab7", "TXT", "targetGlob7")
|
||||
);
|
||||
|
@ -1,34 +1,40 @@
|
||||
{
|
||||
"registrars": [],
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"name": "foo.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"name": "foo.com",
|
||||
"records": [],
|
||||
"registrar": "none",
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "one",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
"target_pattern": "targetGlob1"
|
||||
},
|
||||
{
|
||||
"label_pattern": "two",
|
||||
"rType_pattern": "A, CNAME",
|
||||
"target_pattern": "*"
|
||||
"rType_pattern": "CNAME"
|
||||
},
|
||||
{
|
||||
"label_pattern": "three",
|
||||
"rType_pattern": "TXT",
|
||||
"target_pattern": "findme"
|
||||
"rType_pattern": "A",
|
||||
"target_pattern": "targetGlob3"
|
||||
},
|
||||
{
|
||||
"label_pattern": "lab4"
|
||||
},
|
||||
{
|
||||
"label_pattern": "notype",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "targglob"
|
||||
"target_pattern": "targetGlob5"
|
||||
},
|
||||
{
|
||||
"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.
|
||||
func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||
foundRecords := models.Records{}
|
||||
|
||||
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
||||
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
|
||||
|
||||
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() {
|
||||
rec, err := models.RRtoRC(rr, domain)
|
||||
rec, err := models.RRtoRC(rr, zoneName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -188,7 +194,7 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -489,7 +489,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
switch inst.Type {
|
||||
|
||||
case diff2.REPORT:
|
||||
corrections = append(corrections, &models.Correction{Msg: inst.MsgsJoined})
|
||||
chg = r53Types.Change{}
|
||||
|
||||
case diff2.CREATE:
|
||||
fallthrough
|
||||
@ -586,8 +586,9 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
func reorderInstructions(changes diff2.ChangeList) diff2.ChangeList {
|
||||
var main, tail diff2.ChangeList
|
||||
for _, change := range changes {
|
||||
//if change.Key.Type == "R53_ALIAS" {
|
||||
if strings.HasPrefix(change.Key.Type, "R53_ALIAS_") {
|
||||
// Reports should be early in the list.
|
||||
// R53_ALIAS_ records should go to the tail.
|
||||
if change.Type != diff2.REPORT && strings.HasPrefix(change.Key.Type, "R53_ALIAS_") {
|
||||
tail = append(tail, change)
|
||||
} else {
|
||||
main = append(main, change)
|
||||
@ -889,6 +890,11 @@ func (b *changeBatcher) Next() bool {
|
||||
c := &b.changes[end]
|
||||
|
||||
// Check that we won't exceed 1000 ResourceRecords in the request.
|
||||
if c.ResourceRecordSet == nil {
|
||||
end++
|
||||
continue
|
||||
}
|
||||
|
||||
rrsetSize := len(c.ResourceRecordSet.ResourceRecords)
|
||||
if c.Action == r53Types.ChangeActionUpsert {
|
||||
// "When the value of the Action element is UPSERT, each ResourceRecord element is counted twice."
|
||||
|
Reference in New Issue
Block a user