Skip to content

Commit 28f9e9c

Browse files
authored
feat(source)!: introduce optional force-default-targets (kubernetes-sigs#5316)
* BREAKING CHANGE: Improve default targets management * fix: Remove old test case * fix: Test confirming legacy mode allows empty CRD targets * fix: Remove comments * fix: Move flag definition closer to detault-targets * fix: Initial merge adaptation * fix: Improved legacy needs a chance to work with empty CRD list * fix: Code coverage and dead code * fix: Simpler Endpoints logic * fix: Flag description * feat: Add tutorial * fix: Improve linting * fix: Improve linting * fix: Import linting
1 parent 675cc7c commit 28f9e9c

File tree

9 files changed

+243
-53
lines changed

9 files changed

+243
-53
lines changed

controller/execute.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e
420420
return nil, err
421421
}
422422
// Combine multiple sources into a single, deduplicated source.
423-
combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets))
423+
combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
424424
// Filter targets
425425
targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
426426
combinedSource = source.NewNAT64Source(combinedSource, cfg.NAT64Networks)

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
| `--crd-source-apiversion="externaldns.k8s.io/v1alpha1"` | API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source |
2626
| `--crd-source-kind="DNSEndpoint"` | Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion |
2727
| `--default-targets=DEFAULT-TARGETS` | Set globally default host/IP that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional) |
28+
| `--[no-]force-default-targets` | Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to (improved) legacy behavior which allows empty CRD targets for migration to new state) |
2829
| `--exclude-record-types=EXCLUDE-RECORD-TYPES` | Record types to exclude from management; specify multiple times to exclude many; (optional) |
2930
| `--exclude-target-net=EXCLUDE-TARGET-NET` | Exclude target nets (optional) |
3031
| `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) |

docs/tutorials/crd.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Using CRD Source for DNS Records
2+
3+
This tutorial describes how to use the CRD source with ExternalDNS to manage DNS records. The CRD source allows you to define your desired DNS records declaratively using `DNSEndpoint` custom resources.
4+
5+
## Default Targets and CRD Targets
6+
7+
ExternalDNS has a `--default-targets` flag that can be used to specify a default set of targets for all created DNS records. The behavior of how these default targets interact with targets specified in a `DNSEndpoint` CRD has been refined.
8+
9+
### New Behavior (default)
10+
11+
By default, ExternalDNS now has the following behavior:
12+
13+
- If a `DNSEndpoint` resource has targets specified in its `spec.endpoints[].targets` field, these targets will be used for the DNS record, **overriding** any targets specified via the `--default-targets` flag.
14+
- If a `DNSEndpoint` resource has an **empty** `targets` field, the targets from the `--default-targets` flag will be used. This allows for creating records that point to default load balancers or IPs without explicitly listing them in every `DNSEndpoint` resource.
15+
16+
### Legacy Behavior (`--force-default-targets`)
17+
18+
To maintain backward compatibility and support certain migration scenarios, the `--force-default-targets` flag is available.
19+
20+
- When `--force-default-targets` is used, ExternalDNS will **always** use the targets from `--default-targets`, regardless of whether the `DNSEndpoint` resource has targets specified or not.
21+
This flag allows for a smooth migration path to the new behavior. It allow keeping old CRD resources, allows to start removing targets from one by one resource and then remove the flag.
22+
23+
## Examples
24+
25+
Let's look at how this works in practice. Assume ExternalDNS is running with `--default-targets=1.2.3.4`.
26+
27+
### DNSEndpoint with Targets
28+
29+
Here is a `DNSEndpoint` with a target specified.
30+
31+
```yaml
32+
---
33+
apiVersion: externaldns.k8s.io/v1alpha1
34+
kind: DNSEndpoint
35+
metadata:
36+
name: targets
37+
namespace: default
38+
spec:
39+
endpoints:
40+
- dnsName: smoke-t.example.com
41+
recordTTL: 300
42+
recordType: CNAME
43+
targets:
44+
- placeholder
45+
```
46+
47+
- **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-t.example.com` will be created pointing to `placeholder`.
48+
- **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-t.example.com` will be created pointing to `1.2.3.4`. The `placeholder` target will be ignored.
49+
50+
### DNSEndpoint with Empty/No Targets
51+
52+
Here is a `DNSEndpoint` without any targets specified.
53+
54+
```yaml
55+
---
56+
apiVersion: externaldns.k8s.io/v1alpha1
57+
kind: DNSEndpoint
58+
metadata:
59+
name: no-targets
60+
namespace: default
61+
spec:
62+
endpoints:
63+
- dnsName: smoke-nt.example.com
64+
recordTTL: 300
65+
recordType: CNAME
66+
```
67+
68+
- **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`.
69+
- **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`.
70+
71+
`--force-default-targets` allows migration path to clean CRD resources.

pkg/apis/externaldns/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ type Config struct {
213213
TraefikDisableNew bool
214214
NAT64Networks []string
215215
ExcludeUnschedulable bool
216+
ForceDefaultTargets bool
216217
}
217218

218219
var defaultConfig = &Config{
@@ -375,6 +376,7 @@ var defaultConfig = &Config{
375376
WebhookProviderWriteTimeout: 10 * time.Second,
376377
WebhookServer: false,
377378
ZoneIDFilter: []string{},
379+
ForceDefaultTargets: false,
378380
}
379381

380382
// NewConfig returns new Config object
@@ -458,6 +460,7 @@ func App(cfg *Config) *kingpin.Application {
458460
app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
459461
app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
460462
app.Flag("default-targets", "Set globally default host/IP that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
463+
app.Flag("force-default-targets", "Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to (improved) legacy behavior which allows empty CRD targets for migration to new state)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets)
461464
app.Flag("exclude-record-types", "Record types to exclude from management; specify multiple times to exclude many; (optional)").Default().StringsVar(&cfg.ExcludeDNSRecordTypes)
462465
app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)
463466
app.Flag("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)").Default(strconv.FormatBool(defaultConfig.ExcludeUnschedulable)).BoolVar(&cfg.ExcludeUnschedulable)

source/crd.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,10 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
182182
}
183183

184184
for _, dnsEndpoint := range result.Items {
185-
// Make sure that all endpoints have targets for A or CNAME type
186185
var crdEndpoints []*endpoint.Endpoint
187186
for _, ep := range dnsEndpoint.Spec.Endpoints {
188187
if (ep.RecordType == endpoint.RecordTypeCNAME || ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA) && len(ep.Targets) < 1 {
189-
log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.Name, ep.DNSName)
190-
continue
188+
log.Debugf("Endpoint %s with DNSName %s has an empty list of targets, allowing it to pass through for default-targets processing", dnsEndpoint.Name, ep.DNSName)
191189
}
192190

193191
illegalTarget := false
@@ -202,7 +200,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
202200
}
203201
}
204202
if illegalTarget {
205-
log.Warnf("Endpoint %s with DNSName %s has an illegal target. The subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com')", dnsEndpoint.Name, ep.DNSName)
203+
log.Warnf("Endpoint %s/%s with DNSName %s has an illegal target format.", dnsEndpoint.Namespace, dnsEndpoint.Name, ep.DNSName)
206204
continue
207205
}
208206

source/crd_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ func testCRDSourceEndpoints(t *testing.T) {
224224
expectError: false,
225225
},
226226
{
227-
title: "invalid crd with no targets",
227+
title: "valid crd with no targets (relies on default-targets)",
228228
registeredAPIVersion: "test.k8s.io/v1alpha1",
229229
apiVersion: "test.k8s.io/v1alpha1",
230230
registeredKind: "DNSEndpoint",
@@ -233,13 +233,13 @@ func testCRDSourceEndpoints(t *testing.T) {
233233
registeredNamespace: "foo",
234234
endpoints: []*endpoint.Endpoint{
235235
{
236-
DNSName: "abc.example.org",
236+
DNSName: "no-targets.example.org",
237237
Targets: endpoint.Targets{},
238238
RecordType: endpoint.RecordTypeA,
239239
RecordTTL: 180,
240240
},
241241
},
242-
expectEndpoints: false,
242+
expectEndpoints: true,
243243
expectError: false,
244244
},
245245
{

source/multisource.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,50 @@ package source
1818

1919
import (
2020
"context"
21+
"strings"
2122

2223
"sigs.k8s.io/external-dns/endpoint"
24+
25+
log "github.com/sirupsen/logrus"
2326
)
2427

2528
// multiSource is a Source that merges the endpoints of its nested Sources.
2629
type multiSource struct {
27-
children []Source
28-
defaultTargets []string
30+
children []Source
31+
defaultTargets []string
32+
forceDefaultTargets bool
2933
}
3034

3135
// Endpoints collects endpoints of all nested Sources and returns them in a single slice.
3236
func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
3337
result := []*endpoint.Endpoint{}
38+
hasDefaultTargets := len(ms.defaultTargets) > 0
3439

3540
for _, s := range ms.children {
3641
endpoints, err := s.Endpoints(ctx)
3742
if err != nil {
3843
return nil, err
3944
}
40-
if len(ms.defaultTargets) > 0 {
41-
for i := range endpoints {
45+
46+
if !hasDefaultTargets {
47+
result = append(result, endpoints...)
48+
continue
49+
}
50+
51+
for i := range endpoints {
52+
hasSourceTargets := len(endpoints[i].Targets) > 0
53+
54+
if ms.forceDefaultTargets || !hasSourceTargets {
4255
eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "")
4356
for _, ep := range eps {
4457
ep.Labels = endpoints[i].Labels
4558
}
4659
result = append(result, eps...)
60+
continue
4761
}
48-
} else {
49-
result = append(result, endpoints...)
62+
63+
log.Warnf("Source provided targets for %q (%s), ignoring default targets [%s] due to new behavior. Use --force-default-targets to revert to old behavior.", endpoints[i].DNSName, endpoints[i].RecordType, strings.Join(ms.defaultTargets, ", "))
64+
result = append(result, endpoints[i])
5065
}
5166
}
5267

@@ -60,6 +75,6 @@ func (ms *multiSource) AddEventHandler(ctx context.Context, handler func()) {
6075
}
6176

6277
// NewMultiSource creates a new multiSource.
63-
func NewMultiSource(children []Source, defaultTargets []string) Source {
64-
return &multiSource{children: children, defaultTargets: defaultTargets}
78+
func NewMultiSource(children []Source, defaultTargets []string, forceDefaultTargets bool) Source {
79+
return &multiSource{children: children, defaultTargets: defaultTargets, forceDefaultTargets: forceDefaultTargets}
6580
}

0 commit comments

Comments
 (0)