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: add azure zone list cache #4811

Merged
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
4 changes: 4 additions & 0 deletions docs/tutorials/azure-private-dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ $ az role assignment create --role "Reader" --assignee <appId GUID> --scope <res
$ az role assignment create --role "Private DNS Zone Contributor" --assignee <appId GUID> --scope <dns zone resource id>
```

## Throttling

When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.

## Deploy ExternalDNS
Configure `kubectl` to be able to communicate and authenticate with your cluster.
This is per default done through the file `~/.kube/config`.
Expand Down
4 changes: 4 additions & 0 deletions docs/tutorials/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,10 @@ NOTE: it's also possible to specify (or override) ClientID through `userAssigned

NOTE: make sure the pod is restarted whenever you make a configuration change.

## Throttling

When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.

## Ingress used with ExternalDNS

This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ func main() {
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "ultradns":
p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
case "civo":
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 @@ -104,6 +104,7 @@ type Config struct {
AzureSubscriptionID string
AzureUserAssignedIdentityClientID string
AzureActiveDirectoryAuthorityHost string
AzureZonesCacheDuration time.Duration
CloudflareProxied bool
CloudflareDNSRecordsPerPage int
CoreDNSPrefix string
Expand Down Expand Up @@ -261,6 +262,7 @@ var defaultConfig = &Config{
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
AzureZonesCacheDuration: 0 * time.Second,
CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100,
CoreDNSPrefix: "/skydns/",
Expand Down Expand Up @@ -479,6 +481,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration)
app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile)
app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")

Expand Down
12 changes: 10 additions & 2 deletions provider/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"

log "github.com/sirupsen/logrus"

Expand Down Expand Up @@ -60,13 +61,14 @@ type AzureProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient ZonesClient
zonesCache *zonesCache[dns.Zone]
recordSetsClient RecordSetsClient
}

// NewAzureProvider creates a new Azure provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzureProvider, error) {
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzureProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
Expand All @@ -93,6 +95,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
Expand Down Expand Up @@ -167,6 +170,10 @@ func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)

func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
log.Debugf("Retrieving Azure DNS zones for resource group: %s.", p.resourceGroup)
if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []dns.Zone
pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &dns.ZonesClientListByResourceGroupOptions{Top: nil})
for pager.More() {
Expand All @@ -183,7 +190,8 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
}
}
}
log.Debugf("Found %d Azure DNS zone(s).", len(zones))
log.Debugf("Found %d Azure DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

Expand Down
13 changes: 10 additions & 3 deletions provider/azure/azure_private_dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"

azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
Expand Down Expand Up @@ -55,13 +56,14 @@ type AzurePrivateDNSProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient PrivateZonesClient
zonesCache *zonesCache[privatedns.PrivateZone]
recordSetsClient PrivateRecordSetsClient
}

// 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, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzurePrivateDNSProvider, error) {
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzurePrivateDNSProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
Expand All @@ -88,6 +90,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainF
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
Expand Down Expand Up @@ -177,7 +180,10 @@ func (p *AzurePrivateDNSProvider) ApplyChanges(ctx context.Context, changes *pla

func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.PrivateZone, error) {
log.Debugf("Retrieving Azure Private DNS zones for Resource Group '%s'", p.resourceGroup)

if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure Private DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []privatedns.PrivateZone

pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &privatedns.PrivateZonesClientListByResourceGroupOptions{Top: nil})
Expand All @@ -198,7 +204,8 @@ func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.Priva
}
}

log.Debugf("Found %d Azure Private DNS zone(s).", len(zones))
log.Debugf("Found %d Azure Private DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

Expand Down
1 change: 1 addition & 0 deletions provider/azure/azure_privatedns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilt
dryRun: dryRun,
resourceGroup: resourceGroup,
zonesClient: privateZonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: 0},
recordSetsClient: privateRecordsClient,
}
}
Expand Down
1 change: 1 addition & 0 deletions provider/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoin
userAssignedIdentityClientID: userAssignedIdentityClientID,
activeDirectoryAuthorityHost: activeDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: 0},
recordSetsClient: recordsClient,
}
}
Expand Down
50 changes: 50 additions & 0 deletions provider/azure/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2024 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 azure

import (
"time"
)

// zonesCache is a cache for Azure zones(private or public)
type zonesCache[T any] struct {
age time.Time
szuecs marked this conversation as resolved.
Show resolved Hide resolved
duration time.Duration
zones []T
}

// Reset method to reset the zones and update the age. This will be used to update the cache
// after making a new API call to get the zones.
func (z *zonesCache[T]) Reset(zones []T) {
if z.duration > time.Duration(0) {
z.age = time.Now()
z.zones = zones
}
}

// Get method to retrieve the cached zones. If cache is not expired, this will be used
// instead of making a new API call to get the zones.
func (z *zonesCache[T]) Get() []T {
return z.zones
}

// Expired method to check if the cache has expired based on duration or if zones are empty.
// If cache is expired, a new API call will be made to get the zones. If zones are empty, a new
// API call will be made to get the zones. This case comes in at the time of initialization.
func (z *zonesCache[T]) Expired() bool {
return len(z.zones) < 1 || time.Since(z.age) > z.duration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why z.age and not time.Now() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z.age is the time of last update. Basically the time passed since last update, if it is greater than the allowed duration, then we invalidate the cache. I am not sure what you mean how time.Now() will work here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry makes sense

}
78 changes: 78 additions & 0 deletions provider/azure/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Copyright 2024 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 azure

import (
"testing"
"time"

dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
"github.com/stretchr/testify/assert"
)

func TestzonesCache(t *testing.T) {
now := time.Now()
zoneName := "example.com"
var testCases = map[string]struct {
z *zonesCache[dns.Zone]
expired bool
}{
"inactive-zone-cache": {
&zonesCache[dns.Zone]{
duration: 0 * time.Second,
},
true,
},
"empty-active-zone-cache": {
&zonesCache[dns.Zone]{
duration: 30 * time.Second,
},
true,
},
"expired-zone-cache": {
&zonesCache[dns.Zone]{
age: now.Add(-300 * time.Second),
duration: 30 * time.Second,
},
true,
},
"active-zone-cache": {
&zonesCache[dns.Zone]{
zones: []dns.Zone{{
Name: &zoneName,
}},
duration: 30 * time.Second,
age: now,
},
false,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, testCase.expired, testCase.z.Expired())
var resetZoneLength = 1
if testCase.z.duration == 0 {
resetZoneLength = 0
}
testCase.z.Reset([]dns.Zone{{
Name: &zoneName,
}})
assert.Len(t, testCase.z.Get(), resetZoneLength)
})
}
}