From 425dea47f3c673d74b3448b8a67d77c20d3e6f26 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Thu, 28 Mar 2024 19:56:27 +0700 Subject: [PATCH 1/4] feat(azure): add zone name filter for Azure Private DNS --- main.go | 2 +- provider/azure/azure_private_dns.go | 19 ++++++++++++++++++- provider/azure/azure_privatedns_test.go | 12 +++++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 22fefec95c..2eab3ef9fb 100644 --- a/main.go +++ b/main.go @@ -255,7 +255,7 @@ func main() { case "azure-dns", "azure": p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "azure-private-dns": - p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) + p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "bluecat": p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, cfg.TXTPrefix, cfg.TXTSuffix, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify) case "vinyldns": diff --git a/provider/azure/azure_private_dns.go b/provider/azure/azure_private_dns.go index 43e3bdc433..4fe8bc1537 100644 --- a/provider/azure/azure_private_dns.go +++ b/provider/azure/azure_private_dns.go @@ -48,6 +48,7 @@ type PrivateRecordSetsClient interface { type AzurePrivateDNSProvider struct { provider.BaseProvider domainFilter endpoint.DomainFilter + zoneNameFilter endpoint.DomainFilter zoneIDFilter provider.ZoneIDFilter dryRun bool resourceGroup string @@ -59,7 +60,7 @@ type AzurePrivateDNSProvider struct { // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. // // Returns the provider or an error if a provider could not be created. -func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) { +func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) { cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) @@ -79,6 +80,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainF } return &AzurePrivateDNSProvider{ domainFilter: domainFilter, + zoneNameFilter: zoneNameFilter, zoneIDFilter: zoneIDFilter, dryRun: dryRun, resourceGroup: cfg.ResourceGroup, @@ -122,6 +124,10 @@ func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*end } name = formatAzureDNSName(*recordSet.Name, *zone.Name) + if len(p.zoneNameFilter.Filters) > 0 && !p.domainFilter.Match(name) { + log.Debugf("Skipping return of record %s because it was filtered out by the specified --domain-filter", name) + continue + } targets := extractAzurePrivateDNSTargets(recordSet) if len(targets) == 0 { log.Debugf("Failed to extract targets for '%s' with type '%s'.", name, recordType) @@ -183,6 +189,9 @@ func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.Priva if zone.Name != nil && p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.ID) { zones = append(zones, *zone) + } else if zone.Name != nil && len(p.zoneNameFilter.Filters) > 0 && p.zoneNameFilter.Match(*zone.Name) { + // Handle zoneNameFilter + zones = append(zones, *zone) } } } @@ -236,6 +245,10 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu for zone, endpoints := range deleted { for _, ep := range endpoints { name := p.recordSetNameForZone(zone, ep) + if !p.domainFilter.Match(ep.DNSName) { + log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) + continue + } if p.dryRun { log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone) } else { @@ -259,6 +272,10 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu for zone, endpoints := range updated { for _, ep := range endpoints { name := p.recordSetNameForZone(zone, ep) + if !p.domainFilter.Match(ep.DNSName) { + log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) + continue + } if p.dryRun { log.Infof( "Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go index 567badeaa1..d5a150d7db 100644 --- a/provider/azure/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -224,15 +224,16 @@ func createPrivateMockRecordSetMultiWithTTL(name, recordType string, ttl int64, } // newMockedAzurePrivateDNSProvider creates an AzureProvider comprising the mocked clients for zones and recordsets -func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones []*privatedns.PrivateZone, recordSets []*privatedns.RecordSet) (*AzurePrivateDNSProvider, error) { +func newMockedAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, zones []*privatedns.PrivateZone, recordSets []*privatedns.RecordSet) (*AzurePrivateDNSProvider, error) { zonesClient := newMockPrivateZonesClient(zones) recordSetsClient := newMockPrivateRecordSectsClient(recordSets) - return newAzurePrivateDNSProvider(domainFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil + return newAzurePrivateDNSProvider(domainFilter, zoneNameFilter, zoneIDFilter, dryRun, resourceGroup, &zonesClient, &recordSetsClient), nil } -func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider { +func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, resourceGroup string, privateZonesClient PrivateZonesClient, privateRecordsClient PrivateRecordSetsClient) *AzurePrivateDNSProvider { return &AzurePrivateDNSProvider{ domainFilter: domainFilter, + zoneNameFilter: zoneNameFilter, zoneIDFilter: zoneIDFilter, dryRun: dryRun, resourceGroup: resourceGroup, @@ -242,7 +243,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter } func TestAzurePrivateDNSRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", + provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", []*privatedns.PrivateZone{ createMockPrivateZone("example.com", "/privateDnsZones/example.com"), }, @@ -281,7 +282,7 @@ func TestAzurePrivateDNSRecord(t *testing.T) { } func TestAzurePrivateDNSMultiRecord(t *testing.T) { - provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", + provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), endpoint.NewDomainFilter([]string{}), provider.NewZoneIDFilter([]string{""}), true, "k8s", []*privatedns.PrivateZone{ createMockPrivateZone("example.com", "/privateDnsZones/example.com"), }, @@ -369,6 +370,7 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P zonesClient := newMockPrivateZonesClient(zones) provider := newAzurePrivateDNSProvider( + endpoint.NewDomainFilter([]string{""}), endpoint.NewDomainFilter([]string{""}), provider.NewZoneIDFilter([]string{""}), dryRun, From aa396a9a2f28055e796d4c8d8109d999aa653c79 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Thu, 28 Mar 2024 20:15:32 +0700 Subject: [PATCH 2/4] test(azure): Azure Private DNS zone name filter --- provider/azure/azure_privatedns_test.go | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go index d5a150d7db..1324526f93 100644 --- a/provider/azure/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -432,3 +432,39 @@ func testAzurePrivateDNSApplyChangesInternal(t *testing.T, dryRun bool, client P t.Fatal(err) } } + +func TestAzurePrivateDNSNameFilter(t *testing.T) { + provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"nginx.example.com"}), endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", + []*privatedns.PrivateZone{ + createMockPrivateZone("example.com", "/privateDnsZones/example.com"), + }, + + []*privatedns.RecordSet{ + createPrivateMockRecordSet("@", "NS", "ns1-03.azure-dns.com."), + createPrivateMockRecordSet("@", "SOA", "Email: azuredns-hostmaster.microsoft.com"), + createPrivateMockRecordSet("@", endpoint.RecordTypeA, "123.123.123.122"), + createPrivateMockRecordSet("@", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"), + createPrivateMockRecordSetWithTTL("test.nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), + createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeA, "123.123.123.123", 3600), + createPrivateMockRecordSetWithTTL("nginx", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default", recordTTL), + createPrivateMockRecordSetWithTTL("mail.nginx", endpoint.RecordTypeMX, "20 example.com", recordTTL), + createPrivateMockRecordSetWithTTL("hack", endpoint.RecordTypeCNAME, "hack.azurewebsites.net", 10), + }) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + actual, err := provider.Records(ctx) + if err != nil { + t.Fatal(err) + } + expected := []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("test.nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), + endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"), + endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"), + endpoint.NewEndpointWithTTL("mail.nginx.example.com", endpoint.RecordTypeMX, recordTTL, "20 example.com"), + } + + validateAzureEndpoints(t, actual, expected) +} From 2cad978659e2d2c9a3c0c5dc7c524217fa57d9c7 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Thu, 28 Mar 2024 20:30:17 +0700 Subject: [PATCH 3/4] test(azure): Azure Private DNS apply change with zone name filter --- provider/azure/azure_privatedns_test.go | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go index 1324526f93..dea74c10b1 100644 --- a/provider/azure/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -468,3 +468,87 @@ func TestAzurePrivateDNSNameFilter(t *testing.T) { validateAzureEndpoints(t, actual, expected) } + +func TestAzurePrivateDNSApplyChangesZoneName(t *testing.T) { + recordsClient := mockRecordSetsClient{} + + testAzureApplyChangesInternalZoneName(t, false, &recordsClient) + + validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{ + endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, ""), + endpoint.NewEndpoint("deletedaaaa.foo.example.com", endpoint.RecordTypeAAAA, ""), + endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, ""), + }) + + validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeAAAA, endpoint.TTL(recordTTL), "2001::1:2:3:4", "2001::1:2:3:5"), + endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"), + endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), + endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), + endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), + }) +} + +func testAzurePrivateDNSApplyChangesInternalZoneName(t *testing.T, dryRun bool, client PrivateRecordSetsClient) { + zones := []*privatedns.PrivateZone{ + createMockPrivateZone("example.com", "/privateDnsZones/example.com"), + } + zonesClient := newMockPrivateZonesClient(zones) + + provider := newAzurePrivateDNSProvider( + endpoint.NewDomainFilter([]string{"foo.example.com"}), + endpoint.NewDomainFilter([]string{"example.com"}), + provider.NewZoneIDFilter([]string{""}), + dryRun, + "group", + &zonesClient, + client, + ) + + createRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:4"), + endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.5", "1.2.3.4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeAAAA, "2001::1:2:3:5", "2001::1:2:3:4"), + endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"), + endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"), + endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"), + endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"), + endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"), + } + + currentRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("old.foo.example.com", endpoint.RecordTypeA, "121.212.121.212"), + endpoint.NewEndpoint("oldcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"), + endpoint.NewEndpoint("old.nope.example.com", endpoint.RecordTypeA, "121.212.121.212"), + } + updatedRecords := []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"), + endpoint.NewEndpointWithTTL("new.foo.example.com", endpoint.RecordTypeAAAA, 3600, "2001::111:222:111:222"), + endpoint.NewEndpointWithTTL("newcname.foo.example.com", endpoint.RecordTypeCNAME, 10, "other.com"), + endpoint.NewEndpoint("new.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"), + endpoint.NewEndpoint("new.nope.example.com", endpoint.RecordTypeAAAA, "2001::222:111:222:111"), + } + + deleteRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("deleted.foo.example.com", endpoint.RecordTypeA, "111.222.111.222"), + endpoint.NewEndpoint("deletedaaaa.foo.example.com", endpoint.RecordTypeAAAA, "2001::111:222:111:222"), + endpoint.NewEndpoint("deletedcname.foo.example.com", endpoint.RecordTypeCNAME, "other.com"), + endpoint.NewEndpoint("deleted.nope.example.com", endpoint.RecordTypeA, "222.111.222.111"), + } + + changes := &plan.Changes{ + Create: createRecords, + UpdateNew: updatedRecords, + UpdateOld: currentRecords, + Delete: deleteRecords, + } + + if err := provider.ApplyChanges(context.Background(), changes); err != nil { + t.Fatal(err) + } +} From 3852a3697d3a1492d82b3087ad1063e35a862121 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Thu, 28 Mar 2024 21:43:15 +0700 Subject: [PATCH 4/4] docs(azure): describe --zone-name-filter behavior --- docs/tutorials/azure-private-dns.md | 8 +++++++- docs/tutorials/azure.md | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md index ac5a722381..268c37f66f 100644 --- a/docs/tutorials/azure-private-dns.md +++ b/docs/tutorials/azure-private-dns.md @@ -430,7 +430,13 @@ spec: pathType: Prefix ``` -When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the externaldns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. +When you use ExternalDNS with Ingress resources, it automatically creates DNS records based on the hostnames listed in those Ingress objects. +Those hostnames must match the filters that you defined (if any): + +- By default, `--domain-filter` filters Azure Private DNS zone. +- If you use `--domain-filter` together with `--zone-name-filter`, the behavior changes: `--domain-filter` then filters Ingress domains, not the Azure Private DNS zone name. + +When those hostnames are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 202d7304b2..fb49acab9b 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -736,7 +736,13 @@ spec: number: 80 ``` -When using ExternalDNS with `ingress` objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the external-dns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. +When you use ExternalDNS with Ingress resources, it automatically creates DNS records based on the hostnames listed in those Ingress objects. +Those hostnames must match the filters that you defined (if any): + +- By default, `--domain-filter` filters Azure DNS zone. +- If you use `--domain-filter` together with `--zone-name-filter`, the behavior changes: `--domain-filter` then filters Ingress domains, not the Azure DNS zone name. + +When those hostnames are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: