From 569a82a035c446b8f88be8bfc448020eace74405 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 22 Apr 2025 13:41:54 +0200 Subject: [PATCH 01/14] BREAKING CHANGE: Improve default targets management --- controller/execute.go | 2 +- docs/flags.md | 1 + pkg/apis/externaldns/types.go | 3 + source/crd.go | 19 +++-- source/crd_test.go | 19 +++++ source/multisource.go | 38 +++++++-- source/multisource_test.go | 149 ++++++++++++++++++++++++---------- 7 files changed, 171 insertions(+), 60 deletions(-) diff --git a/controller/execute.go b/controller/execute.go index 4113fd644..3855b92b7 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -126,7 +126,7 @@ func Execute() { targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets) // Combine multiple sources into a single, deduplicated source. - endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets)) + endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, cfg.ForceDefaultTargets)) endpointsSource = source.NewNAT64Source(endpointsSource, cfg.NAT64Networks) endpointsSource = source.NewTargetFilterSource(endpointsSource, targetFilter) diff --git a/docs/flags.md b/docs/flags.md index 9218ba868..4e14f3281 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -46,6 +46,7 @@ | `--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) | | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | | `--exclude-target-net=EXCLUDE-TARGET-NET` | Exclude target nets (optional) | +| `--[no-]force-default-targets` | Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to legacy behavior, default is false) | | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | | `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 36cd3a9bb..7420c6c6b 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -214,6 +214,7 @@ type Config struct { TraefikDisableNew bool NAT64Networks []string ExcludeUnschedulable bool + ForceDefaultTargets bool } var defaultConfig = &Config{ @@ -378,6 +379,7 @@ var defaultConfig = &Config{ TraefikDisableNew: false, NAT64Networks: []string{}, ExcludeUnschedulable: true, + ForceDefaultTargets: false, } // NewConfig returns new Config object @@ -482,6 +484,7 @@ func App(cfg *Config) *kingpin.Application { 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) app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter) app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) + app.Flag("force-default-targets", "Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to legacy behavior, default is false)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets) app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy) app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks) diff --git a/source/crd.go b/source/crd.go index 70a724073..46d4e99cd 100644 --- a/source/crd.go +++ b/source/crd.go @@ -180,14 +180,12 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } for _, dnsEndpoint := range result.Items { - // Make sure that all endpoints have targets for A or CNAME type - crdEndpoints := []*endpoint.Endpoint{} + crdEndpoints := []*endpoint.Endpoint{} // Temporary list for endpoints from this specific CRD for _, ep := range dnsEndpoint.Spec.Endpoints { - if (ep.RecordType == "CNAME" || ep.RecordType == "A" || ep.RecordType == "AAAA") && len(ep.Targets) < 1 { - log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.Name, ep.DNSName) - continue - } + // Note: Target validation (like empty target check for A/CNAME) is removed here + // to allow default-targets logic to function correctly. + // Validate target format (e.g., trailing dots) illegalTarget := false for _, target := range ep.Targets { if ep.RecordType != "NAPTR" && strings.HasSuffix(target, ".") { @@ -200,18 +198,23 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } } if illegalTarget { - 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) - continue + log.Warnf("Endpoint %s/%s with DNSName %s has an illegal target format.", dnsEndpoint.Namespace, dnsEndpoint.Name, ep.DNSName) + continue // Skip this specific endpoint } + // Ensure labels are initialized if ep.Labels == nil { ep.Labels = endpoint.NewLabels() } + // Add the valid endpoint to a temporary list for this CRD crdEndpoints = append(crdEndpoints, ep) } + // Add resource label to all valid endpoints from this CRD cs.setResourceLabel(&dnsEndpoint, crdEndpoints) + + // Add the processed endpoints for this CRD to the main list endpoints = append(endpoints, crdEndpoints...) if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation { diff --git a/source/crd_test.go b/source/crd_test.go index fb57146b8..0c95bb93b 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -234,6 +234,25 @@ func testCRDSourceEndpoints(t *testing.T) { expectEndpoints: false, expectError: false, }, + { + title: "valid crd with no targets (relies on default-targets)", + registeredAPIVersion: "test.k8s.io/v1alpha1", + apiVersion: "test.k8s.io/v1alpha1", + registeredKind: "DNSEndpoint", + kind: "DNSEndpoint", + namespace: "foo", + registeredNamespace: "foo", + endpoints: []*endpoint.Endpoint{ + { + DNSName: "no-targets.example.org", + Targets: endpoint.Targets{}, // Empty targets, should rely on default-targets later + RecordType: endpoint.RecordTypeA, + RecordTTL: 180, + }, + }, + expectEndpoints: true, // Expect the endpoint to be processed, default targets applied later + expectError: false, + }, { title: "valid crd gvk with single endpoint", registeredAPIVersion: "test.k8s.io/v1alpha1", diff --git a/source/multisource.go b/source/multisource.go index 542b7d93d..6ed6f3840 100644 --- a/source/multisource.go +++ b/source/multisource.go @@ -18,34 +18,54 @@ package source import ( "context" + "strings" + log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" ) // multiSource is a Source that merges the endpoints of its nested Sources. type multiSource struct { - children []Source - defaultTargets []string + children []Source + defaultTargets []string + forceDefaultTargets bool } // Endpoints collects endpoints of all nested Sources and returns them in a single slice. func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { result := []*endpoint.Endpoint{} + hasDefaultTargets := len(ms.defaultTargets) > 0 for _, s := range ms.children { endpoints, err := s.Endpoints(ctx) if err != nil { return nil, err } - if len(ms.defaultTargets) > 0 { + + if hasDefaultTargets { for i := range endpoints { - eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") - for _, ep := range eps { - ep.Labels = endpoints[i].Labels + hasSourceTargets := len(endpoints[i].Targets) > 0 + + if !ms.forceDefaultTargets && hasSourceTargets { + // New behavior (default): Source targets exist, use them and ignore defaults. + // Log a warning every time this happens if defaults are configured. + 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, ", ")) + result = append(result, endpoints[i]) + continue + } else if ms.forceDefaultTargets || !hasSourceTargets { + // Old behavior (forced via flag) OR New behavior (source targets are empty): Apply default targets. + eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") + for _, ep := range eps { + ep.Labels = endpoints[i].Labels + } + result = append(result, eps...) + } else { + // This case should logically not be reached given the conditions above, but handles completeness. + result = append(result, endpoints[i]) } - result = append(result, eps...) } } else { + // No default targets configured, just append source endpoints. result = append(result, endpoints...) } } @@ -60,6 +80,6 @@ func (ms *multiSource) AddEventHandler(ctx context.Context, handler func()) { } // NewMultiSource creates a new multiSource. -func NewMultiSource(children []Source, defaultTargets []string) Source { - return &multiSource{children: children, defaultTargets: defaultTargets} +func NewMultiSource(children []Source, defaultTargets []string, forceDefaultTargets bool) Source { + return &multiSource{children: children, defaultTargets: defaultTargets, forceDefaultTargets: forceDefaultTargets} } diff --git a/source/multisource_test.go b/source/multisource_test.go index 56a3fb604..37830cc22 100644 --- a/source/multisource_test.go +++ b/source/multisource_test.go @@ -89,7 +89,7 @@ func testMultiSourceEndpoints(t *testing.T) { } // Create our object under test and get the endpoints. - source := NewMultiSource(sources, nil) + source := NewMultiSource(sources, nil, false) // Get endpoints from the source. endpoints, err := source.Endpoints(context.Background()) @@ -116,7 +116,7 @@ func testMultiSourceEndpointsWithError(t *testing.T) { src.On("Endpoints").Return(nil, errSomeError) // Create our object under test and get the endpoints. - source := NewMultiSource([]Source{src}, nil) + source := NewMultiSource([]Source{src}, nil, false) // Get endpoints from our source. _, err := source.Endpoints(context.Background()) @@ -127,44 +127,109 @@ func testMultiSourceEndpointsWithError(t *testing.T) { } func testMultiSourceEndpointsDefaultTargets(t *testing.T) { - // Create the expected default targets - defaultTargetsA := []string{"127.0.0.1", "127.0.0.2"} - defaultTargetsAAAA := []string{"2001:db8::1"} - defaultTargetsCName := []string{"foo.example.org"} - defaultTargets := append(defaultTargetsA, defaultTargetsCName...) - defaultTargets = append(defaultTargets, defaultTargetsAAAA...) - labels := endpoint.Labels{"foo": "bar"} - - // Create the expected endpoints - expectedEndpoints := []*endpoint.Endpoint{ - {DNSName: "foo", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, - {DNSName: "bar", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, - {DNSName: "foo", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, - {DNSName: "bar", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, - {DNSName: "foo", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, - {DNSName: "bar", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, - } - - // Create the source endpoints with different targets - sourceEndpoints := []*endpoint.Endpoint{ - {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, - {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, - } - - // Create a mocked source returning source targets - src := new(testutils.MockSource) - src.On("Endpoints").Return(sourceEndpoints, nil) - - // Create our object under test with non-empty defaultTargets and get the endpoints. - source := NewMultiSource([]Source{src}, defaultTargets) - - // Get endpoints from our source. - endpoints, err := source.Endpoints(context.Background()) - require.NoError(t, err) - - // Validate returned endpoints against desired endpoints. - validateEndpoints(t, endpoints, expectedEndpoints) - - // Validate that the nested sources were called. - src.AssertExpectations(t) + t.Run("Defaults applied when source targets are empty", func(t *testing.T) { + defaultTargetsA := []string{"127.0.0.1", "127.0.0.2"} + defaultTargetsAAAA := []string{"2001:db8::1"} + defaultTargetsCName := []string{"foo.example.org"} + defaultTargets := append(defaultTargetsA, defaultTargetsCName...) + defaultTargets = append(defaultTargets, defaultTargetsAAAA...) + labels := endpoint.Labels{"foo": "bar"} + + // Endpoints FROM SOURCE have NO targets + sourceEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: endpoint.Targets{}, Labels: labels}, + {DNSName: "bar", Targets: endpoint.Targets{}, Labels: labels}, + } + + // Expected endpoints SHOULD HAVE the default targets applied + expectedEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, + {DNSName: "foo", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, + {DNSName: "foo", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, + } + + src := new(testutils.MockSource) + src.On("Endpoints").Return(sourceEndpoints, nil) + + // Test with forceDefaultTargets=false (default behavior) + source := NewMultiSource([]Source{src}, defaultTargets, false) + + endpoints, err := source.Endpoints(context.Background()) + require.NoError(t, err) + + validateEndpoints(t, endpoints, expectedEndpoints) + + src.AssertExpectations(t) + }) + + t.Run("Defaults NOT applied when source targets exist", func(t *testing.T) { + defaultTargets := []string{"127.0.0.1"} // Default target + labels := endpoint.Labels{"foo": "bar"} + + // Endpoints FROM SOURCE HAVE targets + sourceEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, + {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, + } + + // Expected endpoints SHOULD MATCH the source endpoints (defaults ignored) + expectedEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, + {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, + } + + src := new(testutils.MockSource) + src.On("Endpoints").Return(sourceEndpoints, nil) + + // Test with forceDefaultTargets=false (default behavior) + source := NewMultiSource([]Source{src}, defaultTargets, false) + + endpoints, err := source.Endpoints(context.Background()) + require.NoError(t, err) + + validateEndpoints(t, endpoints, expectedEndpoints) + + src.AssertExpectations(t) + }) + + t.Run("Defaults forced when source targets exist and flag is set", func(t *testing.T) { + defaultTargetsA := []string{"127.0.0.1", "127.0.0.2"} + defaultTargetsAAAA := []string{"2001:db8::1"} + defaultTargetsCName := []string{"foo.example.org"} + defaultTargets := append(defaultTargetsA, defaultTargetsCName...) + defaultTargets = append(defaultTargets, defaultTargetsAAAA...) + labels := endpoint.Labels{"foo": "bar"} + + // Endpoints FROM SOURCE HAVE targets + sourceEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, + {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, + } + + // Expected endpoints SHOULD HAVE the default targets applied (old behavior) + expectedEndpoints := []*endpoint.Endpoint{ + {DNSName: "foo", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, + {DNSName: "foo", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, + {DNSName: "foo", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, + {DNSName: "bar", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, + } + + src := new(testutils.MockSource) + src.On("Endpoints").Return(sourceEndpoints, nil) + + // Test with forceDefaultTargets=true (legacy behavior) + source := NewMultiSource([]Source{src}, defaultTargets, true) + + endpoints, err := source.Endpoints(context.Background()) + require.NoError(t, err) + + validateEndpoints(t, endpoints, expectedEndpoints) + + src.AssertExpectations(t) + }) } From 6b84cffec625e10ee4553a754dc86c9a05769d01 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 22 Apr 2025 15:51:05 +0200 Subject: [PATCH 02/14] fix: Remove old test case --- source/crd_test.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/source/crd_test.go b/source/crd_test.go index 0c95bb93b..3785ca02f 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -215,25 +215,6 @@ func testCRDSourceEndpoints(t *testing.T) { expectEndpoints: false, expectError: false, }, - { - title: "invalid crd with no targets", - registeredAPIVersion: "test.k8s.io/v1alpha1", - apiVersion: "test.k8s.io/v1alpha1", - registeredKind: "DNSEndpoint", - kind: "DNSEndpoint", - namespace: "foo", - registeredNamespace: "foo", - endpoints: []*endpoint.Endpoint{ - { - DNSName: "abc.example.org", - Targets: endpoint.Targets{}, - RecordType: endpoint.RecordTypeA, - RecordTTL: 180, - }, - }, - expectEndpoints: false, - expectError: false, - }, { title: "valid crd with no targets (relies on default-targets)", registeredAPIVersion: "test.k8s.io/v1alpha1", From 01f71b05f7590aa9f66669def4be5e7a6b7bbccb Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Sun, 18 May 2025 12:19:25 +0200 Subject: [PATCH 03/14] fix: Test confirming legacy mode allows empty CRD targets --- source/multisource_test.go | 41 +++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/source/multisource_test.go b/source/multisource_test.go index 37830cc22..a8c70a6fa 100644 --- a/source/multisource_test.go +++ b/source/multisource_test.go @@ -135,7 +135,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { defaultTargets = append(defaultTargets, defaultTargetsAAAA...) labels := endpoint.Labels{"foo": "bar"} - // Endpoints FROM SOURCE have NO targets + // Endpoints FROM SOURCE has NO targets sourceEndpoints := []*endpoint.Endpoint{ {DNSName: "foo", Targets: endpoint.Targets{}, Labels: labels}, {DNSName: "bar", Targets: endpoint.Targets{}, Labels: labels}, @@ -169,7 +169,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { defaultTargets := []string{"127.0.0.1"} // Default target labels := endpoint.Labels{"foo": "bar"} - // Endpoints FROM SOURCE HAVE targets + // Endpoints FROM SOURCE HAS targets sourceEndpoints := []*endpoint.Endpoint{ {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, @@ -203,7 +203,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { defaultTargets = append(defaultTargets, defaultTargetsAAAA...) labels := endpoint.Labels{"foo": "bar"} - // Endpoints FROM SOURCE HAVE targets + // Endpoints FROM SOURCE HAS targets sourceEndpoints := []*endpoint.Endpoint{ {DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}, Labels: labels}, {DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}, Labels: labels}, @@ -232,4 +232,39 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { src.AssertExpectations(t) }) + + t.Run("Defaults applied when source targets are empty and flag is set", func(t *testing.T) { + defaultTargetsA := []string{"127.0.0.1", "127.0.0.2"} + defaultTargetsAAAA := []string{"2001:db8::1"} + defaultTargetsCName := []string{"foo.example.org"} + defaultTargets := append(defaultTargetsA, defaultTargetsAAAA...) + defaultTargets = append(defaultTargets, defaultTargetsCName...) + + labels := endpoint.Labels{"foo": "bar"} + + // Endpoints FROM SOURCE has NO targets + sourceEndpoints := []*endpoint.Endpoint{ + {DNSName: "empty-target-test", Targets: endpoint.Targets{}, Labels: labels}, + } + + // Expected endpoints SHOULD HAVE the default targets applied + expectedEndpoints := []*endpoint.Endpoint{ + {DNSName: "empty-target-test", Targets: defaultTargetsA, RecordType: "A", Labels: labels}, + {DNSName: "empty-target-test", Targets: defaultTargetsAAAA, RecordType: "AAAA", Labels: labels}, + {DNSName: "empty-target-test", Targets: defaultTargetsCName, RecordType: "CNAME", Labels: labels}, + } + + src := new(testutils.MockSource) + src.On("Endpoints").Return(sourceEndpoints, nil) + + // Test with forceDefaultTargets=true + source := NewMultiSource([]Source{src}, defaultTargets, true) + + endpoints, err := source.Endpoints(context.Background()) + require.NoError(t, err) + + validateEndpoints(t, endpoints, expectedEndpoints) + + src.AssertExpectations(t) + }) } From 84586619bb5dbe3db2b7a0a15c51efe7f6a17426 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Fri, 23 May 2025 12:23:00 +0200 Subject: [PATCH 04/14] fix: Remove comments --- source/crd.go | 12 ++---------- source/crd_test.go | 4 ++-- source/multisource.go | 5 ----- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/source/crd.go b/source/crd.go index d6bd6253c..70cee0f12 100644 --- a/source/crd.go +++ b/source/crd.go @@ -180,12 +180,8 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } for _, dnsEndpoint := range result.Items { - crdEndpoints := []*endpoint.Endpoint{} // Temporary list for endpoints from this specific CRD + crdEndpoints := []*endpoint.Endpoint{} for _, ep := range dnsEndpoint.Spec.Endpoints { - // Note: Target validation (like empty target check for A/CNAME) is removed here - // to allow default-targets logic to function correctly. - - // Validate target format (e.g., trailing dots) illegalTarget := false for _, target := range ep.Targets { if ep.RecordType != "NAPTR" && strings.HasSuffix(target, ".") { @@ -199,22 +195,18 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } if illegalTarget { log.Warnf("Endpoint %s/%s with DNSName %s has an illegal target format.", dnsEndpoint.Namespace, dnsEndpoint.Name, ep.DNSName) - continue // Skip this specific endpoint + continue } - // Ensure labels are initialized if ep.Labels == nil { ep.Labels = endpoint.NewLabels() } - // Add the valid endpoint to a temporary list for this CRD crdEndpoints = append(crdEndpoints, ep) } - // Add resource label to all valid endpoints from this CRD cs.setResourceLabel(&dnsEndpoint, crdEndpoints) - // Add the processed endpoints for this CRD to the main list endpoints = append(endpoints, crdEndpoints...) if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation { diff --git a/source/crd_test.go b/source/crd_test.go index de0187489..8d4fd40dd 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -226,12 +226,12 @@ func testCRDSourceEndpoints(t *testing.T) { endpoints: []*endpoint.Endpoint{ { DNSName: "no-targets.example.org", - Targets: endpoint.Targets{}, // Empty targets, should rely on default-targets later + Targets: endpoint.Targets{}, RecordType: endpoint.RecordTypeA, RecordTTL: 180, }, }, - expectEndpoints: true, // Expect the endpoint to be processed, default targets applied later + expectEndpoints: true, expectError: false, }, { diff --git a/source/multisource.go b/source/multisource.go index 6ed6f3840..49555d741 100644 --- a/source/multisource.go +++ b/source/multisource.go @@ -47,25 +47,20 @@ func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err hasSourceTargets := len(endpoints[i].Targets) > 0 if !ms.forceDefaultTargets && hasSourceTargets { - // New behavior (default): Source targets exist, use them and ignore defaults. - // Log a warning every time this happens if defaults are configured. 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, ", ")) result = append(result, endpoints[i]) continue } else if ms.forceDefaultTargets || !hasSourceTargets { - // Old behavior (forced via flag) OR New behavior (source targets are empty): Apply default targets. eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") for _, ep := range eps { ep.Labels = endpoints[i].Labels } result = append(result, eps...) } else { - // This case should logically not be reached given the conditions above, but handles completeness. result = append(result, endpoints[i]) } } } else { - // No default targets configured, just append source endpoints. result = append(result, endpoints...) } } From 404d4643deceb65682bef9996302c9c4dc701b60 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Fri, 23 May 2025 12:40:34 +0200 Subject: [PATCH 05/14] fix: Move flag definition closer to detault-targets --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 19d8e460c..70b2006bd 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -468,6 +468,7 @@ func App(cfg *Config) *kingpin.Application { 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) 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) 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) + 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, default is false)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets) app.Flag("exclude-record-types", "Record types to exclude from management; specify multiple times to exclude many; (optional)").Default().StringsVar(&cfg.ExcludeDNSRecordTypes) app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) app.Flag("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)").Default(strconv.FormatBool(defaultConfig.ExcludeUnschedulable)).BoolVar(&cfg.ExcludeUnschedulable) @@ -493,7 +494,6 @@ func App(cfg *Config) *kingpin.Application { app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "f5-transportserver", "traefik-proxy") app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter) - app.Flag("force-default-targets", "Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to legacy behavior, default is false)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets) app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy) app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) From d9c146bd92dc69ce1bbd1b6d2d17e707bc05c8f8 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 19:38:04 +0200 Subject: [PATCH 06/14] fix: Initial merge adaptation --- controller/execute.go | 2 +- docs/flags.md | 2 +- source/store.go | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/controller/execute.go b/controller/execute.go index 19c9ad736..0b0e427cf 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -420,7 +420,7 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e return nil, err } // Combine multiple sources into a single, deduplicated source. - combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets)) + combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets)) // Filter targets targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets) combinedSource = source.NewNAT64Source(combinedSource, cfg.NAT64Networks) diff --git a/docs/flags.md b/docs/flags.md index b900004fa..997b0a394 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -25,6 +25,7 @@ | `--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 | | `--crd-source-kind="DNSEndpoint"` | Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion | | `--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) | +| `--[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, default is false) | | `--exclude-record-types=EXCLUDE-RECORD-TYPES` | Record types to exclude from management; specify multiple times to exclude many; (optional) | | `--exclude-target-net=EXCLUDE-TARGET-NET` | Exclude target nets (optional) | | `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) | @@ -49,7 +50,6 @@ | `--service-type-filter=SERVICE-TYPE-FILTER` | The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName) | | `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy) | | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | -| `--[no-]force-default-targets` | Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to legacy behavior, default is false) | | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | | `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) | diff --git a/source/store.go b/source/store.go index dc5812ad5..82a03f1b9 100644 --- a/source/store.go +++ b/source/store.go @@ -78,6 +78,7 @@ type Config struct { SkipperRouteGroupVersion string RequestTimeout time.Duration DefaultTargets []string + ForceDefaultTargets bool OCPRouterName string UpdateEvents bool ResolveLoadBalancerHostname bool @@ -123,6 +124,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion, RequestTimeout: cfg.RequestTimeout, DefaultTargets: cfg.DefaultTargets, + ForceDefaultTargets: cfg.ForceDefaultTargets, OCPRouterName: cfg.OCPRouterName, UpdateEvents: cfg.UpdateEvents, ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname, From e4d758174e7c08f83755beb2a390b797175da97d Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 19:53:43 +0200 Subject: [PATCH 07/14] fix: Improved legacy needs a chance to work with empty CRD list --- source/crd.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/crd.go b/source/crd.go index 33f6045ee..a0c54e1e0 100644 --- a/source/crd.go +++ b/source/crd.go @@ -182,12 +182,10 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } for _, dnsEndpoint := range result.Items { - // Make sure that all endpoints have targets for A or CNAME type var crdEndpoints []*endpoint.Endpoint for _, ep := range dnsEndpoint.Spec.Endpoints { if (ep.RecordType == endpoint.RecordTypeCNAME || ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA) && len(ep.Targets) < 1 { - log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.Name, ep.DNSName) - continue + 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) } illegalTarget := false From e7e638ec2949a129e016f559cc2d494ba2d28c79 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 20:08:34 +0200 Subject: [PATCH 08/14] fix: Code coverage and dead code --- source/multisource.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/multisource.go b/source/multisource.go index 49555d741..be65cdb5d 100644 --- a/source/multisource.go +++ b/source/multisource.go @@ -50,14 +50,12 @@ func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err 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, ", ")) result = append(result, endpoints[i]) continue - } else if ms.forceDefaultTargets || !hasSourceTargets { + } else { eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") for _, ep := range eps { ep.Labels = endpoints[i].Labels } result = append(result, eps...) - } else { - result = append(result, endpoints[i]) } } } else { From b6350c7f752560ae6aa09f479968153133146e3b Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 20:15:12 +0200 Subject: [PATCH 09/14] fix: Simpler Endpoints logic --- source/multisource.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/source/multisource.go b/source/multisource.go index be65cdb5d..f8aea1c6e 100644 --- a/source/multisource.go +++ b/source/multisource.go @@ -42,24 +42,25 @@ func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err return nil, err } - if hasDefaultTargets { - for i := range endpoints { - hasSourceTargets := len(endpoints[i].Targets) > 0 - - if !ms.forceDefaultTargets && hasSourceTargets { - 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, ", ")) - result = append(result, endpoints[i]) - continue - } else { - eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") - for _, ep := range eps { - ep.Labels = endpoints[i].Labels - } - result = append(result, eps...) + if !hasDefaultTargets { + result = append(result, endpoints...) + continue + } + + for i := range endpoints { + hasSourceTargets := len(endpoints[i].Targets) > 0 + + if ms.forceDefaultTargets || !hasSourceTargets { + eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") + for _, ep := range eps { + ep.Labels = endpoints[i].Labels } + result = append(result, eps...) + continue } - } else { - result = append(result, endpoints...) + + 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, ", ")) + result = append(result, endpoints[i]) } } From ae7e26b4c083bccb3b694f0f63beb1adb8bce616 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 22:12:16 +0200 Subject: [PATCH 10/14] fix: Flag description --- docs/flags.md | 2 +- pkg/apis/externaldns/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/flags.md b/docs/flags.md index 997b0a394..c881660c3 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -25,7 +25,7 @@ | `--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 | | `--crd-source-kind="DNSEndpoint"` | Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion | | `--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) | -| `--[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, default is false) | +| `--[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) | | `--exclude-record-types=EXCLUDE-RECORD-TYPES` | Record types to exclude from management; specify multiple times to exclude many; (optional) | | `--exclude-target-net=EXCLUDE-TARGET-NET` | Exclude target nets (optional) | | `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 2a7d1ac71..fa5818bd6 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -460,7 +460,7 @@ func App(cfg *Config) *kingpin.Application { 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) 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) 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) - 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, default is false)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets) + 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) app.Flag("exclude-record-types", "Record types to exclude from management; specify multiple times to exclude many; (optional)").Default().StringsVar(&cfg.ExcludeDNSRecordTypes) app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) app.Flag("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)").Default(strconv.FormatBool(defaultConfig.ExcludeUnschedulable)).BoolVar(&cfg.ExcludeUnschedulable) From 4cc2452506d82b707c496cd9d4e0717a7d36d5c2 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 22:27:57 +0200 Subject: [PATCH 11/14] feat: Add tutorial --- docs/tutorials/crd.md | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/tutorials/crd.md diff --git a/docs/tutorials/crd.md b/docs/tutorials/crd.md new file mode 100644 index 000000000..8f844261e --- /dev/null +++ b/docs/tutorials/crd.md @@ -0,0 +1,69 @@ +# Using CRD Source for DNS Records + +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. + +## Default Targets and CRD Targets + +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. + +### New Behavior (default) + +By default, ExternalDNS now has the following behavior: +- 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. +- 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. + +### Legacy Behavior (`--force-default-targets`) + +To maintain backward compatibility and support certain migration scenarios, the `--force-default-targets` flag is available. + +- 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. 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. + +## Examples + +Let's look at how this works in practice. Assume ExternalDNS is running with `--default-targets=1.2.3.4`. + +### DNSEndpoint with Targets + +Here is a `DNSEndpoint` with a target specified. + +```yaml +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: targets + namespace: default +spec: + endpoints: + - dnsName: smoke-t.example.com + recordTTL: 300 + recordType: CNAME + targets: + - placeholder +``` + +* **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-t.example.com` will be created pointing to `placeholder`. +* **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. + +### DNSEndpoint with Empty/No Targets + +Here is a `DNSEndpoint` without any targets specified. + +```yaml +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: no-targets + namespace: default +spec: + endpoints: + - dnsName: smoke-nt.example.com + recordTTL: 300 + recordType: CNAME +``` + +* **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. +* **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. + +`--force-default-targets` allows migration path to clean CRD resources. From f77a3a5262cb760ce22cb26c88e8843951966ed9 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 22:38:42 +0200 Subject: [PATCH 12/14] fix: Improve linting --- docs/tutorials/crd.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/crd.md b/docs/tutorials/crd.md index 8f844261e..b74f0ca58 100644 --- a/docs/tutorials/crd.md +++ b/docs/tutorials/crd.md @@ -9,6 +9,7 @@ ExternalDNS has a `--default-targets` flag that can be used to specify a default ### New Behavior (default) By default, ExternalDNS now has the following behavior: + - 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. - 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. @@ -42,8 +43,8 @@ spec: - placeholder ``` -* **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-t.example.com` will be created pointing to `placeholder`. -* **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. +- **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-t.example.com` will be created pointing to `placeholder`. +- **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. ### DNSEndpoint with Empty/No Targets @@ -63,7 +64,7 @@ spec: recordType: CNAME ``` -* **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. -* **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. +- **Without `--force-default-targets` (New Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. +- **With `--force-default-targets` (Legacy Behavior):** A CNAME record for `smoke-nt.example.com` will be created pointing to `1.2.3.4`. `--force-default-targets` allows migration path to clean CRD resources. From 2676686f8904276f1f9476584ca01628d77d2185 Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 22:45:47 +0200 Subject: [PATCH 13/14] fix: Improve linting --- docs/tutorials/crd.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/crd.md b/docs/tutorials/crd.md index b74f0ca58..81834a8b9 100644 --- a/docs/tutorials/crd.md +++ b/docs/tutorials/crd.md @@ -17,7 +17,8 @@ By default, ExternalDNS now has the following behavior: To maintain backward compatibility and support certain migration scenarios, the `--force-default-targets` flag is available. -- 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. 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. +- 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. +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. ## Examples From 587a55d20eb9273fdbe368b6450ba069943c12bd Mon Sep 17 00:00:00 2001 From: Alen Zubic Date: Tue, 17 Jun 2025 22:54:18 +0200 Subject: [PATCH 14/14] fix: Import linting --- source/multisource.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/multisource.go b/source/multisource.go index f8aea1c6e..80f5335b8 100644 --- a/source/multisource.go +++ b/source/multisource.go @@ -20,8 +20,9 @@ import ( "context" "strings" - log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" + + log "github.com/sirupsen/logrus" ) // multiSource is a Source that merges the endpoints of its nested Sources.