2016-08-23 08:31:50 +08:00
package diff
import (
"fmt"
2017-05-03 23:46:39 +08:00
"log"
2016-08-23 08:31:50 +08:00
"sort"
2017-01-12 03:38:07 +08:00
"github.com/StackExchange/dnscontrol/models"
2018-02-01 23:32:38 +08:00
"github.com/miekg/dns/dnsutil"
2016-08-23 08:31:50 +08:00
)
2018-01-10 01:53:16 +08:00
// Correlation stores a difference between two domains.
2017-01-12 03:38:07 +08:00
type Correlation struct {
d * differ
Existing * models . RecordConfig
Desired * models . RecordConfig
}
2018-01-10 01:53:16 +08:00
// Changeset stores many Correlation.
2017-01-12 03:38:07 +08:00
type Changeset [ ] Correlation
2016-08-23 08:31:50 +08:00
2018-01-10 01:53:16 +08:00
// Differ is an interface for computing the difference between two zones.
2017-01-12 03:38:07 +08:00
type Differ interface {
2018-01-10 01:53:16 +08:00
// IncrementalDiff performs a diff on a record-by-record basis, and returns a sets for which records need to be created, deleted, or modified.
2017-01-12 03:38:07 +08:00
IncrementalDiff ( existing [ ] * models . RecordConfig ) ( unchanged , create , toDelete , modify Changeset )
2017-09-13 23:49:15 +08:00
// ChangedGroups performs a diff more appropriate for providers with a "RecordSet" model, where all records with the same name and type are grouped.
// Individual record changes are often not useful in such scenarios. Instead we return a map of record keys to a list of change descriptions within that group.
ChangedGroups ( existing [ ] * models . RecordConfig ) map [ models . RecordKey ] [ ] string
2016-08-23 08:31:50 +08:00
}
2018-01-10 01:53:16 +08:00
// New is a constructor for a Differ.
2017-01-12 03:38:07 +08:00
func New ( dc * models . DomainConfig , extraValues ... func ( * models . RecordConfig ) map [ string ] string ) Differ {
return & differ {
dc : dc ,
extraValues : extraValues ,
}
}
type differ struct {
dc * models . DomainConfig
extraValues [ ] func ( * models . RecordConfig ) map [ string ] string
}
// get normalized content for record. target, ttl, mxprio, and specified metadata
func ( d * differ ) content ( r * models . RecordConfig ) string {
2017-07-20 03:53:40 +08:00
content := fmt . Sprintf ( "%v ttl=%d" , r . Content ( ) , r . TTL )
2017-01-12 03:38:07 +08:00
for _ , f := range d . extraValues {
ROUTE53: Support Route53's ALIAS record type (#239) (#301)
* Stable comparison of metadata (#239)
Iterating over a map in Go never produces twice the same ordering.
Thus when comparing two metadata map with more than one key, the
`differ` is always finding differences.
To properly compare records metadata, we need to iterate the maps
in a deterministic way.
Signed-off-by: Brice Figureau <brice@daysofwonder.com>
* Support for Route53 ALIAS record type (#239)
Route53 ALIAS doesn't behave like a regular ALIAS, and is much more
limited as its target can only be some specific AWS resources or
another record in the same zone.
According to #239, this change adds a new directive R53_ALIAS which
implements this specific alias. This record type can only be used
with the Route53 provider.
This directive usage looks like this:
```js
D("example.com", REGISTRAR, DnsProvider("ROUTE53"),
R53_ALIAS("foo1", "A", "bar") // record in same zone
R53_ALIAS("foo2", "A",
"blahblah.elasticloadbalancing.us-west-1.amazonaws.com",
R53_ZONE('Z368ELLRRE2KJ0')) // ELB in us-west-1
```
Unfortunately, Route53 requires indicating the hosted zone id
where the target is defined (those are listed in AWS documentation,
see the R53_ALIAS documentation for links).
2018-01-16 18:53:12 +08:00
// sort the extra values map keys to perform a deterministic
// comparison since Golang maps iteration order is not guaranteed
valueMap := f ( r )
keys := make ( [ ] string , 0 )
for k := range valueMap {
keys = append ( keys , k )
}
sort . Strings ( keys )
for _ , k := range keys {
v := valueMap [ k ]
2017-01-12 03:38:07 +08:00
content += fmt . Sprintf ( " %s=%s" , k , v )
}
}
return content
2016-08-23 08:31:50 +08:00
}
2017-01-12 03:38:07 +08:00
func ( d * differ ) IncrementalDiff ( existing [ ] * models . RecordConfig ) ( unchanged , create , toDelete , modify Changeset ) {
2016-08-23 08:31:50 +08:00
unchanged = Changeset { }
create = Changeset { }
toDelete = Changeset { }
modify = Changeset { }
2017-01-12 03:38:07 +08:00
desired := d . dc . Records
2016-08-23 08:31:50 +08:00
2018-01-10 01:53:16 +08:00
// sort existing and desired by name
2016-08-23 08:31:50 +08:00
type key struct {
name , rType string
}
2017-01-12 03:38:07 +08:00
existingByNameAndType := map [ key ] [ ] * models . RecordConfig { }
desiredByNameAndType := map [ key ] [ ] * models . RecordConfig { }
2016-08-23 08:31:50 +08:00
for _ , e := range existing {
2018-02-01 23:32:38 +08:00
if d . matchIgnored ( e . NameFQDN , d . dc . Name ) {
log . Printf ( "Ignoring record %s %s due to IGNORE" , e . NameFQDN , e . Type )
2018-01-16 04:39:29 +08:00
} else {
k := key { e . NameFQDN , e . Type }
existingByNameAndType [ k ] = append ( existingByNameAndType [ k ] , e )
}
2016-08-23 08:31:50 +08:00
}
2018-01-16 04:39:29 +08:00
for _ , dr := range desired {
2018-02-01 23:32:38 +08:00
if d . matchIgnored ( dr . NameFQDN , d . dc . Name ) {
panic ( fmt . Sprintf ( "Trying to update/add IGNOREd record: %s %s" , dr . NameFQDN , dr . Type ) )
2018-01-16 04:39:29 +08:00
} else {
k := key { dr . NameFQDN , dr . Type }
desiredByNameAndType [ k ] = append ( desiredByNameAndType [ k ] , dr )
}
2016-08-23 08:31:50 +08:00
}
2018-01-10 01:53:16 +08:00
// if NO_PURGE is set, just remove anything that is only in existing.
2017-05-03 23:46:39 +08:00
if d . dc . KeepUnknown {
for k := range existingByNameAndType {
if _ , ok := desiredByNameAndType [ k ] ; ! ok {
log . Printf ( "Ignoring record set %s %s due to NO_PURGE" , k . rType , k . name )
delete ( existingByNameAndType , k )
}
}
}
2017-01-12 03:38:07 +08:00
// Look through existing records. This will give us changes and deletions and some additions.
// Each iteration is only for a single type/name record set
2016-08-23 08:31:50 +08:00
for key , existingRecords := range existingByNameAndType {
desiredRecords := desiredByNameAndType [ key ]
2018-01-10 01:53:16 +08:00
// first look through records that are the same target on both sides. Those are either modifications or unchanged
2016-08-23 08:31:50 +08:00
for i := len ( existingRecords ) - 1 ; i >= 0 ; i -- {
ex := existingRecords [ i ]
for j , de := range desiredRecords {
2017-01-12 03:38:07 +08:00
if de . Target == ex . Target {
2018-01-10 01:53:16 +08:00
// they're either identical or should be a modification of each other (ttl or metadata changes)
2017-01-12 03:38:07 +08:00
if d . content ( de ) == d . content ( ex ) {
unchanged = append ( unchanged , Correlation { d , ex , de } )
2016-08-23 08:31:50 +08:00
} else {
2017-01-12 03:38:07 +08:00
modify = append ( modify , Correlation { d , ex , de } )
2016-08-23 08:31:50 +08:00
}
// remove from both slices by index
existingRecords = existingRecords [ : i + copy ( existingRecords [ i : ] , existingRecords [ i + 1 : ] ) ]
desiredRecords = desiredRecords [ : j + copy ( desiredRecords [ j : ] , desiredRecords [ j + 1 : ] ) ]
break
}
}
}
2017-01-12 03:38:07 +08:00
desiredLookup := map [ string ] * models . RecordConfig { }
existingLookup := map [ string ] * models . RecordConfig { }
// build index based on normalized content data
2016-08-23 08:31:50 +08:00
for _ , ex := range existingRecords {
2017-01-12 03:38:07 +08:00
normalized := d . content ( ex )
2016-08-23 08:31:50 +08:00
if existingLookup [ normalized ] != nil {
panic ( fmt . Sprintf ( "DUPLICATE E_RECORD FOUND: %s %s" , key , normalized ) )
}
existingLookup [ normalized ] = ex
}
for _ , de := range desiredRecords {
2017-01-12 03:38:07 +08:00
normalized := d . content ( de )
2016-08-23 08:31:50 +08:00
if desiredLookup [ normalized ] != nil {
panic ( fmt . Sprintf ( "DUPLICATE D_RECORD FOUND: %s %s" , key , normalized ) )
}
desiredLookup [ normalized ] = de
}
// if a record is in both, it is unchanged
for norm , ex := range existingLookup {
if de , ok := desiredLookup [ norm ] ; ok {
2017-01-12 03:38:07 +08:00
unchanged = append ( unchanged , Correlation { d , ex , de } )
2016-08-23 08:31:50 +08:00
delete ( existingLookup , norm )
delete ( desiredLookup , norm )
}
}
2018-01-10 01:53:16 +08:00
// sort records by normalized text. Keeps behaviour deterministic
2017-01-12 03:38:07 +08:00
existingStrings , desiredStrings := sortedKeys ( existingLookup ) , sortedKeys ( desiredLookup )
2016-08-23 08:31:50 +08:00
// Modifications. Take 1 from each side.
for len ( desiredStrings ) > 0 && len ( existingStrings ) > 0 {
2017-01-12 03:38:07 +08:00
modify = append ( modify , Correlation { d , existingLookup [ existingStrings [ 0 ] ] , desiredLookup [ desiredStrings [ 0 ] ] } )
2016-08-23 08:31:50 +08:00
existingStrings = existingStrings [ 1 : ]
desiredStrings = desiredStrings [ 1 : ]
}
// If desired still has things they are additions
for _ , norm := range desiredStrings {
rec := desiredLookup [ norm ]
2017-01-12 03:38:07 +08:00
create = append ( create , Correlation { d , nil , rec } )
2016-08-23 08:31:50 +08:00
}
// if found , but not desired, delete it
for _ , norm := range existingStrings {
rec := existingLookup [ norm ]
2017-01-12 03:38:07 +08:00
toDelete = append ( toDelete , Correlation { d , rec , nil } )
2016-08-23 08:31:50 +08:00
}
// remove this set from the desired list to indicate we have processed it.
delete ( desiredByNameAndType , key )
}
2018-01-10 01:53:16 +08:00
// any name/type sets not already processed are pure additions
2016-08-23 08:31:50 +08:00
for name := range existingByNameAndType {
delete ( desiredByNameAndType , name )
}
for _ , desiredList := range desiredByNameAndType {
for _ , rec := range desiredList {
2017-01-12 03:38:07 +08:00
create = append ( create , Correlation { d , nil , rec } )
2016-08-23 08:31:50 +08:00
}
}
return
}
2017-09-13 23:49:15 +08:00
func ( d * differ ) ChangedGroups ( existing [ ] * models . RecordConfig ) map [ models . RecordKey ] [ ] string {
changedKeys := map [ models . RecordKey ] [ ] string { }
_ , create , delete , modify := d . IncrementalDiff ( existing )
for _ , c := range create {
changedKeys [ c . Desired . Key ( ) ] = append ( changedKeys [ c . Desired . Key ( ) ] , c . String ( ) )
}
for _ , d := range delete {
changedKeys [ d . Existing . Key ( ) ] = append ( changedKeys [ d . Existing . Key ( ) ] , d . String ( ) )
}
for _ , m := range modify {
changedKeys [ m . Desired . Key ( ) ] = append ( changedKeys [ m . Desired . Key ( ) ] , m . String ( ) )
}
return changedKeys
}
2016-08-23 08:31:50 +08:00
func ( c Correlation ) String ( ) string {
if c . Existing == nil {
2017-01-12 03:38:07 +08:00
return fmt . Sprintf ( "CREATE %s %s %s" , c . Desired . Type , c . Desired . NameFQDN , c . d . content ( c . Desired ) )
2016-08-23 08:31:50 +08:00
}
if c . Desired == nil {
2017-01-12 03:38:07 +08:00
return fmt . Sprintf ( "DELETE %s %s %s" , c . Existing . Type , c . Existing . NameFQDN , c . d . content ( c . Existing ) )
}
return fmt . Sprintf ( "MODIFY %s %s: (%s) -> (%s)" , c . Existing . Type , c . Existing . NameFQDN , c . d . content ( c . Existing ) , c . d . content ( c . Desired ) )
}
func sortedKeys ( m map [ string ] * models . RecordConfig ) [ ] string {
s := [ ] string { }
for v := range m {
s = append ( s , v )
2016-08-23 08:31:50 +08:00
}
2017-01-12 03:38:07 +08:00
sort . Strings ( s )
return s
2016-08-23 08:31:50 +08:00
}
2018-01-16 04:39:29 +08:00
2018-02-01 23:32:38 +08:00
func ( d * differ ) matchIgnored ( nameFQDN , domain string ) bool {
// ignored labels are not fqdn
name := dnsutil . TrimDomainName ( nameFQDN , domain )
2018-01-16 04:39:29 +08:00
for _ , tst := range d . dc . IgnoredLabels {
2018-02-01 23:32:38 +08:00
if name == tst || nameFQDN == tst {
2018-01-16 04:39:29 +08:00
return true
}
}
return false
}