From c5e0227180662890b227fe4512c8fc81e95f594f Mon Sep 17 00:00:00 2001 From: Enrique Gonzalez Date: Thu, 9 Apr 2020 14:37:45 +0200 Subject: [PATCH] feat: add regex domain filters Signed-off-by: Enrique Gonzalez --- endpoint/domain_filter.go | 48 ++++++++++++++++++++++++++++-- main.go | 8 ++++- pkg/apis/externaldns/types.go | 6 ++++ pkg/apis/externaldns/types_test.go | 8 +++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/endpoint/domain_filter.go b/endpoint/domain_filter.go index ced496d0d..4678aa9ee 100644 --- a/endpoint/domain_filter.go +++ b/endpoint/domain_filter.go @@ -17,7 +17,10 @@ limitations under the License. package endpoint import ( + "regexp" "strings" + + log "github.com/sirupsen/logrus" ) // DomainFilter holds a lists of valid domain names @@ -26,6 +29,10 @@ type DomainFilter struct { Filters []string // exclude define what domains not to match exclude []string + // regex defines a regular expression to match the domains + regex string + // regexExclusion defines a regular expression to exclude the domains matched + regexExclusion string } // prepareFilters provides consistent trimming for filters/exclude params @@ -39,16 +46,26 @@ func prepareFilters(filters []string) []string { // NewDomainFilterWithExclusions returns a new DomainFilter, given a list of matches and exclusions func NewDomainFilterWithExclusions(domainFilters []string, excludeDomains []string) DomainFilter { - return DomainFilter{prepareFilters(domainFilters), prepareFilters(excludeDomains)} + return DomainFilter{prepareFilters(domainFilters), prepareFilters(excludeDomains), "", ""} } // NewDomainFilter returns a new DomainFilter given a comma separated list of domains func NewDomainFilter(domainFilters []string) DomainFilter { - return DomainFilter{prepareFilters(domainFilters), []string{}} + return DomainFilter{prepareFilters(domainFilters), []string{}, "", ""} +} + +// NewRegexDomainFilter returns a new DomainFilter given a regular expression +func NewRegexDomainFilter(regexDomainFilter string, regexDomainExclusion string) DomainFilter { + return DomainFilter{[]string{}, []string{}, regexDomainFilter, regexDomainExclusion} } // Match checks whether a domain can be found in the DomainFilter. +// RegexFilter takes precedence over Filters func (df DomainFilter) Match(domain string) bool { + if df.regex != "" { + return matchRegex(df.regex, df.regexExclusion, domain) + } + return matchFilter(df.Filters, domain, true) && !matchFilter(df.exclude, domain, false) } @@ -78,9 +95,34 @@ func matchFilter(filters []string, domain string, emptyval bool) bool { return false } +// matchRegex determines if a domain matches the configured regular expressions in the DomainFilter. +// The negativeRegex, if set, takes precedence over regex. Therefore, +// matchRegex returns true when only regex regular expression matches the domain. +// Otherwise, if either negativeRegex matches or regex does not match the domain, it will return false. +func matchRegex(regex string, negativeRegex string, domain string) bool { + strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) + + if negativeRegex != "" { + match, err := regexp.MatchString(negativeRegex, strippedDomain) + if err != nil { + log.Errorf("Failed to filter domain %s with the regex-exclusion filter: %v", domain, err) + } + if match { + return false + } + } + match, err := regexp.MatchString(regex, strippedDomain) + if err != nil { + log.Errorf("Failed to filter domain %s with the regex filter: %v", domain, err) + } + return match +} + // IsConfigured returns true if DomainFilter is configured, false otherwise func (df DomainFilter) IsConfigured() bool { - if len(df.Filters) == 1 { + if df.regex != "" { + return true + } else if len(df.Filters) == 1 { return df.Filters[0] != "" } return len(df.Filters) > 0 diff --git a/main.go b/main.go index b31b0884c..ddb51ee0c 100644 --- a/main.go +++ b/main.go @@ -115,7 +115,13 @@ func main() { // Combine multiple sources into a single, deduplicated source. endpointsSource := source.NewDedupSource(source.NewMultiSource(sources)) - domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains) + // RegexDomainFilter overrides DomainFilter + var domainFilter endpoint.DomainFilter + if cfg.RegexDomainFilter != "" { + domainFilter = endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion) + } else { + domainFilter = endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains) + } zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter) zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index c567a68ea..854a6711d 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -61,6 +61,8 @@ type Config struct { GoogleBatchChangeInterval time.Duration DomainFilter []string ExcludeDomains []string + RegexDomainFilter string + RegexDomainExclusion string ZoneIDFilter []string AlibabaCloudConfigFile string AlibabaCloudZoneType string @@ -164,6 +166,8 @@ var defaultConfig = &Config{ GoogleBatchChangeInterval: time.Second, DomainFilter: []string{}, ExcludeDomains: []string{}, + RegexDomainFilter: "", + RegexDomainExclusion: "", AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", AWSZoneTagFilter: []string{}, @@ -315,6 +319,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) + app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default("").StringVar(&cfg.RegexDomainFilter) + app.Flag("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional)").Default("").StringVar(&cfg.RegexDomainExclusion) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("google-batch-change-size", "When using the Google provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.GoogleBatchChangeSize)).IntVar(&cfg.GoogleBatchChangeSize) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 2257f410f..dba88e74e 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -44,6 +44,8 @@ var ( GoogleBatchChangeInterval: time.Second, DomainFilter: []string{""}, ExcludeDomains: []string{""}, + RegexDomainFilter: "", + RegexDomainExclusion: "", ZoneIDFilter: []string{""}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", @@ -117,6 +119,8 @@ var ( GoogleBatchChangeInterval: time.Second * 2, DomainFilter: []string{"example.org", "company.com"}, ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, + RegexDomainFilter: "(example\\.org|company\\.com)$", + RegexDomainExclusion: "xapi\\.(example\\.org|company\\.com)$", ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "private", @@ -247,6 +251,8 @@ func TestParseFlags(t *testing.T) { "--domain-filter=company.com", "--exclude-domains=xapi.example.org", "--exclude-domains=xapi.company.com", + "--regex-domain-filter=(example\\.org|company\\.com)$", + "--regex-domain-exclusion=xapi\\.(example\\.org|company\\.com)$", "--zone-id-filter=/hostedzone/ZTST1", "--zone-id-filter=/hostedzone/ZTST2", "--aws-zone-type=private", @@ -325,6 +331,8 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca", "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", "EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com", + "EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$", + "EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$", "EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081", "EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key", "EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",