Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-provider): create flag to support sub-domains match parent #4236

Merged
merged 5 commits into from
Feb 7, 2024
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
9 changes: 9 additions & 0 deletions docs/tutorials/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,15 @@ Annotations which are specific to AWS.

`external-dns.alpha.kubernetes.io/aws-target-hosted-zone` can optionally be set to the ID of a Route53 hosted zone. This will force external-dns to use the specified hosted zone when creating an ALIAS target.

### aws-zone-match-parent
`aws-zone-match-parent` allows support subdomains within the same zone by using their parent domain, i.e --domain-filter=x.example.com would create a DNS entry for x.example.com (and subdomains thereof).

```yaml
## hosted zone domain: example.com
--domain-filter=x.example.com,example.com
--aws-zone-match-parent
```

## Verify ExternalDNS works (Service example)

Create the following sample application to test that ExternalDNS works.
Expand Down
21 changes: 21 additions & 0 deletions endpoint/domain_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "."
thiagoluiznunes marked this conversation as resolved.
Show resolved Hide resolved
continue
}
if strings.HasSuffix(filter, "."+strippedDomain) {
return true
}
}
return false
}
101 changes: 101 additions & 0 deletions endpoint/domain_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."},
thiagoluiznunes marked this conversation as resolved.
Show resolved Hide resolved
[]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+".")
}
})
}
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ func main() {
ZoneIDFilter: zoneIDFilter,
ZoneTypeFilter: zoneTypeFilter,
ZoneTagFilter: zoneTagFilter,
ZoneMatchParent: cfg.AWSZoneMatchParent,
BatchChangeSize: cfg.AWSBatchChangeSize,
BatchChangeInterval: cfg.AWSBatchChangeInterval,
EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type Config struct {
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AWSZoneMatchParent bool
AWSDynamoDBRegion string
AWSDynamoDBTable string
AzureConfigFile string
Expand Down Expand Up @@ -257,6 +258,7 @@ var defaultConfig = &Config{
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{},
AWSZoneMatchParent: false,
AWSAssumeRole: "",
AWSAssumeRoleExternalID: "",
AWSBatchChangeSize: 1000,
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var (
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{""},
AWSZoneMatchParent: false,
AWSAssumeRole: "",
AWSAssumeRoleExternalID: "",
AWSBatchChangeSize: 1000,
Expand Down Expand Up @@ -166,6 +167,7 @@ var (
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
AWSZoneTagFilter: []string{"tag=foo"},
AWSZoneMatchParent: true,
AWSAssumeRole: "some-other-role",
AWSAssumeRoleExternalID: "pg2000",
AWSBatchChangeSize: 100,
Expand Down Expand Up @@ -351,6 +353,7 @@ func TestParseFlags(t *testing.T) {
"--exclude-target-net=1.1.0.0/9",
"--aws-zone-type=private",
"--aws-zone-tags=tag=foo",
"--aws-zone-match-parent",
"--aws-assume-role=some-other-role",
"--aws-assume-role-external-id=pg2000",
"--aws-batch-change-size=100",
Expand Down Expand Up @@ -473,6 +476,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_ZONE_MATCH_PARENT": "true",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_ASSUME_ROLE_EXTERNAL_ID": "pg2000",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
Expand Down
15 changes: 12 additions & 3 deletions provider/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading