Skip to content

Commit

Permalink
add target filters based on network
Browse files Browse the repository at this point in the history
  • Loading branch information
tobikris committed Apr 8, 2022
1 parent 6d7f465 commit 692f2bb
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ Conversely, to force the public IP: `external-dns.alpha.kubernetes.io/access=pub

If this annotation is not set, and the node has both public and private IP addresses, then the public IP will be used by default.

Some loadbalancer implementations assign multiple IP addresses as external addresses. You can filter the generated targets by their networks
using `--target-net-filter=10.0.0.0/8` or `--exclude-target-net=10.0.0.0/8`.

### Can external-dns manage(add/remove) records in a hosted zone which is setup in different AWS account?

Yes, give it the correct cross-account/assume-role permissions and use the `--aws-assume-role` flag https://github.com/kubernetes-sigs/external-dns/pull/524#issue-181256561
Expand Down
99 changes: 99 additions & 0 deletions endpoint/target_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package endpoint

import (
"net"
"strings"

log "github.com/sirupsen/logrus"
)

// TargetFilterInterface defines the interface to select matching targets for a specific provider or runtime
type TargetFilterInterface interface {
Match(target string) bool
IsConfigured() bool
}

// TargetNetFilter holds a lists of valid target names
type TargetNetFilter struct {
// FilterNets define what targets to match
FilterNets []*net.IPNet
// excludeNets define what targets not to match
excludeNets []*net.IPNet
}

// prepareTargetFilters provides consistent trimming for filters/exclude params
func prepareTargetFilters(filters []string) []*net.IPNet {
fs := make([]*net.IPNet, 0)

for _, filter := range filters {
filter = strings.TrimSpace(filter)

_, filterNet, err := net.ParseCIDR(filter)
if err != nil {
log.Errorf("Invalid target net filter: %s", filter)

continue
}

fs = append(fs, filterNet)
}
return fs
}

// NewTargetNetFilterWithExclusions returns a new TargetNetFilter, given a list of matches and exclusions
func NewTargetNetFilterWithExclusions(targetFilterNets []string, excludeNets []string) TargetNetFilter {
return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)}
}

// NewTargetNetFilter returns a new TargetNetFilter given a comma separated list of targets
func NewTargetNetFilter(targetFilterNets []string) TargetNetFilter {
return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets)}
}

// Match checks whether a target can be found in the TargetNetFilter.
func (tf TargetNetFilter) Match(target string) bool {
return matchTargetNetFilter(tf.FilterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false)
}

// matchTargetNetFilter determines if any `filters` match `target`.
// If no `filters` are provided, behavior depends on `emptyval`
// (empty `tf.filters` matches everything, while empty `tf.exclude` excludes nothing)
func matchTargetNetFilter(filters []*net.IPNet, target string, emptyval bool) bool {
if len(filters) == 0 {
return emptyval
}

for _, filter := range filters {
ip := net.ParseIP(target)

if filter.Contains(ip) {
return true
}
}

return false
}

// IsConfigured returns true if TargetFilter is configured, false otherwise
func (tf TargetNetFilter) IsConfigured() bool {
if len(tf.FilterNets) == 1 {
return tf.FilterNets[0].Network() != ""
}
return len(tf.FilterNets) > 0
}
153 changes: 153 additions & 0 deletions endpoint/target_filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package endpoint

import (
"testing"

"github.com/stretchr/testify/assert"
)

type targetFilterTest struct {
targetFilter []string
exclusions []string
targets []string
expected bool
}

var targetFilterTests = []targetFilterTest{
{
[]string{"10.0.0.0/8"},
[]string{},
[]string{"10.1.2.3"},
true,
},
{
[]string{" 10.0.0.0/8 "},
[]string{},
[]string{"10.1.2.3"},
true,
},
{
[]string{"0"},
[]string{},
[]string{"10.1.2.3"},
true,
},
{
[]string{"10.0.0.0/8"},
[]string{},
[]string{"1.1.1.1"},
false,
},
{
[]string{},
[]string{"10.0.0.0/8"},
[]string{"1.1.1.1"},
true,
},
{
[]string{},
[]string{"10.0.0.0/8"},
[]string{"10.1.2.3"},
false,
},
}

func TestTargetFilterMatch(t *testing.T) {
for i, tt := range targetFilterTests {
if len(tt.exclusions) > 0 {
t.Logf("NewTargetFilter() doesn't support exclusions - skipping test %+v", tt)
continue
}
targetFilter := NewTargetNetFilter(tt.targetFilter)
for _, target := range tt.targets {
assert.Equal(t, tt.expected, targetFilter.Match(target), "should not fail: %v in test-case #%v", target, i)
}
}
}

func TestTargetFilterWithExclusions(t *testing.T) {
for i, tt := range targetFilterTests {
if len(tt.exclusions) == 0 {
tt.exclusions = append(tt.exclusions, "")
}
targetFilter := NewTargetNetFilterWithExclusions(tt.targetFilter, tt.exclusions)
for _, target := range tt.targets {
assert.Equal(t, tt.expected, targetFilter.Match(target), "should not fail: %v in test-case #%v", target, i)
}
}
}

func TestTargetFilterMatchWithEmptyFilter(t *testing.T) {
for _, tt := range targetFilterTests {
targetFilter := TargetNetFilter{}
for i, target := range tt.targets {
assert.True(t, targetFilter.Match(target), "should not fail: %v in test-case #%v", target, i)
}
}
}

func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) {
emptyFilters := []string{}
assert.Equal(t, true, matchFilter(emptyFilters, "sometarget.com", true))
assert.Equal(t, false, matchFilter(emptyFilters, "sometarget.com", false))
}

func TestTargetFilterIsConfigured(t *testing.T) {
for _, tt := range []struct {
filters []string
exclude []string
expected bool
}{
{
[]string{""},
[]string{""},
false,
},
{
[]string{" "},
[]string{" "},
false,
},
{
[]string{"", ""},
[]string{""},
false,
},
{
[]string{"10/8"},
[]string{" "},
false,
},
{
[]string{"10.0.0.0/8"},
[]string{" "},
true,
},
{
[]string{" 10.0.0.0/8 "},
[]string{" ignored "},
true,
},
} {
t.Run("test IsConfigured", func(t *testing.T) {
tf := NewTargetNetFilterWithExclusions(tt.filters, tt.exclude)
assert.Equal(t, tt.expected, tf.IsConfigured())
})
}
}
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,12 @@ func main() {
log.Fatal(err)
}

// Filter targets
targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)

// Combine multiple sources into a single, deduplicated source.
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets))
endpointsSource = source.NewTargetFilterSource(endpointsSource, targetFilter)

// RegexDomainFilter overrides DomainFilter
var domainFilter endpoint.DomainFilter
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type Config struct {
RegexDomainExclusion *regexp.Regexp
ZoneNameFilter []string
ZoneIDFilter []string
TargetNetFilter []string
ExcludeTargetNets []string
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
AWSZoneType string
Expand Down Expand Up @@ -217,6 +219,8 @@ var defaultConfig = &Config{
ExcludeDomains: []string{},
RegexDomainFilter: regexp.MustCompile(""),
RegexDomainExclusion: regexp.MustCompile(""),
TargetNetFilter: []string{},
ExcludeTargetNets: []string{},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{},
Expand Down Expand Up @@ -395,6 +399,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter)
app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)

// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").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", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns")
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ var (
RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"),
ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
TargetNetFilter: []string{"10.0.0.0/9", "10.1.0.0/9"},
ExcludeTargetNets: []string{"1.0.0.0/9", "1.1.0.0/9"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
AWSZoneTagFilter: []string{"tag=foo"},
Expand Down Expand Up @@ -319,6 +321,10 @@ func TestParseFlags(t *testing.T) {
"--zone-name-filter=yapi.company.com",
"--zone-id-filter=/hostedzone/ZTST1",
"--zone-id-filter=/hostedzone/ZTST2",
"--target-net-filter=10.0.0.0/9",
"--target-net-filter=10.1.0.0/9",
"--exclude-target-net=1.0.0.0/9",
"--exclude-target-net=1.1.0.0/9",
"--aws-zone-type=private",
"--aws-zone-tags=tag=foo",
"--aws-assume-role=some-other-role",
Expand Down Expand Up @@ -420,6 +426,8 @@ func TestParseFlags(t *testing.T) {
"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_TARGET_NET_FILTER": "10.0.0.0/9\n10.1.0.0/9",
"EXTERNAL_DNS_EXCLUDE_TARGET_NET": "1.0.0.0/9\n1.1.0.0/9",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",
Expand Down
65 changes: 65 additions & 0 deletions source/targetfiltersource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package source

import (
"context"

"sigs.k8s.io/external-dns/endpoint"
)

// targetFilterSource is a Source that removes endpoints matching the target filter from its wrapped source.
type targetFilterSource struct {
source Source
targetFilter endpoint.TargetFilterInterface
}

// NewTargetFilterSource creates a new targetFilterSource wrapping the provided Source.
func NewTargetFilterSource(source Source, targetFilter endpoint.TargetFilterInterface) Source {
return &targetFilterSource{source: source, targetFilter: targetFilter}
}

// Endpoints collects endpoints from its wrapped source and returns
// them without targets matching the target filter.
func (ms *targetFilterSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
result := []*endpoint.Endpoint{}

endpoints, err := ms.source.Endpoints(ctx)
if err != nil {
return nil, err
}

for _, ep := range endpoints {
filteredTargets := []string{}

for _, t := range ep.Targets {
if ms.targetFilter.Match(t) {
filteredTargets = append(filteredTargets, t)
}
}

ep.Targets = filteredTargets

result = append(result, ep)
}

return result, nil
}

func (ms *targetFilterSource) AddEventHandler(ctx context.Context, handler func()) {
ms.source.AddEventHandler(ctx, handler)
}

0 comments on commit 692f2bb

Please sign in to comment.