mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
feat(source/min-ttl): added min-ttl support
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
This commit is contained in:
parent
ad653a63b3
commit
be3dd5f897
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
- 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`.
|
||||
|
||||
|
@ -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) |
|
||||
|
@ -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) {
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
61
source/wrappers/post_processor.go
Normal file
61
source/wrappers/post_processor.go
Normal file
@ -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()) {
|
||||
|
||||
}
|
153
source/wrappers/post_processor_test.go
Normal file
153
source/wrappers/post_processor_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user