From 70835ab7bdc6d210c91ad54257950f57bf8b646d Mon Sep 17 00:00:00 2001 From: thiagoluiznunes Date: Tue, 6 Feb 2024 10:33:01 -0300 Subject: [PATCH] feat(aws-provider): create flag to support sub-domains match parent The current implementation of external-dns from sig-external-dns does not support domain filtering (--domain-filter) for sub-domains on Route53, such as test.sub-domain.domain.com. The function MatchParent was recently removed from the base code, but it is still necessary for this purpose. An example of a use case for this support is having a cluster per hosted zone with a hundred ingress related to that zone with different variants of sub-domains. With the matchParent function and zone-match-parent flag, external-dns will now support an extended automatic match for sub-domains. --- endpoint/domain_filter.go | 21 +++++++ endpoint/domain_filter_test.go | 101 +++++++++++++++++++++++++++++++++ pkg/apis/externaldns/types.go | 3 + provider/aws/aws.go | 15 ++++- 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/endpoint/domain_filter.go b/endpoint/domain_filter.go index 21cbd2ec6..308599d80 100644 --- a/endpoint/domain_filter.go +++ b/endpoint/domain_filter.go @@ -199,3 +199,24 @@ func (df *DomainFilter) UnmarshalJSON(b []byte) error { *df = NewRegexDomainFilter(include, exclude) return nil } + +func (df DomainFilter) MatchParent(domain string) bool { + if matchFilter(df.exclude, domain, false) { + return false + } + if len(df.Filters) == 0 { + return true + } + + strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) + for _, filter := range df.Filters { + if filter == "" || strings.HasPrefix(filter, ".") { + // We don't check parents if the filter is prefixed with "." + continue + } + if strings.HasSuffix(filter, "."+strippedDomain) { + return true + } + } + return false +} diff --git a/endpoint/domain_filter_test.go b/endpoint/domain_filter_test.go index 9fe747ef6..58f0e99d9 100644 --- a/endpoint/domain_filter_test.go +++ b/endpoint/domain_filter_test.go @@ -668,3 +668,104 @@ func deserialize[T any](t *testing.T, serialized map[string]T) DomainFilter { return deserialized } + +func TestDomainFilterMatchParent(t *testing.T) { + parentMatchTests := []domainFilterTest{ + { + []string{"a.example.com."}, + []string{}, + []string{"example.com"}, + true, + map[string][]string{ + "include": {"a.example.com"}, + }, + }, + { + []string{" a.example.com "}, + []string{}, + []string{"example.com"}, + true, + map[string][]string{ + "include": {"a.example.com"}, + }, + }, + { + []string{""}, + []string{}, + []string{"example.com"}, + true, + map[string][]string{}, + }, + { + []string{".a.example.com."}, + []string{}, + []string{"example.com"}, + false, + map[string][]string{ + "include": {".a.example.com"}, + }, + }, + { + []string{"a.example.com.", "b.example.com"}, + []string{}, + []string{"example.com"}, + true, + map[string][]string{ + "include": {"a.example.com", "b.example.com"}, + }, + }, + { + []string{"a.example.com"}, + []string{}, + []string{"b.example.com"}, + false, + map[string][]string{ + "include": {"a.example.com"}, + }, + }, + { + []string{"example.com"}, + []string{}, + []string{"example.com"}, + false, + map[string][]string{ + "include": {"example.com"}, + }, + }, + { + []string{"example.com"}, + []string{}, + []string{"anexample.com"}, + false, + map[string][]string{ + "include": {"example.com"}, + }, + }, + { + []string{""}, + []string{}, + []string{""}, + true, + map[string][]string{}, + }, + } + for i, tt := range parentMatchTests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions) + + assertSerializes(t, domainFilter, tt.expectedSerialization) + deserialized := deserialize(t, map[string][]string{ + "include": tt.domainFilter, + "exclude": tt.exclusions, + }) + + for _, domain := range tt.domains { + assert.Equal(t, tt.expected, domainFilter.MatchParent(domain), "%v", domain) + assert.Equal(t, tt.expected, domainFilter.MatchParent(domain+"."), "%v", domain+".") + + assert.Equal(t, tt.expected, deserialized.MatchParent(domain), "deserialized %v", domain) + assert.Equal(t, tt.expected, deserialized.MatchParent(domain+"."), "deserialized %v", domain+".") + } + }) + } +} diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 46d46ad97..79638d57b 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -92,6 +92,7 @@ type Config struct { AWSPreferCNAME bool AWSZoneCacheDuration time.Duration AWSSDServiceCleanup bool + AWSZoneMatchParent bool AWSDynamoDBRegion string AWSDynamoDBTable string AzureConfigFile string @@ -257,6 +258,7 @@ var defaultConfig = &Config{ AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", AWSZoneTagFilter: []string{}, + AWSZoneMatchParent: false, AWSAssumeRole: "", AWSAssumeRoleExternalID: "", AWSBatchChangeSize: 1000, @@ -488,6 +490,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("aws-api-retries", "When using the AWS API, set the maximum number of retries before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries) app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME) app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration) + app.Flag("aws-zone-match-parent", "Expand limit possible target by sub-domains (default: disabled)").BoolVar(&cfg.AWSZoneMatchParent) app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup) app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure)").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile) app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup) diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 6b1690f3c..ccfad83b2 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -239,8 +239,10 @@ type AWSProvider struct { zoneTypeFilter provider.ZoneTypeFilter // filter hosted zones by tags zoneTagFilter provider.ZoneTagFilter - preferCNAME bool - zonesCache *zonesListCache + // extend filter for sub-domains in the zone (e.g. first.us-east-1.example.com) + zoneMatchParent bool + preferCNAME bool + zonesCache *zonesListCache // queue for collecting changes to submit them in the next iteration, but after all other changes failedChangesQueue map[string]Route53Changes } @@ -251,6 +253,7 @@ type AWSConfig struct { ZoneIDFilter provider.ZoneIDFilter ZoneTypeFilter provider.ZoneTypeFilter ZoneTagFilter provider.ZoneTagFilter + ZoneMatchParent bool BatchChangeSize int BatchChangeInterval time.Duration EvaluateTargetHealth bool @@ -267,6 +270,7 @@ func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error zoneIDFilter: awsConfig.ZoneIDFilter, zoneTypeFilter: awsConfig.ZoneTypeFilter, zoneTagFilter: awsConfig.ZoneTagFilter, + zoneMatchParent: awsConfig.ZoneMatchParent, batchChangeSize: awsConfig.BatchChangeSize, batchChangeInterval: awsConfig.BatchChangeInterval, evaluateTargetHealth: awsConfig.EvaluateTargetHealth, @@ -301,7 +305,12 @@ func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone } if !p.domainFilter.Match(aws.StringValue(zone.Name)) { - continue + if !p.zoneMatchParent { + continue + } + if !p.domainFilter.MatchParent(aws.StringValue(zone.Name)) { + continue + } } // Only fetch tags if a tag filter was specified