Skip to content

Commit c0a9eed

Browse files
authored
feat(source): optional exclusion of unschedulable nodes (#5045)
* feat(source/node): Make exclusion of unschedulable Nodes configurable This fixes a behavioral regression introduced in #4761, where nodes that were previously added to DNS are removed when they are considered unschedulable, for example due to automated maintenance tasks. This change will introduce a new flag called `exclude-unschedulable`, which defaults to `true` in order to keep in line with the current behavior. However, it would also be reasonable to restore the initial behavior before * Allow testing for expected log entries in testNodeSourceEndpoints This commit adds the required logic to be able to test for the existence (and absence) of certain log messages in testNodeSourceEndpoints. As an example, this is implemented for the tests around excludeUnschedulable. A side effect of using LogsToBuffer is that tests can't run in parallel due to the log buffer being shared across all parallel test cases. As such, these specific tests are now executed one after another. * Ensure logging is only hooked for tests that require it * Document new exclude-unschedulable flag for nodes source
1 parent c5af75e commit c0a9eed

File tree

7 files changed

+99
-47
lines changed

7 files changed

+99
-47
lines changed

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
| `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group |
5050
| `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group |
5151
| `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) |
52+
| `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) |
5253
| `--[no-]expose-internal-ipv6` | When using the node source, expose internal IPv6 addresses (optional). Default is true. |
5354
| `--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, ibmcloud, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) |
5455
| `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. |

docs/sources/nodes.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ The node source adds an `A` record per each node `externalIP` (if not found, any
77
It also adds an `AAAA` record per each node IPv6 `internalIP`. Refer to the [IPv6 Behavior](#ipv6-behavior) section for more details.
88
The TTL of the records can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation.
99

10-
Nodes marked as **Unschedulable** as per [core/v1/NodeSpec](https://pkg.go.dev/k8s.io/[email protected]/core/v1#NodeSpec) are excluded.
11-
This avoid exposing Unhealthy, NotReady or SchedulingDisabled (cordon) nodes.
10+
Nodes marked as **Unschedulable** as per [core/v1/NodeSpec](https://pkg.go.dev/k8s.io/[email protected]/core/v1#NodeSpec) are excluded by default.
11+
As such, no DNS records are created for Unhealthy, NotReady or SchedulingDisabled (cordon) nodes (and existing ones are removed).
12+
In case you want to override the default, for example if you manage per-host DNS records via ExternalDNS, you can specify `--no-exclude-unschedulable` to always expose nodes no matter their status.
1213

1314
## IPv6 Behavior
1415

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
TraefikDisableLegacy bool
214214
TraefikDisableNew bool
215215
NAT64Networks []string
216+
ExcludeUnschedulable bool
216217
}
217218

218219
var defaultConfig = &Config{
@@ -376,6 +377,7 @@ var defaultConfig = &Config{
376377
TraefikDisableLegacy: false,
377378
TraefikDisableNew: false,
378379
NAT64Networks: []string{},
380+
ExcludeUnschedulable: true,
379381
}
380382

381383
// NewConfig returns new Config object
@@ -483,6 +485,7 @@ func App(cfg *Config) *kingpin.Application {
483485
app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy)
484486
app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew)
485487
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)
488+
app.Flag("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)").Default(strconv.FormatBool(defaultConfig.ExcludeUnschedulable)).BoolVar(&cfg.ExcludeUnschedulable)
486489
app.Flag("expose-internal-ipv6", "When using the node source, expose internal IPv6 addresses (optional). Default is true.").BoolVar(&cfg.ExposeInternalIPV6)
487490

488491
// Flags related to providers

pkg/apis/externaldns/types_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ var (
132132
WebhookProviderURL: "http://localhost:8888",
133133
WebhookProviderReadTimeout: 5 * time.Second,
134134
WebhookProviderWriteTimeout: 10 * time.Second,
135+
ExcludeUnschedulable: true,
135136
}
136137

137138
overriddenConfig = &Config{
@@ -247,6 +248,7 @@ var (
247248
WebhookProviderURL: "http://localhost:8888",
248249
WebhookProviderReadTimeout: 5 * time.Second,
249250
WebhookProviderWriteTimeout: 10 * time.Second,
251+
ExcludeUnschedulable: false,
250252
}
251253
)
252254

@@ -386,6 +388,7 @@ func TestParseFlags(t *testing.T) {
386388
"--managed-record-types=AAAA",
387389
"--managed-record-types=CNAME",
388390
"--managed-record-types=NS",
391+
"--no-exclude-unschedulable",
389392
"--rfc2136-batch-change-size=100",
390393
"--rfc2136-load-balancing-strategy=round-robin",
391394
"--rfc2136-host=rfc2136-host1",
@@ -505,6 +508,7 @@ func TestParseFlags(t *testing.T) {
505508
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
506509
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
507510
"EXTERNAL_DNS_MANAGED_RECORD_TYPES": "A\nAAAA\nCNAME\nNS",
511+
"EXTERNAL_DNS_EXCLUDE_UNSCHEDULABLE": "false",
508512
"EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100",
509513
"EXTERNAL_DNS_RFC2136_LOAD_BALANCING_STRATEGY": "round-robin",
510514
"EXTERNAL_DNS_RFC2136_HOST": "rfc2136-host1\nrfc2136-host2",

source/node.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,17 @@ import (
3636
const warningMsg = "The default behavior of exposing internal IPv6 addresses will change in the next minor version. Use --no-expose-internal-ipv6 flag to opt-in to the new behavior."
3737

3838
type nodeSource struct {
39-
client kubernetes.Interface
40-
annotationFilter string
41-
fqdnTemplate *template.Template
42-
nodeInformer coreinformers.NodeInformer
43-
labelSelector labels.Selector
44-
exposeInternalIPV6 bool
39+
client kubernetes.Interface
40+
annotationFilter string
41+
fqdnTemplate *template.Template
42+
nodeInformer coreinformers.NodeInformer
43+
labelSelector labels.Selector
44+
excludeUnschedulable bool
45+
exposeInternalIPV6 bool
4546
}
4647

4748
// NewNodeSource creates a new nodeSource with the given config.
48-
func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string, labelSelector labels.Selector, exposeInternalIPv6 bool) (Source, error) {
49+
func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string, labelSelector labels.Selector, exposeInternalIPv6 bool, excludeUnschedulable bool) (Source, error) {
4950
tmpl, err := parseTemplate(fqdnTemplate)
5051
if err != nil {
5152
return nil, err
@@ -73,12 +74,13 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat
7374
}
7475

7576
return &nodeSource{
76-
client: kubeClient,
77-
annotationFilter: annotationFilter,
78-
fqdnTemplate: tmpl,
79-
nodeInformer: nodeInformer,
80-
labelSelector: labelSelector,
81-
exposeInternalIPV6: exposeInternalIPv6,
77+
client: kubeClient,
78+
annotationFilter: annotationFilter,
79+
fqdnTemplate: tmpl,
80+
nodeInformer: nodeInformer,
81+
labelSelector: labelSelector,
82+
excludeUnschedulable: excludeUnschedulable,
83+
exposeInternalIPV6: exposeInternalIPv6,
8284
}, nil
8385
}
8486

@@ -106,7 +108,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
106108
continue
107109
}
108110

109-
if node.Spec.Unschedulable {
111+
if node.Spec.Unschedulable && ns.excludeUnschedulable {
110112
log.Debugf("Skipping node %s because it is unschedulable", node.Name)
111113
continue
112114
}

source/node_test.go

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func testNodeSourceNewNodeSource(t *testing.T) {
8383
ti.fqdnTemplate,
8484
labels.Everything(),
8585
true,
86+
true,
8687
)
8788

8889
if ti.expectError {
@@ -99,18 +100,21 @@ func testNodeSourceEndpoints(t *testing.T) {
99100
t.Parallel()
100101

101102
for _, tc := range []struct {
102-
title string
103-
annotationFilter string
104-
labelSelector string
105-
fqdnTemplate string
106-
nodeName string
107-
nodeAddresses []v1.NodeAddress
108-
labels map[string]string
109-
annotations map[string]string
110-
exposeInternalIPv6 bool // default to true for this version. Change later when the next minor version is released.
111-
unschedulable bool // default to false
112-
expected []*endpoint.Endpoint
113-
expectError bool
103+
title string
104+
annotationFilter string
105+
labelSelector string
106+
fqdnTemplate string
107+
nodeName string
108+
nodeAddresses []v1.NodeAddress
109+
labels map[string]string
110+
annotations map[string]string
111+
excludeUnschedulable bool // default to false
112+
exposeInternalIPv6 bool // default to true for this version. Change later when the next minor version is released.
113+
unschedulable bool // default to false
114+
expected []*endpoint.Endpoint
115+
expectError bool
116+
expectedLogs []string
117+
expectedAbsentLogs []string
114118
}{
115119
{
116120
title: "node with short hostname returns one endpoint",
@@ -363,16 +367,40 @@ func testNodeSourceEndpoints(t *testing.T) {
363367
},
364368
},
365369
{
366-
title: "unschedulable node return nothing",
367-
nodeName: "node1",
368-
exposeInternalIPv6: true,
369-
nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
370-
unschedulable: true,
371-
expected: []*endpoint.Endpoint{},
370+
title: "unschedulable node return nothing with excludeUnschedulable=true",
371+
nodeName: "node1",
372+
exposeInternalIPv6: true,
373+
nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
374+
unschedulable: true,
375+
excludeUnschedulable: true,
376+
expected: []*endpoint.Endpoint{},
377+
expectedLogs: []string{
378+
"Skipping node node1 because it is unschedulable",
379+
},
380+
},
381+
{
382+
title: "unschedulable node returns node with excludeUnschedulable=false",
383+
nodeName: "node1",
384+
nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
385+
unschedulable: true,
386+
excludeUnschedulable: false,
387+
expected: []*endpoint.Endpoint{
388+
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
389+
},
390+
expectedAbsentLogs: []string{
391+
"Skipping node node1 because it is unschedulable",
392+
},
372393
},
373394
} {
374395
tc := tc
375396
t.Run(tc.title, func(t *testing.T) {
397+
var buf *bytes.Buffer
398+
if len(tc.expectedLogs) == 0 && len(tc.expectedAbsentLogs) == 0 {
399+
t.Parallel()
400+
} else {
401+
buf = testutils.LogsToBuffer(log.DebugLevel, t)
402+
}
403+
376404
labelSelector := labels.Everything()
377405
if tc.labelSelector != "" {
378406
var err error
@@ -408,6 +436,7 @@ func testNodeSourceEndpoints(t *testing.T) {
408436
tc.fqdnTemplate,
409437
labelSelector,
410438
tc.exposeInternalIPv6,
439+
tc.excludeUnschedulable,
411440
)
412441
require.NoError(t, err)
413442

@@ -420,24 +449,33 @@ func testNodeSourceEndpoints(t *testing.T) {
420449

421450
// Validate returned endpoints against desired endpoints.
422451
validateEndpoints(t, endpoints, tc.expected)
452+
453+
for _, entry := range tc.expectedLogs {
454+
assert.Contains(t, buf.String(), entry)
455+
}
456+
457+
for _, entry := range tc.expectedAbsentLogs {
458+
assert.NotContains(t, buf.String(), entry)
459+
}
423460
})
424461
}
425462
}
426463

427464
func testNodeEndpointsWithIPv6(t *testing.T) {
428465
for _, tc := range []struct {
429-
title string
430-
annotationFilter string
431-
labelSelector string
432-
fqdnTemplate string
433-
nodeName string
434-
nodeAddresses []v1.NodeAddress
435-
labels map[string]string
436-
annotations map[string]string
437-
exposeInternalIPv6 bool // default to true for this version. Change later when the next minor version is released.
438-
unschedulable bool // default to false
439-
expected []*endpoint.Endpoint
440-
expectError bool
466+
title string
467+
annotationFilter string
468+
labelSelector string
469+
fqdnTemplate string
470+
nodeName string
471+
nodeAddresses []v1.NodeAddress
472+
labels map[string]string
473+
annotations map[string]string
474+
excludeUnschedulable bool // defaults to false
475+
exposeInternalIPv6 bool // default to true for this version. Change later when the next minor version is released.
476+
unschedulable bool // default to false
477+
expected []*endpoint.Endpoint
478+
expectError bool
441479
}{
442480
{
443481
title: "node with only internal IPs should return internal IPvs irrespective of exposeInternalIPv6",
@@ -516,6 +554,7 @@ func testNodeEndpointsWithIPv6(t *testing.T) {
516554
tc.fqdnTemplate,
517555
labelSelector,
518556
tc.exposeInternalIPv6,
557+
tc.excludeUnschedulable,
519558
)
520559
require.NoError(t, err)
521560

source/store.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type Config struct {
8282
ResolveLoadBalancerHostname bool
8383
TraefikDisableLegacy bool
8484
TraefikDisableNew bool
85+
ExcludeUnschedulable bool
8586
ExposeInternalIPv6 bool
8687
}
8788

@@ -126,6 +127,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config {
126127
ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname,
127128
TraefikDisableLegacy: cfg.TraefikDisableLegacy,
128129
TraefikDisableNew: cfg.TraefikDisableNew,
130+
ExcludeUnschedulable: cfg.ExcludeUnschedulable,
129131
ExposeInternalIPv6: cfg.ExposeInternalIPV6,
130132
}
131133
}
@@ -264,7 +266,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
264266
if err != nil {
265267
return nil, err
266268
}
267-
return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6)
269+
return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable)
268270
case "service":
269271
client, err := p.KubeClient()
270272
if err != nil {

0 commit comments

Comments
 (0)