add target filters based on network

This commit is contained in:
Tobias Krischer 2022-04-08 18:41:40 +02:00
parent 6d7f465062
commit 692f2bbc23
No known key found for this signature in database
GPG Key ID: E9D8FDB171E04060
7 changed files with 338 additions and 0 deletions

View File

@ -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

99
endpoint/target_filter.go Normal file
View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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

View File

@ -76,6 +76,8 @@ type Config struct {
RegexDomainExclusion *regexp.Regexp
ZoneNameFilter []string
ZoneIDFilter []string
TargetNetFilter []string
ExcludeTargetNets []string
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
AWSZoneType string
@ -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{},
@ -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")

View File

@ -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"},
@ -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",
@ -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",

View File

@ -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)
}