2016-08-23 08:31:50 +08:00
package diff
import (
"fmt"
2022-11-08 00:27:04 +08:00
"regexp"
2016-08-23 08:31:50 +08:00
"sort"
2017-01-12 03:38:07 +08:00
2020-04-15 04:47:30 +08:00
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
2023-02-19 23:54:53 +08:00
"github.com/fatih/color"
2022-08-15 08:46:56 +08:00
"github.com/gobwas/glob"
2016-08-23 08:31:50 +08:00
)
2022-12-12 06:28:58 +08:00
// Correlation stores a difference between two records.
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.
2020-08-21 03:49:00 +08:00
IncrementalDiff ( existing [ ] * models . RecordConfig ) ( unchanged , create , toDelete , modify Changeset , err error )
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.
2020-08-21 03:49:00 +08:00
ChangedGroups ( existing [ ] * models . RecordConfig ) ( map [ models . RecordKey ] [ ] string , error )
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 ,
2019-05-27 22:14:29 +08:00
2020-08-18 23:14:34 +08:00
// compile IGNORE_NAME glob patterns
compiledIgnoredNames : compileIgnoredNames ( dc . IgnoredNames ) ,
// compile IGNORE_TARGET glob patterns
compiledIgnoredTargets : compileIgnoredTargets ( dc . IgnoredTargets ) ,
2017-01-12 03:38:07 +08:00
}
}
2022-11-08 00:27:04 +08:00
// An ignoredName must match both the name glob and one of the recordTypes in rTypes. If rTypes is empty, any
// record type will match.
type ignoredName struct {
nameGlob glob . Glob
rTypes [ ] string
}
2017-01-12 03:38:07 +08:00
type differ struct {
dc * models . DomainConfig
extraValues [ ] func ( * models . RecordConfig ) map [ string ] string
2019-05-27 22:14:29 +08:00
2022-11-08 00:27:04 +08:00
compiledIgnoredNames [ ] ignoredName
2020-08-18 23:14:34 +08:00
compiledIgnoredTargets [ ] glob . Glob
2017-01-12 03:38:07 +08:00
}
// get normalized content for record. target, ttl, mxprio, and specified metadata
func ( d * differ ) content ( r * models . RecordConfig ) string {
2022-08-12 04:13:24 +08:00
// get the extra values maps to add to the comparison.
2019-04-23 03:41:39 +08:00
var allMaps [ ] map [ string ] string
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
valueMap := f ( r )
2019-04-23 03:41:39 +08:00
allMaps = append ( allMaps , valueMap )
2017-01-12 03:38:07 +08:00
}
2022-08-12 04:13:24 +08:00
return r . ToDiffable ( allMaps ... )
2016-08-23 08:31:50 +08:00
}
2021-04-13 20:59:47 +08:00
func apexException ( rec * models . RecordConfig ) bool {
// Providers often add NS and SOA records at the apex. These
// should not be included in certain checks.
return ( rec . Type == "NS" || rec . Type == "SOA" ) && rec . GetLabel ( ) == "@"
}
func ignoreNameException ( rec * models . RecordConfig ) bool {
// People wanted it to be possible to disable this safety check.
// Ok, here it is. You now have two risks:
// 1. Two owners (DNSControl and some other entity) toggling a record between two settings.
// 2. The other owner wiping all records at this label, which won't be noticed until the next time dnscontrol is run.
//fmt.Printf("********** DEBUG IGNORE %v %v %q\n", rec.GetLabel(), rec.Type, rec.Metadata["ignore_name_disable_safety_check"])
// See https://github.com/StackExchange/dnscontrol/issues/1106
_ , ok := rec . Metadata [ "ignore_name_disable_safety_check" ]
return ok
}
2020-08-21 03:49:00 +08:00
func ( d * differ ) IncrementalDiff ( existing [ ] * models . RecordConfig ) ( unchanged , create , toDelete , modify Changeset , err error ) {
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
2021-04-13 20:59:47 +08:00
//fmt.Printf("********** DEBUG: STARTING IncrementalDiff\n")
2018-01-10 01:53:16 +08:00
// sort existing and desired by name
2018-09-04 22:55:27 +08:00
existingByNameAndType := map [ models . RecordKey ] [ ] * models . RecordConfig { }
desiredByNameAndType := map [ models . RecordKey ] [ ] * models . RecordConfig { }
2021-04-13 20:59:47 +08:00
//fmt.Printf("********** DEBUG: existing list %+v\n", existing)
// Gather the existing records. Skip over any that should be ignored.
2016-08-23 08:31:50 +08:00
for _ , e := range existing {
2021-04-13 20:59:47 +08:00
//fmt.Printf("********** DEBUG: existing %v %v %v\n", e.GetLabel(), e.Type, e.GetTargetCombined())
2022-11-08 00:27:04 +08:00
if d . matchIgnoredName ( e . GetLabel ( ) , e . Type ) {
2021-04-13 20:59:47 +08:00
//fmt.Printf("Ignoring record %s %s due to IGNORE_NAME\n", e.GetLabel(), e.Type)
2020-08-18 23:14:34 +08:00
printer . Debugf ( "Ignoring record %s %s due to IGNORE_NAME\n" , e . GetLabel ( ) , e . Type )
} else if d . matchIgnoredTarget ( e . GetTargetField ( ) , e . Type ) {
2021-04-13 20:59:47 +08:00
//fmt.Printf("Ignoring record %s %s due to IGNORE_TARGET\n", e.GetLabel(), e.Type)
2020-08-18 23:14:34 +08:00
printer . Debugf ( "Ignoring record %s %s due to IGNORE_TARGET\n" , e . GetLabel ( ) , e . Type )
2018-01-16 04:39:29 +08:00
} else {
2018-09-04 22:55:27 +08:00
k := e . Key ( )
2018-01-16 04:39:29 +08:00
existingByNameAndType [ k ] = append ( existingByNameAndType [ k ] , e )
}
2016-08-23 08:31:50 +08:00
}
2021-04-13 20:59:47 +08:00
// Review the desired records. If we're modifying one that should be ignored, that's an error.
//fmt.Printf("********** DEBUG: desired list %+v\n", desired)
2018-01-16 04:39:29 +08:00
for _ , dr := range desired {
2021-04-13 20:59:47 +08:00
//fmt.Printf("********** DEBUG: desired %v %v %v -- %v %v\n", dr.GetLabel(), dr.Type, dr.GetTargetCombined(), apexException(dr), d.matchIgnoredName(dr.GetLabel()))
2022-11-08 00:27:04 +08:00
if d . matchIgnoredName ( dr . GetLabel ( ) , dr . Type ) {
2021-04-13 20:59:47 +08:00
//if !apexException(dr) || !ignoreNameException(dr) {
if ( ! ignoreNameException ( dr ) ) && ( ! apexException ( dr ) ) {
return nil , nil , nil , nil , fmt . Errorf ( "trying to update/add IGNORE_NAMEd record: %s %s" , dr . GetLabel ( ) , dr . Type )
2021-12-14 22:47:32 +08:00
//} else {
// fmt.Printf("********** DEBUG: desired EXCEPTION\n")
2021-04-13 20:59:47 +08:00
}
2020-08-18 23:14:34 +08:00
} else if d . matchIgnoredTarget ( dr . GetTargetField ( ) , dr . Type ) {
2020-08-31 07:52:37 +08:00
return nil , nil , nil , nil , fmt . Errorf ( "trying to update/add IGNORE_TARGETd record: %s %s" , dr . GetLabel ( ) , dr . Type )
2018-01-16 04:39:29 +08:00
} else {
2018-09-04 22:55:27 +08:00
k := dr . Key ( )
2018-01-16 04:39:29 +08:00
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 {
2018-10-09 04:10:44 +08:00
printer . Debugf ( "Ignoring record set %s %s due to NO_PURGE\n" , k . Type , k . NameFQDN )
2017-05-03 23:46:39 +08:00
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 ]
2019-10-09 21:48:25 +08:00
// Very first, get rid of any identical records. Easy.
for i := len ( existingRecords ) - 1 ; i >= 0 ; i -- {
ex := existingRecords [ i ]
for j , de := range desiredRecords {
if d . content ( de ) != d . content ( ex ) {
continue
}
unchanged = append ( unchanged , Correlation { d , ex , de } )
existingRecords = existingRecords [ : i + copy ( existingRecords [ i : ] , existingRecords [ i + 1 : ] ) ]
desiredRecords = desiredRecords [ : j + copy ( desiredRecords [ j : ] , desiredRecords [ j + 1 : ] ) ]
break
}
}
// Next, match by target. This will give the most natural modifications.
2016-08-23 08:31:50 +08:00
for i := len ( existingRecords ) - 1 ; i >= 0 ; i -- {
ex := existingRecords [ i ]
for j , de := range desiredRecords {
2018-03-20 05:18:58 +08:00
if de . GetTargetField ( ) == ex . GetTargetField ( ) {
2019-10-09 21:48:25 +08:00
// two records share a target, but different content (ttl or metadata changes)
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 )
2022-03-26 00:09:24 +08:00
//fmt.Printf("DEBUG: normalized: %v\n", normalized)
// NB(tlim): Commenting this out. If the provider is returning
// records that are exact duplicates, that's bad and against the
// RFCs. However, we shouldn't error out. Instead, we should
// continue so that we can delete them. Experience shows one
// record will be deleted per iteration but at least the problem
// will fix itself that way. Erroring out means it will require
// manually fixing (going to the control panel, deleting
// individual records, etc.)
//if existingLookup[normalized] != nil {
// return nil, nil, nil, nil, fmt.Errorf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized)
//}
2016-08-23 08:31:50 +08:00
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 {
2020-08-21 03:49:00 +08:00
return nil , nil , nil , nil , fmt . Errorf ( "DUPLICATE D_RECORD FOUND: %s %s" , key , normalized )
2016-08-23 08:31:50 +08:00
}
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
}
2020-07-07 08:18:24 +08:00
// if found, but not desired, delete it
2016-08-23 08:31:50 +08:00
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
}
}
2020-05-31 00:03:33 +08:00
// Sort the lists. This is purely cosmetic.
sort . Slice ( unchanged , func ( i , j int ) bool { return ChangesetLess ( unchanged , i , j ) } )
sort . Slice ( create , func ( i , j int ) bool { return ChangesetLess ( create , i , j ) } )
sort . Slice ( toDelete , func ( i , j int ) bool { return ChangesetLess ( toDelete , i , j ) } )
2016-08-23 08:31:50 +08:00
return
}
2020-06-18 21:37:57 +08:00
// ChangesetLess returns true if c[i] < c[j].
2020-05-31 00:03:33 +08:00
func ChangesetLess ( c Changeset , i , j int ) bool {
var a , b string
// Which fields are we comparing?
// Usually only Desired OR Existing content exists (we're either
// adding or deleting records). In those cases, just use whichever
// isn't nil.
// In the case where both Desired AND Existing exist, it doesn't
// matter which we use as long as we are consistent. I flipped a
// coin and picked to use Desired in that case.
if c [ i ] . Desired != nil {
a = c [ i ] . Desired . NameFQDN
} else {
a = c [ i ] . Existing . NameFQDN
}
if c [ j ] . Desired != nil {
b = c [ j ] . Desired . NameFQDN
} else {
b = c [ j ] . Existing . NameFQDN
}
return a < b
// TODO(tlim): This won't correctly sort:
// []string{"example.com", "foo.example.com", "bar.example.com"}
// A simple way to do that correctly is to split on ".", reverse the
// elements, and sort on the result.
}
2020-07-07 08:18:24 +08:00
// CorrectionLess returns true when comparing corrections.
2020-07-01 17:55:20 +08:00
func CorrectionLess ( c [ ] * models . Correction , i , j int ) bool {
return c [ i ] . Msg < c [ j ] . Msg
}
2020-08-21 03:49:00 +08:00
func ( d * differ ) ChangedGroups ( existing [ ] * models . RecordConfig ) ( map [ models . RecordKey ] [ ] string , error ) {
2017-09-13 23:49:15 +08:00
changedKeys := map [ models . RecordKey ] [ ] string { }
2021-03-01 20:09:49 +08:00
_ , create , toDelete , modify , err := d . IncrementalDiff ( existing )
2020-08-21 03:49:00 +08:00
if err != nil {
return nil , err
}
2017-09-13 23:49:15 +08:00
for _ , c := range create {
changedKeys [ c . Desired . Key ( ) ] = append ( changedKeys [ c . Desired . Key ( ) ] , c . String ( ) )
}
2021-03-01 20:09:49 +08:00
for _ , d := range toDelete {
2017-09-13 23:49:15 +08:00
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 ( ) )
}
2020-08-21 03:49:00 +08:00
return changedKeys , nil
2017-09-13 23:49:15 +08:00
}
2020-01-21 03:13:32 +08:00
// DebugKeyMapMap debug prints the results from ChangedGroups.
func DebugKeyMapMap ( note string , m map [ models . RecordKey ] [ ] string ) {
// The output isn't pretty but it is useful.
fmt . Println ( "DEBUG:" , note )
// Extract the keys
var keys [ ] models . RecordKey
for k := range m {
keys = append ( keys , k )
}
sort . SliceStable ( keys , func ( i , j int ) bool {
if keys [ i ] . NameFQDN == keys [ j ] . NameFQDN {
return keys [ i ] . Type < keys [ j ] . Type
}
return keys [ i ] . NameFQDN < keys [ j ] . NameFQDN
} )
// Pretty print the map:
for _ , k := range keys {
fmt . Printf ( " %v %v:\n" , k . Type , k . NameFQDN )
for _ , s := range m [ k ] {
fmt . Printf ( " -- %q\n" , s )
}
}
}
2016-08-23 08:31:50 +08:00
func ( c Correlation ) String ( ) string {
if c . Existing == nil {
2023-02-19 23:54:53 +08:00
return color . GreenString ( fmt . Sprintf ( "+ CREATE %s %s %s" , c . Desired . Type , c . Desired . GetLabelFQDN ( ) , c . d . content ( c . Desired ) ) )
2016-08-23 08:31:50 +08:00
}
if c . Desired == nil {
2023-02-19 23:54:53 +08:00
return color . RedString ( fmt . Sprintf ( "- DELETE %s %s %s" , c . Existing . Type , c . Existing . GetLabelFQDN ( ) , c . d . content ( c . Existing ) ) )
2017-01-12 03:38:07 +08:00
}
2023-02-19 23:54:53 +08:00
return color . YellowString ( "± MODIFY %s %s: (%s) -> (%s)" , c . Existing . Type , c . Existing . GetLabelFQDN ( ) , c . d . content ( c . Existing ) , c . d . content ( c . Desired ) )
2017-01-12 03:38:07 +08:00
}
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
2022-11-08 00:27:04 +08:00
var spaceCommaTokenizerRegexp = regexp . MustCompile ( ` \s*,\s* ` )
func compileIgnoredNames ( ignoredNames [ ] * models . IgnoreName ) [ ] ignoredName {
result := make ( [ ] ignoredName , 0 , len ( ignoredNames ) )
2019-05-27 22:14:29 +08:00
2020-08-18 23:14:34 +08:00
for _ , tst := range ignoredNames {
2022-11-08 00:27:04 +08:00
g , err := glob . Compile ( tst . Pattern , '.' )
2019-05-27 22:14:29 +08:00
if err != nil {
2022-11-08 00:27:04 +08:00
panic ( fmt . Sprintf ( "Failed to compile IGNORE_NAME pattern %q: %v" , tst . Pattern , err ) )
2019-05-27 22:14:29 +08:00
}
2022-11-08 00:27:04 +08:00
t := [ ] string { }
if tst . Types != "" {
t = spaceCommaTokenizerRegexp . Split ( tst . Types , - 1 )
}
result = append ( result , ignoredName { nameGlob : g , rTypes : t } )
2019-05-27 22:14:29 +08:00
}
return result
}
2020-08-18 23:14:34 +08:00
func compileIgnoredTargets ( ignoredTargets [ ] * models . IgnoreTarget ) [ ] glob . Glob {
result := make ( [ ] glob . Glob , 0 , len ( ignoredTargets ) )
for _ , tst := range ignoredTargets {
if tst . Type != "CNAME" {
panic ( fmt . Sprintf ( "Invalid rType for IGNORE_TARGET %v" , tst . Type ) )
}
g , err := glob . Compile ( tst . Pattern , '.' )
if err != nil {
panic ( fmt . Sprintf ( "Failed to compile IGNORE_TARGET pattern %q: %v" , tst , err ) )
}
result = append ( result , g )
}
return result
}
2022-11-08 00:27:04 +08:00
func ( d * differ ) matchIgnoredName ( name string , rType string ) bool {
2020-08-18 23:14:34 +08:00
for _ , tst := range d . compiledIgnoredNames {
2022-11-08 00:27:04 +08:00
//fmt.Printf("********** DEBUG: matchIgnoredName %q %q %v %v\n", name, rType, tst, tst.nameGlob.Match(name))
if tst . nameGlob . Match ( name ) {
if tst . rTypes == nil {
return true
}
for _ , rt := range tst . rTypes {
if rt == "*" || rt == rType {
return true
}
}
2018-01-16 04:39:29 +08:00
}
}
return false
}
2020-08-18 23:14:34 +08:00
func ( d * differ ) matchIgnoredTarget ( target string , rType string ) bool {
if rType != "CNAME" {
return false
}
for _ , tst := range d . compiledIgnoredTargets {
if tst . Match ( target ) {
return true
}
}
return false
}