sigs.k8s.io/external-dns@v0.14.1/endpoint/domain_filter.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package endpoint
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  )
    27  
    28  type MatchAllDomainFilters []*DomainFilter
    29  
    30  func (f MatchAllDomainFilters) Match(domain string) bool {
    31  	for _, filter := range f {
    32  		if filter == nil {
    33  			continue
    34  		}
    35  		if !filter.Match(domain) {
    36  			return false
    37  		}
    38  	}
    39  	return true
    40  }
    41  
    42  // DomainFilter holds a lists of valid domain names
    43  type DomainFilter struct {
    44  	// Filters define what domains to match
    45  	Filters []string
    46  	// exclude define what domains not to match
    47  	exclude []string
    48  	// regex defines a regular expression to match the domains
    49  	regex *regexp.Regexp
    50  	// regexExclusion defines a regular expression to exclude the domains matched
    51  	regexExclusion *regexp.Regexp
    52  }
    53  
    54  // domainFilterSerde is a helper type for serializing and deserializing DomainFilter.
    55  type domainFilterSerde struct {
    56  	Include      []string `json:"include,omitempty"`
    57  	Exclude      []string `json:"exclude,omitempty"`
    58  	RegexInclude string   `json:"regexInclude,omitempty"`
    59  	RegexExclude string   `json:"regexExclude,omitempty"`
    60  }
    61  
    62  // prepareFilters provides consistent trimming for filters/exclude params
    63  func prepareFilters(filters []string) []string {
    64  	var fs []string
    65  	for _, filter := range filters {
    66  		if domain := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(filter), ".")); domain != "" {
    67  			fs = append(fs, domain)
    68  		}
    69  	}
    70  	return fs
    71  }
    72  
    73  // NewDomainFilterWithExclusions returns a new DomainFilter, given a list of matches and exclusions
    74  func NewDomainFilterWithExclusions(domainFilters []string, excludeDomains []string) DomainFilter {
    75  	return DomainFilter{Filters: prepareFilters(domainFilters), exclude: prepareFilters(excludeDomains)}
    76  }
    77  
    78  // NewDomainFilter returns a new DomainFilter given a comma separated list of domains
    79  func NewDomainFilter(domainFilters []string) DomainFilter {
    80  	return DomainFilter{Filters: prepareFilters(domainFilters)}
    81  }
    82  
    83  // NewRegexDomainFilter returns a new DomainFilter given a regular expression
    84  func NewRegexDomainFilter(regexDomainFilter *regexp.Regexp, regexDomainExclusion *regexp.Regexp) DomainFilter {
    85  	return DomainFilter{regex: regexDomainFilter, regexExclusion: regexDomainExclusion}
    86  }
    87  
    88  // Match checks whether a domain can be found in the DomainFilter.
    89  // RegexFilter takes precedence over Filters
    90  func (df DomainFilter) Match(domain string) bool {
    91  	if df.regex != nil && df.regex.String() != "" || df.regexExclusion != nil && df.regexExclusion.String() != "" {
    92  		return matchRegex(df.regex, df.regexExclusion, domain)
    93  	}
    94  
    95  	return matchFilter(df.Filters, domain, true) && !matchFilter(df.exclude, domain, false)
    96  }
    97  
    98  // matchFilter determines if any `filters` match `domain`.
    99  // If no `filters` are provided, behavior depends on `emptyval`
   100  // (empty `df.filters` matches everything, while empty `df.exclude` excludes nothing)
   101  func matchFilter(filters []string, domain string, emptyval bool) bool {
   102  	if len(filters) == 0 {
   103  		return emptyval
   104  	}
   105  
   106  	strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
   107  	for _, filter := range filters {
   108  		if filter == "" {
   109  			continue
   110  		}
   111  
   112  		if strings.HasPrefix(filter, ".") && strings.HasSuffix(strippedDomain, filter) {
   113  			return true
   114  		} else if strings.Count(strippedDomain, ".") == strings.Count(filter, ".") {
   115  			if strippedDomain == filter {
   116  				return true
   117  			}
   118  		} else if strings.HasSuffix(strippedDomain, "."+filter) {
   119  			return true
   120  		}
   121  	}
   122  	return false
   123  }
   124  
   125  // matchRegex determines if a domain matches the configured regular expressions in DomainFilter.
   126  // negativeRegex, if set, takes precedence over regex.  Therefore, matchRegex returns true when
   127  // only regex regular expression matches the domain
   128  // Otherwise, if either negativeRegex matches or regex does not match the domain, it returns false
   129  func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain string) bool {
   130  	strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
   131  
   132  	if negativeRegex != nil && negativeRegex.String() != "" {
   133  		return !negativeRegex.MatchString(strippedDomain)
   134  	}
   135  	return regex.MatchString(strippedDomain)
   136  }
   137  
   138  // IsConfigured returns true if any inclusion or exclusion rules have been specified.
   139  func (df DomainFilter) IsConfigured() bool {
   140  	if df.regex != nil && df.regex.String() != "" {
   141  		return true
   142  	} else if df.regexExclusion != nil && df.regexExclusion.String() != "" {
   143  		return true
   144  	}
   145  	return len(df.Filters) > 0 || len(df.exclude) > 0
   146  }
   147  
   148  func (df DomainFilter) MarshalJSON() ([]byte, error) {
   149  	if df.regex != nil || df.regexExclusion != nil {
   150  		var include, exclude string
   151  		if df.regex != nil {
   152  			include = df.regex.String()
   153  		}
   154  		if df.regexExclusion != nil {
   155  			exclude = df.regexExclusion.String()
   156  		}
   157  		return json.Marshal(domainFilterSerde{
   158  			RegexInclude: include,
   159  			RegexExclude: exclude,
   160  		})
   161  	}
   162  	sort.Strings(df.Filters)
   163  	sort.Strings(df.exclude)
   164  	return json.Marshal(domainFilterSerde{
   165  		Include: df.Filters,
   166  		Exclude: df.exclude,
   167  	})
   168  }
   169  
   170  func (df *DomainFilter) UnmarshalJSON(b []byte) error {
   171  	var deserialized domainFilterSerde
   172  	err := json.Unmarshal(b, &deserialized)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	if deserialized.RegexInclude == "" && deserialized.RegexExclude == "" {
   178  		*df = NewDomainFilterWithExclusions(deserialized.Include, deserialized.Exclude)
   179  		return nil
   180  	}
   181  
   182  	if len(deserialized.Include) > 0 || len(deserialized.Exclude) > 0 {
   183  		return errors.New("cannot have both domain list and regex")
   184  	}
   185  
   186  	var include, exclude *regexp.Regexp
   187  	if deserialized.RegexInclude != "" {
   188  		include, err = regexp.Compile(deserialized.RegexInclude)
   189  		if err != nil {
   190  			return fmt.Errorf("invalid regexInclude: %w", err)
   191  		}
   192  	}
   193  	if deserialized.RegexExclude != "" {
   194  		exclude, err = regexp.Compile(deserialized.RegexExclude)
   195  		if err != nil {
   196  			return fmt.Errorf("invalid regexExclude: %w", err)
   197  		}
   198  	}
   199  	*df = NewRegexDomainFilter(include, exclude)
   200  	return nil
   201  }
   202  
   203  func (df DomainFilter) MatchParent(domain string) bool {
   204  	if matchFilter(df.exclude, domain, false) {
   205  		return false
   206  	}
   207  	if len(df.Filters) == 0 {
   208  		return true
   209  	}
   210  
   211  	strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
   212  	for _, filter := range df.Filters {
   213  		if filter == "" || strings.HasPrefix(filter, ".") {
   214  			// We don't check parents if the filter is prefixed with "."
   215  			continue
   216  		}
   217  		if strings.HasSuffix(filter, "."+strippedDomain) {
   218  			return true
   219  		}
   220  	}
   221  	return false
   222  }