Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
be3dd5f
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
51bf78e
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
1e85a23
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
ca1ed8d
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
2d9385f
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
e65e233
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
9bb7cce
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Jul 11, 2025
7c96dc3
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 16, 2025
b5ff56f
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 16, 2025
bd4678a
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 21, 2025
c9e26ed
Merge branch 'master' into feat-default-ttl
ivankatliarchuk Aug 28, 2025
7722aeb
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 28, 2025
a419624
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 28, 2025
5acae05
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Aug 28, 2025
2fb38b7
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Sep 8, 2025
6d125a7
feat(source): add min-ttl support
ivankatliarchuk Sep 9, 2025
0dde96f
feat(source): add min-ttl support
ivankatliarchuk Sep 9, 2025
27d4fae
feat(source): add min-ttl support
ivankatliarchuk Sep 9, 2025
09fe108
feat(source/min-ttl): added min-ttl support
ivankatliarchuk Sep 9, 2025
a1e0162
feat(source): add min-ttl support
ivankatliarchuk Sep 9, 2025
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
1 change: 1 addition & 0 deletions controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
cfg.AddSourceWrapper("target-filter")
}
combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL))
return combinedSource, nil
}

Expand Down
14 changes: 10 additions & 4 deletions docs/advanced/ttl.md
Original file line number Diff line number Diff line change
@@ -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: <duration>` annotation or flag `--min-ttl=<duration>`. 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

On other flags / annotation pair, the annotation (always) override the CLI flag.

I have doubt about this specific point of the behavior 🤔 .

@ivankatliarchuk Wdyt ? Do you have a strong motivation behind it that I missed ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Need to review it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So, this is an edge case. Only when value is set to 0. I have tried to change that, but it will require to change the whole logic behind TTL

  1. When TTL is 0, it's classed as not configured
    func (ttl TTL) IsConfigured() bool {
    . So 0 is the default value, and if annotation is setting it to 0, it will not take any effect.
  2. I was thinking to change TTL struct
    RecordTTL TTL `json:"recordTTL,omitempty"`
    to not just be int64, but the change is quite complex

So I'm proposing to keep this edge case as is, and if we have any feature requests, to revisit this edge case in the future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Basically, at the moment, following two are equal in terms of TTL

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echoserver
  namespace: nm-001
....
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echoserver
  namespace: nm-001
  annotations:
    external-dns.alpha.kubernetes.io/ttl: "0"

One is without TTL annotation and the other one with will TTL annotation set to explicitly to 0. This case at the moment classed as TTL is not configured.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This decision was explicitly being made here #320 (comment)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

And if there is annotation TTL, and if it's set to 0, is currently overrided in providers, as well as there are explicit flags example --ns1-min-ttl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

And here #320 (comment)

Basically, when annotation is set to 0, it means use defaults.

- 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
Expand Down Expand Up @@ -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`.

Expand Down
4 changes: 3 additions & 1 deletion docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/contributing/source-wrappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +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 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) |
Expand Down
8 changes: 8 additions & 0 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,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) {
Expand Down
47 changes: 47 additions & 0 deletions endpoint/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,50 @@ func TestTargets_UniqueOrdered(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: 30,
inputTTL: 0,
expectedTTL: 30,
isConfigured: true,
},
{
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())
})
}
}
2 changes: 2 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ type Config struct {
TXTEncryptAESKey string `secure:"yes"`
Interval time.Duration
MinEventSyncInterval time.Duration
MinTTL time.Duration
Once bool
DryRun bool
UpdateEvents bool
Expand Down Expand Up @@ -654,6 +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 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")
Expand Down
82 changes: 82 additions & 0 deletions source/wrappers/post_processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
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 (
"context"
"time"

log "github.com/sirupsen/logrus"

"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) {
if int64(ttl.Seconds()) > 0 {
cfg.isConfigured = true
cfg.ttl = int64(ttl.Seconds())
}
}
}

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 {
if ep == nil {
continue
}
ep.WithMinTTL(pp.cfg.ttl)
// Additional post-processing can be added here.
}

return endpoints, nil
}

func (pp *postProcessor) AddEventHandler(ctx context.Context, handler func()) {
log.Debug("postProcessor: adding event handler")
pp.source.AddEventHandler(ctx, handler)
}
Loading
Loading