From dbaca73de26bcad0e42397ce7b0802fc6d5d348e Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Tue, 14 Nov 2023 12:59:39 -0800 Subject: [PATCH] oracle provider: dns zone cache Signed-off-by: Anders Swanson --- docs/tutorials/oracle.md | 3 ++ main.go | 2 +- pkg/apis/externaldns/types.go | 3 ++ pkg/apis/externaldns/types_test.go | 4 ++ provider/oci/cache.go | 44 ++++++++++++++++++ provider/oci/cache_test.go | 75 ++++++++++++++++++++++++++++++ provider/oci/oci.go | 17 +++++-- provider/oci/oci_test.go | 6 ++- 8 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 provider/oci/cache.go create mode 100644 provider/oci/cache_test.go diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index d9fa15fbd5..994f296386 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -180,6 +180,9 @@ spec: # Specifies the OCI DNS Zone scope, defaults to GLOBAL. # May be GLOBAL, PRIVATE, or an empty value to specify both GLOBAL and PRIVATE OCI DNS Zones # - --oci-zone-scope=GLOBAL + # Specifies the zone cache duration, defaults to 0s. If set to 0s, the zone cache is disabled. + # Use of zone caching is recommended to reduce the amount of requests sent to OCI DNS. + # - --oci-zones-cache-duration=0s volumeMounts: - name: config mountPath: /etc/kubernetes/ diff --git a/main.go b/main.go index b86fea3f7b..d0756f84d3 100644 --- a/main.go +++ b/main.go @@ -360,7 +360,7 @@ func main() { } else { config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) } - + config.ZoneCacheDuration = cfg.OCIZoneCacheDuration if err == nil { p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.OCIZoneScope, cfg.DryRun) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 5d65704c7a..0ae71197d9 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -136,6 +136,7 @@ type Config struct { OCICompartmentOCID string OCIAuthInstancePrincipal bool OCIZoneScope string + OCIZoneCacheDuration time.Duration InMemoryZones []string OVHEndpoint string OVHApiRateLimit int @@ -293,6 +294,7 @@ var defaultConfig = &Config{ InfobloxCacheDuration: 0, OCIConfigFile: "/etc/kubernetes/oci.yaml", OCIZoneScope: "GLOBAL", + OCIZoneCacheDuration: 0 * time.Second, InMemoryZones: []string{}, OVHEndpoint: "ovh-eu", OVHApiRateLimit: 20, @@ -527,6 +529,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID) app.Flag("oci-zone-scope", "When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both.").Default(defaultConfig.OCIZoneScope).EnumVar(&cfg.OCIZoneScope, "", "GLOBAL", "PRIVATE") app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal) + app.Flag("oci-zones-cache-duration", "When using the OCI provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.OCIZoneCacheDuration.String()).DurationVar(&cfg.OCIZoneCacheDuration) app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt) app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones) app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index b48072f006..a8093fe1eb 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -95,6 +95,7 @@ var ( InfobloxMaxResults: 0, OCIConfigFile: "/etc/kubernetes/oci.yaml", OCIZoneScope: "GLOBAL", + OCIZoneCacheDuration: 0 * time.Second, InMemoryZones: []string{""}, OVHEndpoint: "ovh-eu", OVHApiRateLimit: 20, @@ -205,6 +206,7 @@ var ( InfobloxMaxResults: 2000, OCIConfigFile: "oci.yaml", OCIZoneScope: "PRIVATE", + OCIZoneCacheDuration: 30 * time.Second, InMemoryZones: []string{"example.org", "company.com"}, OVHEndpoint: "ovh-ca", OVHApiRateLimit: 42, @@ -328,6 +330,7 @@ func TestParseFlags(t *testing.T) { "--pdns-skip-tls-verify", "--oci-config-file=oci.yaml", "--oci-zone-scope=PRIVATE", + "--oci-zones-cache-duration=30s", "--tls-ca=/path/to/ca.crt", "--tls-client-cert=/path/to/cert.pem", "--tls-client-cert-key=/path/to/key.pem", @@ -449,6 +452,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000", "EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml", "EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE", + "EXTERNAL_DNS_OCI_ZONES_CACHE_DURATION": "30s", "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", "EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca", "EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42", diff --git a/provider/oci/cache.go b/provider/oci/cache.go new file mode 100644 index 0000000000..1e099355c8 --- /dev/null +++ b/provider/oci/cache.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 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 oci + +import ( + "time" + + "github.com/oracle/oci-go-sdk/v65/dns" +) + +type zoneCache struct { + age time.Time + duration time.Duration + zones map[string]dns.ZoneSummary +} + +func (z *zoneCache) Reset(zones map[string]dns.ZoneSummary) { + if z.duration > time.Duration(0) { + z.age = time.Now() + z.zones = zones + } +} + +func (z *zoneCache) Get() map[string]dns.ZoneSummary { + return z.zones +} + +func (z *zoneCache) Expired() bool { + return len(z.zones) < 1 || time.Since(z.age) > z.duration +} diff --git a/provider/oci/cache_test.go b/provider/oci/cache_test.go new file mode 100644 index 0000000000..a485e200a7 --- /dev/null +++ b/provider/oci/cache_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 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 oci + +import ( + "github.com/oracle/oci-go-sdk/v65/dns" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestZoneCache(t *testing.T) { + now := time.Now() + var testCases = map[string]struct { + z *zoneCache + expired bool + }{ + "inactive-zone-cache": { + &zoneCache{ + duration: 0 * time.Second, + }, + true, + }, + "empty-active-zone-cache": { + &zoneCache{ + duration: 30 * time.Second, + }, + true, + }, + "expired-zone-cache": { + &zoneCache{ + age: now.Add(300 * time.Second), + duration: 30 * time.Second, + }, + true, + }, + "active-zone-cache": { + &zoneCache{ + zones: map[string]dns.ZoneSummary{ + zoneIdBaz: testPrivateZoneSummaryBaz, + }, + duration: 30 * time.Second, + }, + true, + }, + } + + 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(map[string]dns.ZoneSummary{ + zoneIdQux: testPrivateZoneSummaryQux, + }) + assert.Len(t, testCase.z.Get(), resetZoneLength) + }) + } +} diff --git a/provider/oci/oci.go b/provider/oci/oci.go index 97b1ea31f9..c811c14d39 100644 --- a/provider/oci/oci.go +++ b/provider/oci/oci.go @@ -20,6 +20,7 @@ import ( "context" "os" "strings" + "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/common/auth" @@ -49,8 +50,9 @@ type OCIAuthConfig struct { // OCIConfig holds the configuration for the OCI Provider. type OCIConfig struct { - Auth OCIAuthConfig `yaml:"auth"` - CompartmentID string `yaml:"compartment"` + Auth OCIAuthConfig `yaml:"auth"` + CompartmentID string `yaml:"compartment"` + ZoneCacheDuration time.Duration } // OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure @@ -63,6 +65,7 @@ type OCIProvider struct { domainFilter endpoint.DomainFilter zoneIDFilter provider.ZoneIDFilter zoneScope string + zoneCache *zoneCache dryRun bool } @@ -135,11 +138,18 @@ func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFil domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, zoneScope: zoneScope, - dryRun: dryRun, + zoneCache: &zoneCache{ + duration: cfg.ZoneCacheDuration, + }, + dryRun: dryRun, }, nil } func (p *OCIProvider) zones(ctx context.Context) (map[string]dns.ZoneSummary, error) { + if !p.zoneCache.Expired() { + log.Debug("Using cached zones list") + return p.zoneCache.zones, nil + } zones := make(map[string]dns.ZoneSummary) scopes := []dns.GetZoneScopeEnum{dns.GetZoneScopeEnum(p.zoneScope)} // If zone scope is empty, list all zones types. @@ -155,6 +165,7 @@ func (p *OCIProvider) zones(ctx context.Context) (map[string]dns.ZoneSummary, er if len(zones) == 0 { log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter) } + p.zoneCache.Reset(zones) return zones, nil } diff --git a/provider/oci/oci_test.go b/provider/oci/oci_test.go index 2f9bf5fd68..82a82169d2 100644 --- a/provider/oci/oci_test.go +++ b/provider/oci/oci_test.go @@ -21,6 +21,7 @@ import ( "sort" "strings" "testing" + "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/dns" @@ -137,7 +138,10 @@ func newOCIProvider(client ociDNSClient, domainFilter endpoint.DomainFilter, zon domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, zoneScope: zoneScope, - dryRun: dryRun, + zoneCache: &zoneCache{ + duration: 0 * time.Second, + }, + dryRun: dryRun, } }