From be3dd5f8970c6ef5bf5886dca649bdde9a0af53d Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 14:37:28 +0100 Subject: [PATCH 01/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- controller/execute.go | 2 + docs/advanced/ttl.md | 14 ++- docs/flags.md | 1 + endpoint/endpoint.go | 8 ++ endpoint/endpoint_test.go | 46 ++++++++ pkg/apis/externaldns/types.go | 2 + source/wrappers/post_processor.go | 61 ++++++++++ source/wrappers/post_processor_test.go | 153 +++++++++++++++++++++++++ 8 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 source/wrappers/post_processor.go create mode 100644 source/wrappers/post_processor_test.go diff --git a/controller/execute.go b/controller/execute.go index a69a1a7ae2..28eb42154c 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -430,6 +430,8 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets) combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks) combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter) + // should be the last step, so that the post-processed endpoints are applied + combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL)) return combinedSource, nil } diff --git a/docs/advanced/ttl.md b/docs/advanced/ttl.md index 4dff4e6f37..de0809b467 100644 --- a/docs/advanced/ttl.md +++ b/docs/advanced/ttl.md @@ -1,9 +1,15 @@ # Configure DNS record TTL (Time-To-Live) -An optional annotation `external-dns.alpha.kubernetes.io/ttl` is available to customize the TTL value of a DNS record. -TTL is specified as an integer encoded as string representing seconds. +> To customize DNS record TTL (Time-To-Live) in a DNS record`, you can use the `external-dns.alpha.kubernetes.io/ttl: ` annotation or flag `--min-ttl=`. TTL is specified as an integer encoded as string representing seconds. Example; `1s`, `1m2s`, `1h2m11s` -To configure it, simply annotate a service/ingress, e.g.: +Behaviour: + +- If the `external-dns.alpha.kubernetes.io/ttl` annotation is set, it overrides the default TTL(0) value. +- If the annotation is not set, the default TTL value is used, unless the `--min-ttl` flag is provided. +- If the annotation is set to `0`, and the `--min-ttl=1s` flag is provided, the value from `--min-ttl` will be used instead. +- Not all providers support the custom TTL value, and some may override it with their own default values. + +To configure it, annotate a service/ingress, e.g.: ```yaml apiVersion: v1 @@ -140,7 +146,7 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s. -## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation +## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation and `--min-ttl` flag` The `external-dns.alpha.kubernetes.io/ttl` annotation allows you to set a custom **TTL (Time To Live)** for DNS records managed by `external-dns`. diff --git a/docs/flags.md b/docs/flags.md index 68f6307c8e..e1b603f572 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -173,6 +173,7 @@ | `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) | | `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) | | `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) | +| `--min-ttl=MIN-TTL` | Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m10s, 60s, 60) | | `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) | | `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) | | `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) | diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 68f82378a6..fc479e8407 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -415,6 +415,14 @@ func (e *Endpoint) CheckEndpoint() bool { return true } +// WithMinTTL sets the endpoint's TTL to the given value if the current TTL is not configured. +func (e *Endpoint) WithMinTTL(ttl int64) { + if !e.RecordTTL.IsConfigured() && ttl > 0 { + log.Debugf("Overriding existing TTL %d with new value %d for endpoint %s", e.RecordTTL, ttl, e.DNSName) + e.RecordTTL = TTL(ttl) + } +} + // NewMXRecord parses a string representation of an MX record target (e.g., "10 mail.example.com") // and returns an MXTarget struct. Returns an error if the input is invalid. func NewMXRecord(target string) (*MXTarget, error) { diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index d87aaab3c1..5bfca83a1d 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -968,3 +968,49 @@ func TestEndpoint_UniqueOrderedTargets(t *testing.T) { }) } } + +func TestEndpoint_WithMinTTL(t *testing.T) { + tests := []struct { + name string + initialTTL TTL + inputTTL int64 + expectedTTL TTL + isConfigured bool + }{ + { + name: "sets TTL when not configured and input > 0", + initialTTL: 0, + inputTTL: 300, + expectedTTL: 300, + isConfigured: true, + }, + { + name: "does not override when already configured", + initialTTL: 120, + inputTTL: 300, + expectedTTL: 120, + isConfigured: true, + }, + { + name: "does not set when input is zero", + initialTTL: 0, + inputTTL: 0, + expectedTTL: 0, + }, + { + name: "does not set when input is negative", + initialTTL: 0, + inputTTL: -10, + expectedTTL: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &Endpoint{RecordTTL: tt.initialTTL} + ep.WithMinTTL(tt.inputTTL) + assert.Equal(t, tt.expectedTTL, ep.RecordTTL) + assert.Equal(t, tt.isConfigured, ep.RecordTTL.IsConfigured()) + }) + } +} diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 4b21a6a837..5d84da69f6 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -148,6 +148,7 @@ type Config struct { TXTEncryptAESKey string `secure:"yes"` Interval time.Duration MinEventSyncInterval time.Duration + MinTTL time.Duration Once bool DryRun bool UpdateEvents bool @@ -633,6 +634,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once) app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun) app.Flag("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)").BoolVar(&cfg.UpdateEvents) + app.Flag("min-ttl", "Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m10s, 60s, 60)").DurationVar(&cfg.MinTTL) // Miscellaneous flags app.Flag("log-format", "The format in which log messages are printed (default: text, options: text, json)").Default(defaultConfig.LogFormat).EnumVar(&cfg.LogFormat, "text", "json") diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go new file mode 100644 index 0000000000..6d91660aa8 --- /dev/null +++ b/source/wrappers/post_processor.go @@ -0,0 +1,61 @@ +package wrappers + +import ( + "context" + "time" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/source" +) + +type postProcessor struct { + source source.Source + cfg PostProcessorConfig +} + +type PostProcessorConfig struct { + ttl int64 + isConfigured bool +} + +type PostProcessorOption func(*PostProcessorConfig) + +func WithTTL(ttl time.Duration) PostProcessorOption { + return func(cfg *PostProcessorConfig) { + cTTL := int64(ttl.Seconds()) + if cTTL > 0 { + cfg.isConfigured = true + cfg.ttl = cTTL + } + } +} + +func NewPostProcessor(source source.Source, opts ...PostProcessorOption) source.Source { + cfg := PostProcessorConfig{} + for _, opt := range opts { + opt(&cfg) + } + return &postProcessor{source: source, cfg: cfg} +} + +func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { + endpoints, err := pp.source.Endpoints(ctx) + if err != nil { + return nil, err + } + + if !pp.cfg.isConfigured { + return endpoints, nil + } + + for _, ep := range endpoints { + ep.WithMinTTL(pp.cfg.ttl) + // Additional post-processing can be added here. + } + + return endpoints, nil +} + +func (pp *postProcessor) AddEventHandler(_ context.Context, handler func()) { + +} diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go new file mode 100644 index 0000000000..8a1fba229d --- /dev/null +++ b/source/wrappers/post_processor_test.go @@ -0,0 +1,153 @@ +package wrappers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "sigs.k8s.io/external-dns/endpoint" +) + +type mockSource struct { + endpoints []*endpoint.Endpoint + err error +} + +func (m *mockSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { + for _, ep := range m.endpoints { + if ep == nil { + return m.endpoints, fmt.Errorf("skipped nil endpoint") + } + } + return m.endpoints, m.err +} +func (m *mockSource) AddEventHandler(_ context.Context, _ func()) {} + +func TestWithTTL(t *testing.T) { + tests := []struct { + name string + ttlStr string + expectErr bool + expectTTL int64 + isConfigured bool + }{ + { + name: "valid 10m6s", + ttlStr: "10m6s", + expectErr: false, + expectTTL: 606, + isConfigured: true, + }, + { + name: "valid 5m", + ttlStr: "5m", + expectTTL: 300, + isConfigured: true, + }, + { + name: "zero duration", + ttlStr: "0s", + expectTTL: 0, + }, + { + name: "empty duration", + ttlStr: "0s", + expectTTL: 0, + }, + { + name: "invalid duration", + ttlStr: "notaduration", + expectErr: true, + expectTTL: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &PostProcessorConfig{} + ttl, err := time.ParseDuration(tt.ttlStr) + if tt.expectErr { + require.Error(t, err, "should fail to parse duration string") + return + } + require.NoError(t, err, "should parse duration string") + + opt := WithTTL(ttl) + opt(cfg) + + require.Equal(t, tt.isConfigured, cfg.isConfigured, "isConfigured mismatch") + require.Equal(t, tt.expectTTL, cfg.ttl, "ttl mismatch") + }) + } +} + +func TestPostProcessorEndpointsWithTTL(t *testing.T) { + tests := []struct { + title string + ttl string + endpoints []*endpoint.Endpoint + expected []*endpoint.Endpoint + expectErr bool + }{ + { + title: "process endpoints with TTL set", + ttl: "6s", + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo-1", "A", "1.2.3.4"), + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo-3", "A", 0, "1.2.3.6"), + }, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("foo-1", "A", 6, "1.2.3.4"), + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo-3", "A", 6, "1.2.3.6"), + }, + }, + { + title: "skip endpoints processing with TTL set to 0", + ttl: "0s", + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo-1", "A", "1.2.3.4"), + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo-3", "A", 0, "1.2.3.6"), + }, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo-1", "A", "1.2.3.4"), + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + endpoint.NewEndpointWithTTL("foo-3", "A", 0, "1.2.3.6"), + }, + }, + { + title: "skip endpoints processing as nill endpoint detected", + ttl: "0s", + endpoints: []*endpoint.Endpoint{ + nil, + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + }, + expected: []*endpoint.Endpoint{ + nil, + endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), + }, + expectErr: true, + }, + } + for _, tt := range tests { + + t.Run(tt.title, func(t *testing.T) { + + ms := mockSource{endpoints: tt.endpoints} + ttl, _ := time.ParseDuration(tt.ttl) + src := NewPostProcessor(&ms, WithTTL(ttl)) + + endpoints, err := src.Endpoints(context.Background()) + if tt.expectErr { + require.Error(t, err, "expected error for test case: %s", tt.title) + return + } + validateEndpoints(t, endpoints, tt.expected) + }) + } +} From 51bf78ec65b366aa5b565d62c1aa3cf9214ad3c1 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 14:43:15 +0100 Subject: [PATCH 02/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 16 ++++++++++++++++ source/wrappers/post_processor_test.go | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index 6d91660aa8..c47b145922 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 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 wrappers import ( diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go index 8a1fba229d..534541d68a 100644 --- a/source/wrappers/post_processor_test.go +++ b/source/wrappers/post_processor_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 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 wrappers import ( From 1e85a23a637f27731cbfc50e0225f37306ba8857 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 14:49:45 +0100 Subject: [PATCH 03/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index c47b145922..811ff127d1 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -38,10 +38,9 @@ type PostProcessorOption func(*PostProcessorConfig) func WithTTL(ttl time.Duration) PostProcessorOption { return func(cfg *PostProcessorConfig) { - cTTL := int64(ttl.Seconds()) - if cTTL > 0 { + if int64(ttl.Seconds()) > 0 { cfg.isConfigured = true - cfg.ttl = cTTL + cfg.ttl = int64(ttl.Seconds()) } } } From ca1ed8d45518ce7ed022171d8328fa6abc6ee887 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 14:51:20 +0100 Subject: [PATCH 04/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- controller/execute.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/execute.go b/controller/execute.go index 28eb42154c..2278178f74 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -430,7 +430,7 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets) combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks) combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter) - // should be the last step, so that the post-processed endpoints are applied + // should be the last step, so that the post-processed modifications applied combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL)) return combinedSource, nil } From 2d9385fdbd414f63a5919873bb1b4457c9d44021 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 15:22:38 +0100 Subject: [PATCH 05/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- docs/flags.md | 2 +- pkg/apis/externaldns/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/flags.md b/docs/flags.md index e1b603f572..d4666fa9d8 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -173,7 +173,7 @@ | `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) | | `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) | | `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) | -| `--min-ttl=MIN-TTL` | Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m10s, 60s, 60) | +| `--min-ttl=MIN-TTL` | Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m12s, 72s, 72) | | `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) | | `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) | | `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 5d84da69f6..df1cc9c6bc 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -634,7 +634,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once) app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun) app.Flag("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)").BoolVar(&cfg.UpdateEvents) - app.Flag("min-ttl", "Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m10s, 60s, 60)").DurationVar(&cfg.MinTTL) + app.Flag("min-ttl", "Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m12s, 72s, 72)").DurationVar(&cfg.MinTTL) // Miscellaneous flags app.Flag("log-format", "The format in which log messages are printed (default: text, options: text, json)").Default(defaultConfig.LogFormat).EnumVar(&cfg.LogFormat, "text", "json") From e65e2333874630adbae1c4a83d138e486e42fa58 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:38:39 +0100 Subject: [PATCH 06/16] feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- endpoint/endpoint_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index 5bfca83a1d..115b1f9265 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -993,9 +993,9 @@ func TestEndpoint_WithMinTTL(t *testing.T) { }, { name: "does not set when input is zero", - initialTTL: 0, + initialTTL: 30, inputTTL: 0, - expectedTTL: 0, + expectedTTL: 30, }, { name: "does not set when input is negative", From 9bb7cce71e147b8054f4573caec42605f272ec4b Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 11 Jul 2025 16:52:45 +0100 Subject: [PATCH 07/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- endpoint/endpoint_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index 115b1f9265..4aa63936d1 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -992,10 +992,11 @@ func TestEndpoint_WithMinTTL(t *testing.T) { isConfigured: true, }, { - name: "does not set when input is zero", - initialTTL: 30, - inputTTL: 0, - expectedTTL: 30, + name: "does not set when input is zero", + initialTTL: 30, + inputTTL: 0, + expectedTTL: 30, + isConfigured: true, }, { name: "does not set when input is negative", From b5ff56fe63f05701c41a17000fed3ba12cae0446 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Sat, 16 Aug 2025 12:04:20 +0100 Subject: [PATCH 08/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- controller/execute.go | 1 - docs/contributing/source-wrappers.md | 1 + source/wrappers/post_processor.go | 7 +++++-- source/wrappers/post_processor_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/controller/execute.go b/controller/execute.go index 301dc5c334..71d5bd668c 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -435,7 +435,6 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter) cfg.AddSourceWrapper("target-filter") } - // should be the last step, so that the post-processed modifications applied combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL)) return combinedSource, nil } diff --git a/docs/contributing/source-wrappers.md b/docs/contributing/source-wrappers.md index dd9583ea6a..d3e85c252c 100644 --- a/docs/contributing/source-wrappers.md +++ b/docs/contributing/source-wrappers.md @@ -31,6 +31,7 @@ Wrappers solve these key challenges: | `DedupSource` | Remove duplicate DNS records. | Avoid duplicate records from sources. | | `TargetFilterSource` | Include/exclude targets based on CIDRs. | Exclude internal IPs. | | `NAT64Source` | Add NAT64-prefixed AAAA records. | Support IPv6 with NAT64. | +| `PostProcessor` | Add records post-processing. | Configure TTL for all endpoints. | ### Use Cases diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index 811ff127d1..68f1f7b8f7 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -20,6 +20,8 @@ import ( "context" "time" + log "github.com/sirupsen/logrus" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source" ) @@ -71,6 +73,7 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e return endpoints, nil } -func (pp *postProcessor) AddEventHandler(_ context.Context, handler func()) { - +func (pp *postProcessor) AddEventHandler(ctx context.Context, handler func()) { + log.Debug("postProcessor: adding event handler") + pp.source.AddEventHandler(ctx, handler) } diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go index 534541d68a..68de1490ae 100644 --- a/source/wrappers/post_processor_test.go +++ b/source/wrappers/post_processor_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/require" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" ) type mockSource struct { @@ -167,3 +168,27 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { }) } } + +func TestPostProcessor_AddEventHandler(t *testing.T) { + tests := []struct { + title string + input []string + times int + }{ + { + title: "should add event handler", + times: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockSource := testutils.NewMockSource() + + src := NewPostProcessor(mockSource) + src.AddEventHandler(t.Context(), func() {}) + + mockSource.AssertNumberOfCalls(t, "AddEventHandler", tt.times) + }) + } +} From 7722aebb2042feedb2d0326a383513506806cbba Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 28 Aug 2025 11:09:22 +0200 Subject: [PATCH 09/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 2 ++ source/wrappers/post_processor_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index 68f1f7b8f7..c895e2f29c 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -18,6 +18,7 @@ package wrappers import ( "context" + "fmt" "time" log "github.com/sirupsen/logrus" @@ -66,6 +67,7 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e } for _, ep := range endpoints { + fmt.Println(ep, ep.RecordTTL, ep.RecordTTL.IsConfigured()) ep.WithMinTTL(pp.cfg.ttl) // Additional post-processing can be added here. } diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go index 68de1490ae..1e867d86a5 100644 --- a/source/wrappers/post_processor_test.go +++ b/source/wrappers/post_processor_test.go @@ -150,6 +150,28 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { }, expectErr: true, }, + { + title: "endpoint without TTL configured", + ttl: "1s", + endpoints: []*endpoint.Endpoint{ + {DNSName: "foo-1", Targets: endpoint.Targets{"1.2.3.5"}}, + }, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("foo-1", "A", 1, "1.2.3.5"), + }, + }, + { + title: "endpoint foo-2 with TTL configured while foo-1 without TTL configured", + ttl: "1s", + endpoints: []*endpoint.Endpoint{ + {DNSName: "foo-1", Targets: endpoint.Targets{"1.2.3.5"}}, + {DNSName: "foo-2", Targets: endpoint.Targets{"1.2.3.6"}, RecordTTL: endpoint.TTL(0)}, + }, + expected: []*endpoint.Endpoint{ + {DNSName: "foo-1", Targets: endpoint.Targets{"1.2.3.5"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-2", Targets: endpoint.Targets{"1.2.3.6"}, RecordTTL: endpoint.TTL(0)}, + }, + }, } for _, tt := range tests { From a4196247c50a38f9b7fa98a66317a22fa202e665 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 28 Aug 2025 11:34:18 +0200 Subject: [PATCH 10/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 2 -- source/wrappers/post_processor_test.go | 12 +----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index c895e2f29c..68f1f7b8f7 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -18,7 +18,6 @@ package wrappers import ( "context" - "fmt" "time" log "github.com/sirupsen/logrus" @@ -67,7 +66,6 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e } for _, ep := range endpoints { - fmt.Println(ep, ep.RecordTTL, ep.RecordTTL.IsConfigured()) ep.WithMinTTL(pp.cfg.ttl) // Additional post-processing can be added here. } diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go index 1e867d86a5..ce2a9ff389 100644 --- a/source/wrappers/post_processor_test.go +++ b/source/wrappers/post_processor_test.go @@ -150,16 +150,6 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { }, expectErr: true, }, - { - title: "endpoint without TTL configured", - ttl: "1s", - endpoints: []*endpoint.Endpoint{ - {DNSName: "foo-1", Targets: endpoint.Targets{"1.2.3.5"}}, - }, - expected: []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("foo-1", "A", 1, "1.2.3.5"), - }, - }, { title: "endpoint foo-2 with TTL configured while foo-1 without TTL configured", ttl: "1s", @@ -169,7 +159,7 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { }, expected: []*endpoint.Endpoint{ {DNSName: "foo-1", Targets: endpoint.Targets{"1.2.3.5"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "foo-2", Targets: endpoint.Targets{"1.2.3.6"}, RecordTTL: endpoint.TTL(0)}, + {DNSName: "foo-2", Targets: endpoint.Targets{"1.2.3.6"}, RecordTTL: endpoint.TTL(1)}, }, }, } From 5acae05f57629c3dd7fe744a244d25e427b27154 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 28 Aug 2025 11:46:25 +0200 Subject: [PATCH 11/16] feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk --- docs/annotations/annotations.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md index 76af0a9ce5..88ee230319 100644 --- a/docs/annotations/annotations.md +++ b/docs/annotations/annotations.md @@ -175,7 +175,9 @@ are published as CNAME records. Specifies the TTL (time to live) for the resource's DNS records. The value may be specified as either a duration or an integer number of seconds. -It must be between 1 and 2,147,483,647 seconds. +It must be between `1` and `2,147,483,647` seconds. + +> Note; setting the value to `0` means, that TTL is not configured and thus use default. ## Provider-specific annotations From 2fb38b71ed19c102ed7258a5b6fca9cf4af3130e Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:44:47 +0100 Subject: [PATCH 12/16] feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 69c998d581..d41ae26145 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -655,7 +655,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once) app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun) app.Flag("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)").BoolVar(&cfg.UpdateEvents) - app.Flag("min-ttl", "Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m12s, 72s, 72)").DurationVar(&cfg.MinTTL) + app.Flag("min-ttl", "Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72)").DurationVar(&cfg.MinTTL) // Miscellaneous flags app.Flag("log-format", "The format in which log messages are printed (default: text, options: text, json)").Default(defaultConfig.LogFormat).EnumVar(&cfg.LogFormat, "text", "json") From 0dde96f83af48a749e5f17bea138c4f533c4c430 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Tue, 9 Sep 2025 08:06:44 +0100 Subject: [PATCH 13/16] feat(source): add min-ttl support Signed-off-by: ivan katliarchuk --- docs/flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flags.md b/docs/flags.md index 2c207f114c..70fc393b7c 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -174,7 +174,7 @@ | `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) | | `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) | | `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) | -| `--min-ttl=MIN-TTL` | Configure TTL for records in duration format. This value will be used if the TTL for a source is not set. (optional; examples: 1m12s, 72s, 72) | +| `--min-ttl=MIN-TTL` | Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72) | | `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) | | `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) | | `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) | From 27d4faecb1cb7f095c7bf08f498e066e7f44b4c4 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Tue, 9 Sep 2025 08:43:57 +0100 Subject: [PATCH 14/16] feat(source): add min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 6 ++++++ source/wrappers/post_processor_test.go | 24 +++++------------------- source/wrappers/source_test.go | 11 ++++++++++- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index 68f1f7b8f7..78f6a64039 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -18,6 +18,7 @@ package wrappers import ( "context" + "fmt" "time" log "github.com/sirupsen/logrus" @@ -65,7 +66,12 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e return endpoints, nil } + fmt.Println("TTTTT") + for _, ep := range endpoints { + if ep == nil { + continue + } ep.WithMinTTL(pp.cfg.ttl) // Additional post-processing can be added here. } diff --git a/source/wrappers/post_processor_test.go b/source/wrappers/post_processor_test.go index ce2a9ff389..962f5556b5 100644 --- a/source/wrappers/post_processor_test.go +++ b/source/wrappers/post_processor_test.go @@ -18,7 +18,6 @@ package wrappers import ( "context" - "fmt" "testing" "time" @@ -33,16 +32,6 @@ type mockSource struct { err error } -func (m *mockSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { - for _, ep := range m.endpoints { - if ep == nil { - return m.endpoints, fmt.Errorf("skipped nil endpoint") - } - } - return m.endpoints, m.err -} -func (m *mockSource) AddEventHandler(_ context.Context, _ func()) {} - func TestWithTTL(t *testing.T) { tests := []struct { name string @@ -138,7 +127,7 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { }, }, { - title: "skip endpoints processing as nill endpoint detected", + title: "skip endpoints processing for nill endpoint", ttl: "0s", endpoints: []*endpoint.Endpoint{ nil, @@ -148,7 +137,6 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { nil, endpoint.NewEndpointWithTTL("foo-2", "A", 60, "1.2.3.5"), }, - expectErr: true, }, { title: "endpoint foo-2 with TTL configured while foo-1 without TTL configured", @@ -167,15 +155,13 @@ func TestPostProcessorEndpointsWithTTL(t *testing.T) { t.Run(tt.title, func(t *testing.T) { - ms := mockSource{endpoints: tt.endpoints} + ms := new(testutils.MockSource) + ms.On("Endpoints").Return(tt.endpoints, nil) ttl, _ := time.ParseDuration(tt.ttl) - src := NewPostProcessor(&ms, WithTTL(ttl)) + src := NewPostProcessor(ms, WithTTL(ttl)) endpoints, err := src.Endpoints(context.Background()) - if tt.expectErr { - require.Error(t, err, "expected error for test case: %s", tt.title) - return - } + require.NoError(t, err) validateEndpoints(t, endpoints, tt.expected) }) } diff --git a/source/wrappers/source_test.go b/source/wrappers/source_test.go index 896e98af09..b47f0dce3d 100644 --- a/source/wrappers/source_test.go +++ b/source/wrappers/source_test.go @@ -26,11 +26,16 @@ import ( func sortEndpoints(endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { - sort.Strings([]string(ep.Targets)) + if ep != nil { + ep.Targets = endpoint.NewTargets(ep.Targets...) + } } sort.Slice(endpoints, func(i, k int) bool { // Sort by DNSName, RecordType, and Targets ei, ek := endpoints[i], endpoints[k] + if ei == nil || ek == nil { + return true + } if ei.DNSName != ek.DNSName { return ei.DNSName < ek.DNSName } @@ -69,6 +74,10 @@ func validateEndpoints(t *testing.T, endpoints, expected []*endpoint.Endpoint) { func validateEndpoint(t *testing.T, endpoint, expected *endpoint.Endpoint) { t.Helper() + if endpoint == nil && expected == nil { + return + } + if endpoint.DNSName != expected.DNSName { t.Errorf("DNSName expected %q, got %q", expected.DNSName, endpoint.DNSName) } From 09fe108bccb35e11af15c43e23194280a01b6e28 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:41:20 +0100 Subject: [PATCH 15/16] feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- source/wrappers/post_processor.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index 78f6a64039..dc9f8cf587 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -66,8 +66,6 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e return endpoints, nil } - fmt.Println("TTTTT") - for _, ep := range endpoints { if ep == nil { continue From a1e0162767dc91d36cafc9539e9cd433f933c6a5 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Tue, 9 Sep 2025 09:47:35 +0100 Subject: [PATCH 16/16] feat(source): add min-ttl support Signed-off-by: ivan katliarchuk --- source/wrappers/post_processor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/source/wrappers/post_processor.go b/source/wrappers/post_processor.go index dc9f8cf587..3f12db0842 100644 --- a/source/wrappers/post_processor.go +++ b/source/wrappers/post_processor.go @@ -18,7 +18,6 @@ package wrappers import ( "context" - "fmt" "time" log "github.com/sirupsen/logrus"