mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge 9bb7cce71e
into 0e7c3af221
This commit is contained in:
commit
7ece51bd58
@ -430,6 +430,8 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
|
|||||||
targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
|
targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
|
||||||
combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
|
combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
|
||||||
combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
|
combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
|
||||||
|
// should be the last step, so that the post-processed modifications applied
|
||||||
|
combinedSource = wrappers.NewPostProcessor(combinedSource, wrappers.WithTTL(cfg.MinTTL))
|
||||||
return combinedSource, nil
|
return combinedSource, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
# Configure DNS record TTL (Time-To-Live)
|
# 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.
|
> 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`
|
||||||
TTL is specified as an integer encoded as string representing seconds.
|
|
||||||
|
|
||||||
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
|
```yaml
|
||||||
apiVersion: v1
|
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.
|
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`.
|
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`.
|
||||||
|
|
||||||
|
@ -173,6 +173,7 @@
|
|||||||
| `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) |
|
| `--[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-]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) |
|
| `--[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) |
|
||||||
| `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) |
|
| `--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) |
|
| `--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) |
|
| `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) |
|
||||||
|
@ -415,6 +415,14 @@ func (e *Endpoint) CheckEndpoint() bool {
|
|||||||
return true
|
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")
|
// 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.
|
// and returns an MXTarget struct. Returns an error if the input is invalid.
|
||||||
func NewMXRecord(target string) (*MXTarget, error) {
|
func NewMXRecord(target string) (*MXTarget, error) {
|
||||||
|
@ -968,3 +968,50 @@ 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: 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -147,6 +147,7 @@ type Config struct {
|
|||||||
TXTEncryptAESKey string `secure:"yes"`
|
TXTEncryptAESKey string `secure:"yes"`
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
MinEventSyncInterval time.Duration
|
MinEventSyncInterval time.Duration
|
||||||
|
MinTTL time.Duration
|
||||||
Once bool
|
Once bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
UpdateEvents bool
|
UpdateEvents bool
|
||||||
@ -632,6 +633,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("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("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("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)
|
||||||
|
|
||||||
// Miscellaneous flags
|
// 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")
|
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")
|
||||||
|
76
source/wrappers/post_processor.go
Normal file
76
source/wrappers/post_processor.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
ep.WithMinTTL(pp.cfg.ttl)
|
||||||
|
// Additional post-processing can be added here.
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *postProcessor) AddEventHandler(_ context.Context, handler func()) {
|
||||||
|
|
||||||
|
}
|
169
source/wrappers/post_processor_test.go
Normal file
169
source/wrappers/post_processor_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user