Skip to content

Commit 7792e78

Browse files
feat(source): add min-ttl support (#5641)
* feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <[email protected]> * feat(source): add min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source): add min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> * feat(source/min-ttl): added min-ttl support Co-authored-by: Michel Loiseleur <[email protected]> * feat(source): add min-ttl support Signed-off-by: ivan katliarchuk <[email protected]> --------- Signed-off-by: ivan katliarchuk <[email protected]> Co-authored-by: Michel Loiseleur <[email protected]>
1 parent 90ed615 commit 7792e78

File tree

11 files changed

+357
-6
lines changed

11 files changed

+357
-6
lines changed

controller/execute.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
456456
combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
457457
cfg.AddSourceWrapper("target-filter")
458458
}
459+
combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL))
459460
return combinedSource, nil
460461
}
461462

docs/advanced/ttl.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Configure DNS record TTL (Time-To-Live)
22

3-
An optional annotation `external-dns.alpha.kubernetes.io/ttl` is available to customize the TTL value of a DNS record.
4-
TTL is specified as an integer encoded as string representing seconds.
3+
> To customize DNS record TTL (Time-To-Live) in a DNS record`, you can use the `external-dns.alpha.kubernetes.io/ttl: <duration>` annotation or flag `--min-ttl=<duration>`. TTL is specified as an integer encoded as string representing seconds. Example; `1s`, `1m2s`, `1h2m11s`
54
6-
To configure it, simply annotate a service/ingress, e.g.:
5+
Behaviour:
6+
7+
- If the `external-dns.alpha.kubernetes.io/ttl` annotation is set, it overrides the default TTL(0) value.
8+
- If the annotation is not set, the default TTL value is used, unless the `--min-ttl` flag is provided.
9+
- If the annotation is set to `0`, and the `--min-ttl=1s` flag is provided, the value from `--min-ttl` will be used instead.
10+
- Not all providers support the custom TTL value, and some may override it with their own default values.
11+
12+
To configure it, annotate a service/ingress, e.g.:
713

814
```yaml
915
apiVersion: v1
@@ -140,7 +146,7 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou
140146

141147
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.
142148

143-
## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation
149+
## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation and `--min-ttl` flag`
144150

145151
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`.
146152

docs/annotations/annotations.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ are published as CNAME records.
175175
Specifies the TTL (time to live) for the resource's DNS records.
176176

177177
The value may be specified as either a duration or an integer number of seconds.
178-
It must be between 1 and 2,147,483,647 seconds.
178+
It must be between `1` and `2,147,483,647` seconds.
179+
180+
> Note; setting the value to `0` means, that TTL is not configured and thus use default.
179181

180182
## Provider-specific annotations
181183

docs/contributing/source-wrappers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Wrappers solve these key challenges:
3131
| `DedupSource` | Remove duplicate DNS records. | Avoid duplicate records from sources. |
3232
| `TargetFilterSource` | Include/exclude targets based on CIDRs. | Exclude internal IPs. |
3333
| `NAT64Source` | Add NAT64-prefixed AAAA records. | Support IPv6 with NAT64. |
34+
| `PostProcessor` | Add records post-processing. | Configure TTL for all endpoints. |
3435

3536
### Use Cases
3637

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
| `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) |
175175
| `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) |
176176
| `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) |
177+
| `--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) |
177178
| `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) |
178179
| `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) |
179180
| `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) |

endpoint/endpoint.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,14 @@ func (e *Endpoint) CheckEndpoint() bool {
423423
return true
424424
}
425425

426+
// WithMinTTL sets the endpoint's TTL to the given value if the current TTL is not configured.
427+
func (e *Endpoint) WithMinTTL(ttl int64) {
428+
if !e.RecordTTL.IsConfigured() && ttl > 0 {
429+
log.Debugf("Overriding existing TTL %d with new value %d for endpoint %s", e.RecordTTL, ttl, e.DNSName)
430+
e.RecordTTL = TTL(ttl)
431+
}
432+
}
433+
426434
// NewMXRecord parses a string representation of an MX record target (e.g., "10 mail.example.com")
427435
// and returns an MXTarget struct. Returns an error if the input is invalid.
428436
func NewMXRecord(target string) (*MXTarget, error) {

endpoint/endpoint_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,3 +989,50 @@ func TestTargets_UniqueOrdered(t *testing.T) {
989989
})
990990
}
991991
}
992+
993+
func TestEndpoint_WithMinTTL(t *testing.T) {
994+
tests := []struct {
995+
name string
996+
initialTTL TTL
997+
inputTTL int64
998+
expectedTTL TTL
999+
isConfigured bool
1000+
}{
1001+
{
1002+
name: "sets TTL when not configured and input > 0",
1003+
initialTTL: 0,
1004+
inputTTL: 300,
1005+
expectedTTL: 300,
1006+
isConfigured: true,
1007+
},
1008+
{
1009+
name: "does not override when already configured",
1010+
initialTTL: 120,
1011+
inputTTL: 300,
1012+
expectedTTL: 120,
1013+
isConfigured: true,
1014+
},
1015+
{
1016+
name: "does not set when input is zero",
1017+
initialTTL: 30,
1018+
inputTTL: 0,
1019+
expectedTTL: 30,
1020+
isConfigured: true,
1021+
},
1022+
{
1023+
name: "does not set when input is negative",
1024+
initialTTL: 0,
1025+
inputTTL: -10,
1026+
expectedTTL: 0,
1027+
},
1028+
}
1029+
1030+
for _, tt := range tests {
1031+
t.Run(tt.name, func(t *testing.T) {
1032+
ep := &Endpoint{RecordTTL: tt.initialTTL}
1033+
ep.WithMinTTL(tt.inputTTL)
1034+
assert.Equal(t, tt.expectedTTL, ep.RecordTTL)
1035+
assert.Equal(t, tt.isConfigured, ep.RecordTTL.IsConfigured())
1036+
})
1037+
}
1038+
}

pkg/apis/externaldns/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ type Config struct {
149149
TXTEncryptAESKey string `secure:"yes"`
150150
Interval time.Duration
151151
MinEventSyncInterval time.Duration
152+
MinTTL time.Duration
152153
Once bool
153154
DryRun bool
154155
UpdateEvents bool
@@ -720,6 +721,7 @@ func App(cfg *Config) *kingpin.Application {
720721
app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once)
721722
app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun)
722723
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)
724+
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)
723725

724726
// Miscellaneous flags
725727
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")

source/wrappers/post_processor.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package wrappers
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
log "github.com/sirupsen/logrus"
24+
25+
"sigs.k8s.io/external-dns/endpoint"
26+
"sigs.k8s.io/external-dns/source"
27+
)
28+
29+
type postProcessor struct {
30+
source source.Source
31+
cfg PostProcessorConfig
32+
}
33+
34+
type PostProcessorConfig struct {
35+
ttl int64
36+
isConfigured bool
37+
}
38+
39+
type PostProcessorOption func(*PostProcessorConfig)
40+
41+
func WithTTL(ttl time.Duration) PostProcessorOption {
42+
return func(cfg *PostProcessorConfig) {
43+
if int64(ttl.Seconds()) > 0 {
44+
cfg.isConfigured = true
45+
cfg.ttl = int64(ttl.Seconds())
46+
}
47+
}
48+
}
49+
50+
func NewPostProcessor(source source.Source, opts ...PostProcessorOption) source.Source {
51+
cfg := PostProcessorConfig{}
52+
for _, opt := range opts {
53+
opt(&cfg)
54+
}
55+
return &postProcessor{source: source, cfg: cfg}
56+
}
57+
58+
func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
59+
endpoints, err := pp.source.Endpoints(ctx)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
if !pp.cfg.isConfigured {
65+
return endpoints, nil
66+
}
67+
68+
for _, ep := range endpoints {
69+
if ep == nil {
70+
continue
71+
}
72+
ep.WithMinTTL(pp.cfg.ttl)
73+
// Additional post-processing can be added here.
74+
}
75+
76+
return endpoints, nil
77+
}
78+
79+
func (pp *postProcessor) AddEventHandler(ctx context.Context, handler func()) {
80+
log.Debug("postProcessor: adding event handler")
81+
pp.source.AddEventHandler(ctx, handler)
82+
}

0 commit comments

Comments
 (0)