Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,13 +463,15 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
return wrappers.WrapSources(sources, opts)
}

// TODO: move to endpoint package
// TODO: unify and combine all filters not just regex or plain
// RegexDomainFilter overrides DomainFilter
func createDomainFilter(cfg *externaldns.Config) *endpoint.DomainFilter {
if cfg.RegexDomainFilter != nil && cfg.RegexDomainFilter.String() != "" {
if (cfg.RegexDomainFilter != nil && cfg.RegexDomainFilter.String() != "") ||
(cfg.RegexDomainExclusion != nil && cfg.RegexDomainExclusion.String() != "") {
return endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
} else {
return endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
}
return endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
}

// handleSigterm listens for a SIGTERM signal and triggers the provided cancel function
Expand Down
77 changes: 77 additions & 0 deletions controller/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,15 @@ func TestBuildProvider(t *testing.T) {
}
}

// TODO: this test should live in endpoint package
func TestCreateDomainFilter(t *testing.T) {
tests := []struct {
name string
cfg *externaldns.Config
expectedDomainFilter *endpoint.DomainFilter
isConfigured bool
matchDomain string
expectMatch bool
}{
{
name: "RegexDomainFilter",
Expand Down Expand Up @@ -381,13 +384,87 @@ func TestCreateDomainFilter(t *testing.T) {
expectedDomainFilter: endpoint.NewDomainFilterWithExclusions([]string{}, []string{}),
isConfigured: false,
},
{
name: "RegexDomainExclusionWithoutRegexFilter",
cfg: &externaldns.Config{
RegexDomainExclusion: regexp.MustCompile(`test-v1\.3\.example-test\.in`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(nil, regexp.MustCompile(`test-v1\.3\.example-test\.in`)),
isConfigured: true,
matchDomain: "test-v1.3.example-test.in",
expectMatch: false,
},
{
name: "RegexDomainFilterWithMultipleDomains",
cfg: &externaldns.Config{
RegexDomainFilter: regexp.MustCompile(`(example\.com|test\.org)`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(regexp.MustCompile(`(example\.com|test\.org)`), nil),
isConfigured: true,
matchDomain: "api.example.com",
expectMatch: true,
},
{
name: "RegexDomainFilterWithWildcardPattern",
cfg: &externaldns.Config{
RegexDomainFilter: regexp.MustCompile(`.*\.staging\..*`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(regexp.MustCompile(`.*\.staging\..*`), nil),
isConfigured: true,
matchDomain: "app.staging.example.com",
expectMatch: true,
},
{
name: "RegexDomainExclusionWithComplexPattern",
cfg: &externaldns.Config{
RegexDomainExclusion: regexp.MustCompile(`^(internal|private)-.*\.example\.com$`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(nil, regexp.MustCompile(`^(internal|private)-.*\.example\.com$`)),
isConfigured: true,
matchDomain: "internal-service.example.com",
expectMatch: false,
},
{
name: "RegexFilterAndExclusionBothPresent",
cfg: &externaldns.Config{
RegexDomainFilter: regexp.MustCompile(`.*\.prod\..*`),
RegexDomainExclusion: regexp.MustCompile(`temp-.*\.prod\..*`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(regexp.MustCompile(`.*\.prod\..*`), regexp.MustCompile(`temp-.*\.prod\..*`)),
isConfigured: true,
matchDomain: "temp-api.prod.example.com",
expectMatch: false,
},
{
name: "RegexWithEscapedSpecialChars",
cfg: &externaldns.Config{
RegexDomainFilter: regexp.MustCompile(`test\-api\.v\d+\.example\.com`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(regexp.MustCompile(`test\-api\.v\d+\.example\.com`), nil),
isConfigured: true,
matchDomain: "test-api.v2.example.com",
expectMatch: true,
},
{
name: "RegexExclusionWithNumericPattern",
cfg: &externaldns.Config{
RegexDomainExclusion: regexp.MustCompile(`\d{3,}-temp\..*`),
},
expectedDomainFilter: endpoint.NewRegexDomainFilter(nil, regexp.MustCompile(`\d{3,}-temp\..*`)),
isConfigured: true,
matchDomain: "123-temp.example.com",
expectMatch: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := createDomainFilter(tt.cfg)
assert.Equal(t, tt.isConfigured, filter.IsConfigured())
assert.Equal(t, tt.expectedDomainFilter, filter)
if tt.matchDomain != "" {
assert.Equal(t, tt.expectMatch, filter.Match(tt.matchDomain))
}
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
| `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) |
| `--exclude-domains=` | Exclude subdomains (optional) |
| `--regex-domain-filter=` | Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional) |
| `--regex-domain-exclusion=` | Regex filter that excludes domains and target zones matched by regex-domain-filter (optional); Require 'regex-domain-filter' |
| `--regex-domain-exclusion=` | Regex filter that excludes domains and target zones matched by regex-domain-filter (optional) |
| `--zone-name-filter=` | Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional) |
| `--zone-id-filter=` | Filter target zones by hosted zone id; specify multiple times for multiple zones (optional) |
| `--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. |
Expand Down
23 changes: 18 additions & 5 deletions endpoint/domain_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,29 @@ func matchFilter(filters []string, domain string, emptyval bool) bool {
}

// matchRegex determines if a domain matches the configured regular expressions in DomainFilter.
// 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 returns false
// The function checks exclusion first, then inclusion:
// 1. If negativeRegex is set and matches the domain, return false (excluded)
// 2. If regex is set and matches the domain, return true (included)
// 3. If regex is not set but negativeRegex is set, return true (not excluded, no inclusion filter)
// 4. If regex is set but doesn't match, return false (not included)
func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain string) bool {
strippedDomain := normalizeDomain(domain)

// First check exclusion - if domain matches exclusion, reject it
if negativeRegex != nil && negativeRegex.String() != "" {
return !negativeRegex.MatchString(strippedDomain)
if negativeRegex.MatchString(strippedDomain) {
return false
}
}

// Then check inclusion filter if set
if regex != nil && regex.String() != "" {
return regex.MatchString(strippedDomain)
}
return regex.MatchString(strippedDomain)

// If only exclusion is set (no inclusion filter), accept the domain
// since it didn't match the exclusion
return true
}

// IsConfigured returns true if any inclusion or exclusion rules have been specified.
Expand Down
83 changes: 83 additions & 0 deletions endpoint/domain_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,54 @@ var regexDomainFilterTests = []regexDomainFilterTest{
"regexExclude": "^example\\.(?:foo|bar)\\.org$",
},
},
{
// Test case: domain doesn't match include filter, also doesn't match exclusion
// Should be REJECTED because it doesn't match the include filter
regexp.MustCompile(`foo\.org$`),
regexp.MustCompile(`^temp\.`),
[]string{"bar.org", "example.com", "test.net"},
false,
map[string]string{
"regexInclude": `foo\.org$`,
"regexExclude": `^temp\.`,
},
},
{
// Test case: domain matches include filter, doesn't match exclusion
// Should be ACCEPTED
regexp.MustCompile(`\.prod\.example\.com$`),
regexp.MustCompile(`^temp-`),
[]string{"api.prod.example.com", "web.prod.example.com"},
true,
map[string]string{
"regexInclude": `\.prod\.example\.com$`,
"regexExclude": `^temp-`,
},
},
{
// Test case: domain matches both include and exclusion
// Exclusion should take precedence - REJECTED
regexp.MustCompile(`\.prod\.example\.com$`),
regexp.MustCompile(`^temp-`),
[]string{"temp-api.prod.example.com", "temp-web.prod.example.com"},
false,
map[string]string{
"regexInclude": `\.prod\.example\.com$`,
"regexExclude": `^temp-`,
},
},
{
// Test case: domain doesn't match include filter
// Should be REJECTED even if exclusion doesn't match
regexp.MustCompile(`\.staging\.example\.com$`),
regexp.MustCompile(`^internal-`),
[]string{"api.prod.example.com", "web.dev.example.com", "service.test.org"},
false,
map[string]string{
"regexInclude": `\.staging\.example\.com$`,
"regexExclude": `^internal-`,
},
},
}

func TestDomainFilterMatch(t *testing.T) {
Expand Down Expand Up @@ -489,6 +537,41 @@ func TestDomainFilterMatchWithEmptyFilter(t *testing.T) {
}
}

func TestNewDomainFilterWithExclusionsHandlesEmptyInputs(t *testing.T) {
tests := []struct {
name string
filters []string
exclude []string
}{
{
name: "NilSlices",
filters: nil,
exclude: nil,
},
{
name: "EmptySlices",
filters: []string{},
exclude: []string{},
},
{
name: "WhitespaceOnly",
filters: []string{" ", ""},
exclude: []string{"", " "},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
domainFilter := NewDomainFilterWithExclusions(tt.filters, tt.exclude)

assert.False(t, domainFilter.IsConfigured())
assert.Empty(t, domainFilter.Filters)
assert.Empty(t, domainFilter.exclude)
assert.True(t, domainFilter.Match("example.com"))
})
}
}

func TestRegexDomainFilter(t *testing.T) {
for i, tt := range regexDomainFilterTests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ func bindFlags(b FlagBinder, cfg *Config) {
b.StringsVar("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)", []string{""}, &cfg.DomainFilter)
b.StringsVar("exclude-domains", "Exclude subdomains (optional)", []string{""}, &cfg.ExcludeDomains)
b.RegexpVar("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)", defaultConfig.RegexDomainFilter, &cfg.RegexDomainFilter)
b.RegexpVar("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional); Require 'regex-domain-filter' ", defaultConfig.RegexDomainExclusion, &cfg.RegexDomainExclusion)
b.RegexpVar("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional)", defaultConfig.RegexDomainExclusion, &cfg.RegexDomainExclusion)
b.StringsVar("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)", []string{""}, &cfg.ZoneNameFilter)
b.StringsVar("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)", []string{""}, &cfg.ZoneIDFilter)
b.StringVar("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.", defaultConfig.GoogleProject, &cfg.GoogleProject)
Expand Down
Loading