mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
add target filters based on network
This commit is contained in:
parent
6d7f465062
commit
692f2bbc23
@ -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
99
endpoint/target_filter.go
Normal 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
|
||||
}
|
153
endpoint/target_filter_test.go
Normal file
153
endpoint/target_filter_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
4
main.go
4
main.go
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
65
source/targetfiltersource.go
Normal file
65
source/targetfiltersource.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user