mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 13:46:07 +08:00
# Issue * New record type: "RP" (supported by BIND and GANDI_V5) * Cloudflare: CF_REDIRECT/CF_TEMP_REDIRECT now generate CF_SINGLE_REDIRECT records. All PAGE_RULE-based code is removed. PAGE_RULEs are deprecated at Cloudflare. (be careful when upgrading!) * New "v2" RecordConfig: RP and CF_SINGLE_REDIRECT are the only record types that use this method. It shifts most of the work out of JavaScript and into the Go code, making new record types easier to make, easier to test, and easier to use by providers. This opens the door to new things like a potential code-generator for rtypes. Converting existing rtypes will happen over the next year. * When only the TTL changes (MODIFY-TTL), the output lists the TTL change first, not at the end of the line where it is visually lost. * CF_REDIRECT/CF_TEMP_REDIRECT generate different rule "names". They will be updated the first time you "push" with this release. The order of the rules may also change. If you rules depend on a particular order, be very careful with this upgrade! Refactoring: * New "v2" RecordConfig: Record types using this new method simply package the parameters from dnsconfig.js statements like CF_REDIRECT(foo,bar) and send them (raw) to the Go code. The Go code does all processing, validation, etc. and turns them into RecordConfig that store all the rdata in `RecordConfig.F`. No more adding fields to RecordConfig for each new record type! * RecordConfig.IsModernType() returns true if the record uses the new v2 record mechanism. * PostProcess is now a method on DnsConfig and DomainConfig. * DOC: How to create new rtypes using the v2 method (incomplete) Other things: * Integration tests for CF "full proxy" are removed. This feature doesn't exist any more. * DEV: Debugger tips now includes VSCode advice * TESTING: The names of testgroup's can now have extra spaces to make data align better * CF_TEMP_REDIRECT/CF_REDIRECT is now a "builder" that generates CLOUDFLAREAPI_SINGLE_REDIRECT records. * And more! # Resolution --------- Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
345 lines
11 KiB
Go
345 lines
11 KiB
Go
package diff2
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
func analyzeByRecordSet(cc *CompareConfig) (ChangeList, int) {
|
|
var instructions ChangeList
|
|
var actualChangeCount int
|
|
// For each label...
|
|
for _, lc := range cc.ldata {
|
|
// for each type at that label...
|
|
for _, rt := range lc.tdata {
|
|
// ...if there are changes generate an instruction.
|
|
ets := rt.existingTargets
|
|
dts := rt.desiredTargets
|
|
cs := diffTargets(ets, dts)
|
|
actualChangeCount += len(cs)
|
|
msgs := justMsgs(cs)
|
|
if len(msgs) == 0 { // No differences?
|
|
// The records at this rset are the same. No work to be done.
|
|
continue
|
|
}
|
|
if len(ets) == 0 { // Create a new label.
|
|
instructions = append(instructions, mkAdd(lc.label, rt.rType, msgs, rt.desiredRecs))
|
|
} else if len(dts) == 0 { // Delete that label and all its records.
|
|
instructions = append(instructions, mkDelete(lc.label, rt.rType, msgs, rt.existingRecs))
|
|
} else { // Change the records at that label
|
|
instructions = append(instructions, mkChange(lc.label, rt.rType, msgs, rt.existingRecs, rt.desiredRecs))
|
|
}
|
|
}
|
|
}
|
|
|
|
instructions = orderByDependencies(instructions)
|
|
|
|
return instructions, actualChangeCount
|
|
}
|
|
|
|
func analyzeByLabel(cc *CompareConfig) (ChangeList, int) {
|
|
var instructions ChangeList
|
|
var actualChangeCount int
|
|
// Accumulate any changes and collect the info needed to generate instructions.
|
|
for _, lc := range cc.ldata {
|
|
// for each type at that label...
|
|
label := lc.label
|
|
var accMsgs []string
|
|
var accExisting models.Records
|
|
var accDesired models.Records
|
|
msgsByKey := map[models.RecordKey][]string{}
|
|
for _, rt := range lc.tdata {
|
|
// for each type at that label...
|
|
ets := rt.existingTargets
|
|
dts := rt.desiredTargets
|
|
cs := diffTargets(ets, dts)
|
|
actualChangeCount += len(cs)
|
|
msgs := justMsgs(cs)
|
|
k := models.RecordKey{NameFQDN: label, Type: rt.rType}
|
|
msgsByKey[k] = msgs
|
|
accMsgs = append(accMsgs, msgs...) // Accumulate the messages
|
|
accExisting = append(accExisting, rt.existingRecs...) // Accumulate records existing at this label.
|
|
accDesired = append(accDesired, rt.desiredRecs...) // Accumulate records desired at this label.
|
|
}
|
|
|
|
// We now know what changed (accMsgs),
|
|
// what records USED TO EXIST at that label (accExisting),
|
|
// and what records SHOULD EXIST at that label (accDesired).
|
|
// Based on that info, we can generate the instructions.
|
|
|
|
if len(accMsgs) == 0 { // Nothing changed.
|
|
} else if len(accDesired) == 0 { // No new records at the label? This must be a delete.
|
|
instructions = append(instructions, mkDelete(label, "", accMsgs, accExisting))
|
|
} else if len(accExisting) == 0 { // No old records at the label? This must be a change.
|
|
c := mkAdd(label, "", accMsgs, accDesired)
|
|
c.MsgsByKey = msgsByKey
|
|
instructions = append(instructions, c)
|
|
} else { // If we get here, it must be a change.
|
|
instructions = append(instructions, mkChange(label, "", accMsgs, accExisting, accDesired))
|
|
}
|
|
}
|
|
|
|
instructions = orderByDependencies(instructions)
|
|
|
|
return instructions, actualChangeCount
|
|
}
|
|
|
|
func analyzeByRecord(cc *CompareConfig) (ChangeList, int) {
|
|
var instructions ChangeList
|
|
var actualChangeCount int
|
|
// For each label, for each type at that label, see if there are any changes.
|
|
for _, lc := range cc.ldata {
|
|
for _, rt := range lc.tdata {
|
|
ets := rt.existingTargets
|
|
dts := rt.desiredTargets
|
|
cs := diffTargets(ets, dts)
|
|
actualChangeCount += len(cs)
|
|
instructions = append(instructions, cs...)
|
|
}
|
|
}
|
|
|
|
instructions = orderByDependencies(instructions)
|
|
|
|
return instructions, actualChangeCount
|
|
}
|
|
|
|
// FYI: there is no analyzeByZone. diff2.ByZone() calls analyzeByRecords().
|
|
|
|
func mkAdd(l string, t string, msgs []string, newRecs models.Records) Change {
|
|
c := Change{Type: CREATE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
|
|
c.Key.NameFQDN = l
|
|
c.Key.Type = t
|
|
c.New = newRecs
|
|
return c
|
|
}
|
|
|
|
func mkChange(l string, t string, msgs []string, oldRecs, newRecs models.Records) Change {
|
|
c := Change{Type: CHANGE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
|
|
c.Key.NameFQDN = l
|
|
c.Key.Type = t
|
|
c.Old = oldRecs
|
|
c.New = newRecs
|
|
return c
|
|
}
|
|
|
|
func mkDelete(l string, t string, msgs []string, oldRecs models.Records) Change {
|
|
c := Change{Type: DELETE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
|
|
c.Key.NameFQDN = l
|
|
c.Key.Type = t
|
|
c.Old = oldRecs
|
|
return c
|
|
}
|
|
|
|
func removeCommon(existing, desired []targetConfig) ([]targetConfig, []targetConfig) {
|
|
// Sort by comparableFull.
|
|
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull })
|
|
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull })
|
|
|
|
// Build maps required by filterBy
|
|
eKeys := map[string]*targetConfig{}
|
|
for _, v := range existing {
|
|
v := v
|
|
eKeys[v.comparableFull] = &v
|
|
}
|
|
dKeys := map[string]*targetConfig{}
|
|
for _, v := range desired {
|
|
v := v
|
|
dKeys[v.comparableFull] = &v
|
|
}
|
|
|
|
return filterBy(existing, dKeys), filterBy(desired, eKeys)
|
|
}
|
|
|
|
// findTTLChanges finds the records that ONLY change their TTL. For those, generate a Change.
|
|
// Remove such items from the list.
|
|
func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetConfig, ChangeList) {
|
|
if (len(existing) == 0) || (len(desired) == 0) {
|
|
return existing, desired, nil
|
|
}
|
|
|
|
// Sort by comparableNoTTL
|
|
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableNoTTL < existing[j].comparableNoTTL })
|
|
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableNoTTL < desired[j].comparableNoTTL })
|
|
|
|
var instructions ChangeList
|
|
var existDiff, desiredDiff []targetConfig
|
|
ei := 0
|
|
di := 0
|
|
for (ei < len(existing)) && (di < len(desired)) {
|
|
er := existing[ei].rec
|
|
dr := desired[di].rec
|
|
ecomp := existing[ei].comparableNoTTL
|
|
dcomp := desired[di].comparableNoTTL
|
|
|
|
if ecomp == dcomp && er.TTL == dr.TTL {
|
|
panic(fmt.Sprintf(
|
|
"Should not happen. There should be some difference! ecomp=%q dcomp=%q er.TTL=%v dr.TTL=%v\n",
|
|
ecomp, dcomp, er.TTL, dr.TTL))
|
|
}
|
|
|
|
if ecomp == dcomp && er.TTL != dr.TTL {
|
|
m := color.YellowString("± MODIFY-TTL %s %s %s", dr.NameFQDN, dr.Type, humanDiff(existing[ei], desired[di]))
|
|
v := mkChange(dr.NameFQDN, dr.Type, []string{m},
|
|
models.Records{er},
|
|
models.Records{dr},
|
|
)
|
|
v.HintOnlyTTL = true
|
|
instructions = append(instructions, v)
|
|
ei++
|
|
di++
|
|
} else if ecomp < dcomp {
|
|
existDiff = append(existDiff, existing[ei])
|
|
ei++
|
|
} else if ecomp > dcomp {
|
|
desiredDiff = append(desiredDiff, desired[di])
|
|
di++
|
|
} else {
|
|
panic("should not happen. ecomp cant be both == and != dcomp")
|
|
}
|
|
}
|
|
|
|
// Any remainder goes to the *Diff result:
|
|
if ei < len(existing) {
|
|
existDiff = append(existDiff, existing[ei:]...)
|
|
}
|
|
if di < len(desired) {
|
|
desiredDiff = append(desiredDiff, desired[di:]...)
|
|
}
|
|
|
|
return existDiff, desiredDiff, instructions
|
|
}
|
|
|
|
// Return s but remove any items that can be found in m.
|
|
func filterBy(s []targetConfig, m map[string]*targetConfig) []targetConfig {
|
|
i := 0 // output index
|
|
for _, x := range s {
|
|
if _, ok := m[x.comparableFull]; !ok {
|
|
// copy and increment index
|
|
s[i] = x
|
|
i++
|
|
}
|
|
}
|
|
// // Prevent memory leak by erasing truncated values
|
|
// // (not needed if values don't contain pointers, directly or indirectly)
|
|
// for j := i; j < len(s); j++ {
|
|
// s[j] = nil
|
|
// }
|
|
s = s[:i]
|
|
return s
|
|
}
|
|
|
|
// humanDiff returns a human-friendly string showing what changed
|
|
// between a and b.
|
|
func humanDiff(a, b targetConfig) string {
|
|
// TODO(tlim): Records like MX and SRV should have more clever output.
|
|
// For example if only the MX priority changes, show just that.
|
|
|
|
if a.comparableNoTTL != b.comparableNoTTL {
|
|
// The recorddata is different:
|
|
return fmt.Sprintf("(%s) -> (%s)", a.comparableFull, b.comparableFull)
|
|
}
|
|
|
|
// Just the TTLs are different:
|
|
return fmt.Sprintf("ttl=(%d->%d) %s",
|
|
a.rec.TTL, b.rec.TTL,
|
|
a.comparableNoTTL)
|
|
}
|
|
|
|
var echRe = regexp.MustCompile(`ech="?([\w+/=]+)"?`)
|
|
|
|
func diffTargets(existing, desired []targetConfig) ChangeList {
|
|
// fmt.Printf("DEBUG: diffTargets(\nexisting=%v\ndesired=%v\nDEBUG.\n", existing, desired)
|
|
|
|
// Nothing to do?
|
|
if len(existing) == 0 && len(desired) == 0 {
|
|
return nil
|
|
}
|
|
|
|
echs := make(map[string]string)
|
|
for _, v := range existing {
|
|
matches := echRe.FindStringSubmatch(v.rec.SvcParams)
|
|
if len(matches) == 2 {
|
|
echs[v.rec.NameFQDN] = matches[1]
|
|
}
|
|
}
|
|
for i, v := range desired {
|
|
if strings.Contains(v.rec.SvcParams, "ech=IGNORE") {
|
|
var unquoted, quoted string
|
|
if _, ok := echs[v.rec.NameFQDN]; ok {
|
|
unquoted = fmt.Sprintf("ech=%s", echs[v.rec.NameFQDN])
|
|
quoted = fmt.Sprintf("ech=%q", echs[v.rec.NameFQDN])
|
|
} else {
|
|
unquoted = ""
|
|
quoted = ""
|
|
}
|
|
v.rec.SvcParams = echRe.ReplaceAllString(v.rec.SvcParams, unquoted)
|
|
v.comparableFull = echRe.ReplaceAllString(v.comparableFull, quoted)
|
|
v.comparableNoTTL = echRe.ReplaceAllString(v.comparableNoTTL, quoted)
|
|
}
|
|
desired[i] = v
|
|
}
|
|
|
|
var instructions ChangeList
|
|
|
|
// remove the exact matches.
|
|
existing, desired = removeCommon(existing, desired)
|
|
|
|
// At this point the exact matches are removed. However there may be
|
|
// records that have the same GetTargetCombined() but different
|
|
// TTLs.
|
|
|
|
existing, desired, newChanges := findTTLChanges(existing, desired)
|
|
instructions = append(instructions, newChanges...)
|
|
|
|
// Sort by comparableFull
|
|
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull })
|
|
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull })
|
|
|
|
// the remaining chunks are changes (regardless of TTL)
|
|
mi := min(len(existing), len(desired))
|
|
for i := range mi {
|
|
er := existing[i].rec
|
|
dr := desired[i].rec
|
|
|
|
m := color.YellowString("± MODIFY %s %s %s", dr.NameFQDN, dr.Type, humanDiff(existing[i], desired[i]))
|
|
|
|
mkc := mkChange(dr.NameFQDN, dr.Type, []string{m}, models.Records{er}, models.Records{dr})
|
|
if len(existing) == 1 && len(desired) == 1 {
|
|
// If the tdata has exactly 1 item, drop a hint to the providers.
|
|
// For example, MSDNS can use a more efficient command if it knows
|
|
// that `Get-DnsServerResourceRecord -Name FOO -RRType A` will
|
|
// return exactly one record.
|
|
mkc.HintRecordSetLen1 = true
|
|
}
|
|
instructions = append(instructions, mkc)
|
|
}
|
|
|
|
// any left-over existing are deletes
|
|
for i := mi; i < len(existing); i++ {
|
|
er := existing[i].rec
|
|
m := color.RedString("- DELETE %s %s %s", er.NameFQDN, er.Type, existing[i].comparableFull)
|
|
instructions = append(instructions, mkDelete(er.NameFQDN, er.Type, []string{m}, models.Records{er}))
|
|
}
|
|
|
|
// any left-over desired are creates
|
|
for i := mi; i < len(desired); i++ {
|
|
dr := desired[i].rec
|
|
m := color.GreenString("+ CREATE %s %s %s", dr.NameFQDN, dr.Type, desired[i].comparableFull)
|
|
instructions = append(instructions, mkAdd(dr.NameFQDN, dr.Type, []string{m}, models.Records{dr}))
|
|
}
|
|
|
|
return instructions
|
|
}
|
|
|
|
func justMsgs(cl ChangeList) []string {
|
|
var msgs []string
|
|
for _, c := range cl {
|
|
msgs = append(msgs, c.Msgs...)
|
|
}
|
|
return msgs
|
|
}
|