• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / external-dns / 17513627113

06 Sep 2025 10:55AM UTC coverage: 78.149% (+0.5%) from 77.67%
17513627113

Pull #5798

github

TobyTheHutt
test(controller): Fix controller signal termination (kubernetes-sigs#5150)

Prevent data races and premature process terminations by managing signal
capture and synchronization.

Signed-off-by: Tobias Harnickell <tobias.harnickell@bedag.ch>
Pull Request #5798: test(controller): improve coverage on Execute()

27 of 29 new or added lines in 1 file covered. (93.1%)

37 existing lines in 1 file now uncovered.

15504 of 19839 relevant lines covered (78.15%)

737.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.41
/controller/execute.go
1
/*
2
Copyright 2025 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package controller
18

19
import (
20
        "context"
21
        "fmt"
22
        "net/http"
23
        "os"
24
        "os/signal"
25
        "syscall"
26
        "time"
27

28
        "github.com/aws/aws-sdk-go-v2/service/dynamodb"
29
        "github.com/aws/aws-sdk-go-v2/service/route53"
30
        sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
31
        "github.com/go-logr/logr"
32
        "github.com/prometheus/client_golang/prometheus/promhttp"
33
        log "github.com/sirupsen/logrus"
34
        "k8s.io/klog/v2"
35

36
        "sigs.k8s.io/external-dns/endpoint"
37
        "sigs.k8s.io/external-dns/pkg/apis/externaldns"
38
        "sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
39
        "sigs.k8s.io/external-dns/pkg/events"
40
        "sigs.k8s.io/external-dns/pkg/metrics"
41
        "sigs.k8s.io/external-dns/plan"
42
        "sigs.k8s.io/external-dns/provider"
43
        "sigs.k8s.io/external-dns/provider/akamai"
44
        "sigs.k8s.io/external-dns/provider/alibabacloud"
45
        "sigs.k8s.io/external-dns/provider/aws"
46
        "sigs.k8s.io/external-dns/provider/awssd"
47
        "sigs.k8s.io/external-dns/provider/azure"
48
        "sigs.k8s.io/external-dns/provider/civo"
49
        "sigs.k8s.io/external-dns/provider/cloudflare"
50
        "sigs.k8s.io/external-dns/provider/coredns"
51
        "sigs.k8s.io/external-dns/provider/digitalocean"
52
        "sigs.k8s.io/external-dns/provider/dnsimple"
53
        "sigs.k8s.io/external-dns/provider/exoscale"
54
        "sigs.k8s.io/external-dns/provider/gandi"
55
        "sigs.k8s.io/external-dns/provider/godaddy"
56
        "sigs.k8s.io/external-dns/provider/google"
57
        "sigs.k8s.io/external-dns/provider/inmemory"
58
        "sigs.k8s.io/external-dns/provider/linode"
59
        "sigs.k8s.io/external-dns/provider/ns1"
60
        "sigs.k8s.io/external-dns/provider/oci"
61
        "sigs.k8s.io/external-dns/provider/ovh"
62
        "sigs.k8s.io/external-dns/provider/pdns"
63
        "sigs.k8s.io/external-dns/provider/pihole"
64
        "sigs.k8s.io/external-dns/provider/plural"
65
        "sigs.k8s.io/external-dns/provider/rfc2136"
66
        "sigs.k8s.io/external-dns/provider/scaleway"
67
        "sigs.k8s.io/external-dns/provider/transip"
68
        "sigs.k8s.io/external-dns/provider/webhook"
69
        webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
70
        "sigs.k8s.io/external-dns/registry"
71
        "sigs.k8s.io/external-dns/source"
72
        "sigs.k8s.io/external-dns/source/wrappers"
73
)
74

75
// sigtermSignals is a package-level signal channel that is registered in init().
76
// This way, SIGTERM is captured as soon as the package is loaded, preventing
77
// default process termination, even if application startup is delayed.
78
var sigtermSignals chan os.Signal
79

80
func init() {
10✔
81
        sigtermSignals = make(chan os.Signal, 1)
10✔
82
        signal.Notify(sigtermSignals, syscall.SIGTERM)
10✔
83
}
10✔
84

85
func Execute() {
12✔
86
        cfg := externaldns.NewConfig()
12✔
87
        if err := cfg.ParseFlags(os.Args[1:]); err != nil {
15✔
88
                log.Fatalf("flag parsing error: %v", err)
3✔
89
        }
3✔
90
        log.Infof("config: %s", cfg)
9✔
91
        if err := validation.ValidateConfig(cfg); err != nil {
10✔
92
                log.Fatalf("config validation failed: %v", err)
1✔
93
        }
1✔
94

95
        configureLogger(cfg)
8✔
96

8✔
97
        if cfg.DryRun {
10✔
98
                log.Info("running in dry-run mode. No changes to DNS records will be made.")
2✔
99
        }
2✔
100

101
        if log.GetLevel() < log.DebugLevel {
16✔
102
                // Klog V2 is used by k8s.io/apimachinery/pkg/labels and can throw (a lot) of irrelevant logs
8✔
103
                // See https://github.com/kubernetes-sigs/external-dns/issues/2348
8✔
104
                defer klog.ClearLogger()
8✔
105
                klog.SetLogger(logr.Discard())
8✔
106
        }
8✔
107

108
        log.Info(externaldns.Banner())
8✔
109

8✔
110
        ctx, cancel := context.WithCancel(context.Background())
8✔
111

8✔
112
        // Connect global SIGTERM capture to this run's context cancellation.
8✔
113
        go func() {
12✔
114
                <-sigtermSignals
4✔
115
                log.Info("Received SIGTERM. Terminating...")
4✔
116
                cancel()
4✔
117
        }()
4✔
118

119
        go serveMetrics(ctx, cfg.MetricsAddress)
8✔
120

8✔
121
        endpointsSource, err := buildSource(ctx, cfg)
8✔
122
        if err != nil {
9✔
123
                log.Fatal(err)
1✔
124
        }
1✔
125

126
        domainFilter := createDomainFilter(cfg)
7✔
127

7✔
128
        prvdr, err := buildProvider(ctx, cfg, domainFilter)
7✔
129
        if err != nil {
8✔
130
                log.Fatal(err)
1✔
131
        }
1✔
132

133
        if cfg.WebhookServer {
7✔
134
                webhookapi.StartHTTPApi(prvdr, nil, cfg.WebhookProviderReadTimeout, cfg.WebhookProviderWriteTimeout, "127.0.0.1:8888")
1✔
135
                os.Exit(0)
1✔
136
        }
1✔
137

138
        ctrl, err := buildController(ctx, cfg, endpointsSource, prvdr, domainFilter)
5✔
139
        if err != nil {
6✔
140
                log.Fatal(err)
1✔
141
        }
1✔
142

143
        if cfg.Once {
6✔
144
                err := ctrl.RunOnce(ctx)
2✔
145
                if err != nil {
3✔
146
                        log.Fatal(err)
1✔
147
                }
1✔
148

149
                os.Exit(0)
1✔
150
        }
151

152
        if cfg.UpdateEvents {
3✔
153
                // Add RunOnce as the handler function that will be called when ingress/service sources have changed.
1✔
154
                // Note that k8s Informers will perform an initial list operation, which results in the handler
1✔
155
                // function initially being called for every Service/Ingress that exists
1✔
156
                ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
1✔
157
        }
158

159
        ctrl.ScheduleRunOnce(time.Now())
2✔
160
        ctrl.Run(ctx)
2✔
161
}
162

163
func buildProvider(
164
        ctx context.Context,
165
        cfg *externaldns.Config,
166
        domainFilter *endpoint.DomainFilter,
167
) (provider.Provider, error) {
17✔
168
        var p provider.Provider
17✔
169
        var err error
17✔
170

17✔
171
        zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
17✔
172
        zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
17✔
173
        zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
17✔
174
        zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
17✔
175

17✔
176
        switch cfg.Provider {
17✔
UNCOV
177
        case "akamai":
×
UNCOV
178
                p, err = akamai.NewAkamaiProvider(
×
UNCOV
179
                        akamai.AkamaiConfig{
×
UNCOV
180
                                DomainFilter:          domainFilter,
×
UNCOV
181
                                ZoneIDFilter:          zoneIDFilter,
×
UNCOV
182
                                ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
×
UNCOV
183
                                ClientToken:           cfg.AkamaiClientToken,
×
UNCOV
184
                                ClientSecret:          cfg.AkamaiClientSecret,
×
UNCOV
185
                                AccessToken:           cfg.AkamaiAccessToken,
×
186
                                EdgercPath:            cfg.AkamaiEdgercPath,
×
187
                                EdgercSection:         cfg.AkamaiEdgercSection,
×
188
                                DryRun:                cfg.DryRun,
×
189
                        }, nil)
×
190
        case "alibabacloud":
×
191
                p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
×
192
        case "aws":
1✔
193
                configs := aws.CreateV2Configs(cfg)
1✔
194
                clients := make(map[string]aws.Route53API, len(configs))
1✔
195
                for profile, config := range configs {
2✔
196
                        clients[profile] = route53.NewFromConfig(config)
1✔
197
                }
1✔
198

199
                p, err = aws.NewAWSProvider(
1✔
200
                        aws.AWSConfig{
1✔
201
                                DomainFilter:          domainFilter,
1✔
202
                                ZoneIDFilter:          zoneIDFilter,
1✔
203
                                ZoneTypeFilter:        zoneTypeFilter,
1✔
204
                                ZoneTagFilter:         zoneTagFilter,
1✔
205
                                ZoneMatchParent:       cfg.AWSZoneMatchParent,
1✔
206
                                BatchChangeSize:       cfg.AWSBatchChangeSize,
1✔
207
                                BatchChangeSizeBytes:  cfg.AWSBatchChangeSizeBytes,
1✔
208
                                BatchChangeSizeValues: cfg.AWSBatchChangeSizeValues,
1✔
209
                                BatchChangeInterval:   cfg.AWSBatchChangeInterval,
1✔
210
                                EvaluateTargetHealth:  cfg.AWSEvaluateTargetHealth,
1✔
211
                                PreferCNAME:           cfg.AWSPreferCNAME,
1✔
212
                                DryRun:                cfg.DryRun,
1✔
213
                                ZoneCacheDuration:     cfg.AWSZoneCacheDuration,
1✔
214
                        },
1✔
215
                        clients,
1✔
216
                )
1✔
UNCOV
217
        case "aws-sd":
×
UNCOV
218
                // Check that only compatible Registry is used with AWS-SD
×
UNCOV
219
                if cfg.Registry != "noop" && cfg.Registry != "aws-sd" {
×
UNCOV
220
                        log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
×
UNCOV
221
                        cfg.Registry = "aws-sd"
×
UNCOV
222
                }
×
UNCOV
223
                p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, cfg.AWSSDCreateTag, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
×
224
        case "azure-dns", "azure":
1✔
225
                p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun)
1✔
226
        case "azure-private-dns":
×
227
                p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.AzureMaxRetriesCount, cfg.DryRun)
×
228
        case "civo":
×
229
                p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun)
×
230
        case "cloudflare":
×
231
                p, err = cloudflare.NewCloudFlareProvider(
×
232
                        domainFilter,
×
233
                        zoneIDFilter,
×
234
                        cfg.CloudflareProxied,
×
235
                        cfg.DryRun,
×
236
                        cloudflare.RegionalServicesConfig{
×
237
                                Enabled:   cfg.CloudflareRegionalServices,
×
238
                                RegionKey: cfg.CloudflareRegionKey,
×
239
                        },
×
240
                        cloudflare.CustomHostnamesConfig{
×
241
                                Enabled:              cfg.CloudflareCustomHostnames,
×
242
                                MinTLSVersion:        cfg.CloudflareCustomHostnamesMinTLSVersion,
×
243
                                CertificateAuthority: cfg.CloudflareCustomHostnamesCertificateAuthority,
×
244
                        },
×
245
                        cloudflare.DNSRecordsConfig{
×
246
                                PerPage: cfg.CloudflareDNSRecordsPerPage,
×
247
                                Comment: cfg.CloudflareDNSRecordsComment,
×
248
                        })
×
249
        case "google":
×
250
                p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
×
251
        case "digitalocean":
×
252
                p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
×
253
        case "ovh":
×
254
                p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.OVHEnableCNAMERelative, cfg.DryRun)
×
255
        case "linode":
×
256
                p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun)
×
257
        case "dnsimple":
1✔
258
                p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
1✔
259
        case "coredns", "skydns":
1✔
260
                p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
1✔
261
        case "exoscale":
×
262
                p, err = exoscale.NewExoscaleProvider(
×
263
                        cfg.ExoscaleAPIEnvironment,
×
264
                        cfg.ExoscaleAPIZone,
×
265
                        cfg.ExoscaleAPIKey,
×
UNCOV
266
                        cfg.ExoscaleAPISecret,
×
UNCOV
267
                        cfg.DryRun,
×
UNCOV
268
                        exoscale.ExoscaleWithDomain(domainFilter),
×
UNCOV
269
                        exoscale.ExoscaleWithLogging(),
×
270
                )
×
271
        case "inmemory":
9✔
272
                p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil
9✔
273
        case "pdns":
×
274
                p, err = pdns.NewPDNSProvider(
×
275
                        ctx,
×
276
                        pdns.PDNSConfig{
×
277
                                DomainFilter: domainFilter,
×
278
                                DryRun:       cfg.DryRun,
×
279
                                Server:       cfg.PDNSServer,
×
UNCOV
280
                                ServerID:     cfg.PDNSServerID,
×
UNCOV
281
                                APIKey:       cfg.PDNSAPIKey,
×
282
                                TLSConfig: pdns.TLSConfig{
×
283
                                        SkipTLSVerify:         cfg.PDNSSkipTLSVerify,
×
284
                                        CAFilePath:            cfg.TLSCA,
×
285
                                        ClientCertFilePath:    cfg.TLSClientCert,
×
286
                                        ClientCertKeyFilePath: cfg.TLSClientCertKey,
×
287
                                },
×
288
                        },
×
289
                )
×
290
        case "oci":
×
291
                var config *oci.OCIConfig
×
292
                // if the instance-principals flag was set, and a compartment OCID was provided, then ignore the
×
293
                // OCI config file, and provide a config that uses instance principal authentication.
×
294
                if cfg.OCIAuthInstancePrincipal {
×
295
                        if len(cfg.OCICompartmentOCID) == 0 {
×
296
                                err = fmt.Errorf("instance principal authentication requested, but no compartment OCID provided")
×
297
                        } else {
×
298
                                authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true}
×
299
                                config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID}
×
300
                        }
×
301
                } else {
×
302
                        config, err = oci.LoadOCIConfig(cfg.OCIConfigFile)
×
303
                }
×
304
                config.ZoneCacheDuration = cfg.OCIZoneCacheDuration
×
305
                if err == nil {
×
306
                        p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.OCIZoneScope, cfg.DryRun)
×
307
                }
×
308
        case "rfc2136":
1✔
309
                tlsConfig := rfc2136.TLSConfig{
1✔
310
                        UseTLS:                cfg.RFC2136UseTLS,
1✔
311
                        SkipTLSVerify:         cfg.RFC2136SkipTLSVerify,
1✔
312
                        CAFilePath:            cfg.TLSCA,
1✔
313
                        ClientCertFilePath:    cfg.TLSClientCert,
1✔
314
                        ClientCertKeyFilePath: cfg.TLSClientCertKey,
1✔
315
                }
1✔
316
                p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136CreatePTR, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, tlsConfig, cfg.RFC2136LoadBalancingStrategy, nil)
1✔
UNCOV
317
        case "ns1":
×
UNCOV
318
                p, err = ns1.NewNS1Provider(
×
UNCOV
319
                        ns1.NS1Config{
×
UNCOV
320
                                DomainFilter:  domainFilter,
×
UNCOV
321
                                ZoneIDFilter:  zoneIDFilter,
×
UNCOV
322
                                NS1Endpoint:   cfg.NS1Endpoint,
×
UNCOV
323
                                NS1IgnoreSSL:  cfg.NS1IgnoreSSL,
×
UNCOV
324
                                DryRun:        cfg.DryRun,
×
UNCOV
325
                                MinTTLSeconds: cfg.NS1MinTTLSeconds,
×
326
                        },
×
327
                )
×
328
        case "transip":
×
329
                p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
×
330
        case "scaleway":
×
331
                p, err = scaleway.NewScalewayProvider(ctx, domainFilter, cfg.DryRun)
×
332
        case "godaddy":
×
333
                p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
×
334
        case "gandi":
1✔
335
                p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun)
1✔
336
        case "pihole":
1✔
337
                p, err = pihole.NewPiholeProvider(
1✔
338
                        pihole.PiholeConfig{
1✔
339
                                Server:                cfg.PiholeServer,
1✔
340
                                Password:              cfg.PiholePassword,
1✔
341
                                TLSInsecureSkipVerify: cfg.PiholeTLSInsecureSkipVerify,
1✔
342
                                DomainFilter:          domainFilter,
1✔
343
                                DryRun:                cfg.DryRun,
1✔
344
                                APIVersion:            cfg.PiholeApiVersion,
1✔
345
                        },
1✔
346
                )
1✔
UNCOV
347
        case "plural":
×
UNCOV
348
                p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider)
×
UNCOV
349
        case "webhook":
×
UNCOV
350
                p, err = webhook.NewWebhookProvider(cfg.WebhookProviderURL)
×
351
        default:
1✔
352
                err = fmt.Errorf("unknown dns provider: %s", cfg.Provider)
1✔
353
        }
354
        if p != nil && cfg.ProviderCacheTime > 0 {
18✔
355
                p = provider.NewCachedProvider(
1✔
356
                        p,
1✔
357
                        cfg.ProviderCacheTime,
1✔
358
                )
1✔
359
        }
1✔
360
        return p, err
17✔
361
}
362

363
func buildController(
364
        ctx context.Context,
365
        cfg *externaldns.Config,
366
        src source.Source,
367
        p provider.Provider,
368
        filter *endpoint.DomainFilter,
369
) (*Controller, error) {
6✔
370
        policy, ok := plan.Policies[cfg.Policy]
6✔
371
        if !ok {
6✔
UNCOV
372
                return nil, fmt.Errorf("unknown policy: %s", cfg.Policy)
×
UNCOV
373
        }
×
374
        reg, err := selectRegistry(cfg, p)
6✔
375
        if err != nil {
7✔
376
                return nil, err
1✔
377
        }
1✔
378
        eventsCfg := events.NewConfig(
5✔
379
                events.WithKubeConfig(cfg.KubeConfig, cfg.APIServerURL, cfg.RequestTimeout),
5✔
380
                events.WithEmitEvents(cfg.EmitEvents),
5✔
381
                events.WithDryRun(cfg.DryRun))
5✔
382
        var eventEmitter events.EventEmitter
5✔
383
        if eventsCfg.IsEnabled() {
5✔
384
                eventCtrl, err := events.NewEventController(eventsCfg)
×
385
                if err != nil {
×
386
                        log.Fatal(err)
×
387
                }
×
388
                eventCtrl.Run(ctx)
×
389
                eventEmitter = eventCtrl
×
390
        }
391

392
        return &Controller{
5✔
393
                Source:               src,
5✔
394
                Registry:             reg,
5✔
395
                Policy:               policy,
5✔
396
                Interval:             cfg.Interval,
5✔
397
                DomainFilter:         filter,
5✔
398
                ManagedRecordTypes:   cfg.ManagedDNSRecordTypes,
5✔
399
                ExcludeRecordTypes:   cfg.ExcludeDNSRecordTypes,
5✔
400
                MinEventSyncInterval: cfg.MinEventSyncInterval,
5✔
401
                EventEmitter:         eventEmitter,
5✔
402
        }, nil
5✔
403
}
404

405
// This function configures the logger format and level based on the provided configuration.
406
func configureLogger(cfg *externaldns.Config) {
11✔
407
        if cfg.LogFormat == "json" {
12✔
408
                log.SetFormatter(&log.JSONFormatter{})
1✔
409
        }
1✔
410
        ll, err := log.ParseLevel(cfg.LogLevel)
11✔
411
        if err != nil {
12✔
412
                log.Fatalf("failed to parse log level: %v", err)
1✔
413
        }
1✔
414
        log.SetLevel(ll)
11✔
415
}
416

417
// selectRegistry selects the appropriate registry implementation based on the configuration in cfg.
418
// It initializes and returns a registry along with any error encountered during setup.
419
// Supported registry types include: dynamodb, noop, txt, and aws-sd.
420
func selectRegistry(cfg *externaldns.Config, p provider.Provider) (registry.Registry, error) {
11✔
421
        var r registry.Registry
11✔
422
        var err error
11✔
423
        switch cfg.Registry {
11✔
424
        case "dynamodb":
2✔
425
                var dynamodbOpts []func(*dynamodb.Options)
2✔
426
                if cfg.AWSDynamoDBRegion != "" {
3✔
427
                        dynamodbOpts = []func(*dynamodb.Options){
1✔
428
                                func(opts *dynamodb.Options) {
2✔
429
                                        opts.Region = cfg.AWSDynamoDBRegion
1✔
430
                                },
1✔
431
                        }
432
                }
433
                r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.NewFromConfig(aws.CreateDefaultV2Config(cfg), dynamodbOpts...), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval)
2✔
434
        case "noop":
1✔
435
                r, err = registry.NewNoopRegistry(p)
1✔
436
        case "txt":
6✔
437
                r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey))
6✔
438
        case "aws-sd":
1✔
439
                r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
1✔
440
        default:
1✔
441
                log.Fatalf("unknown registry: %s", cfg.Registry)
1✔
442
        }
443
        return r, err
11✔
444
}
445

446
// buildSource creates and configures the source(s) for endpoint discovery based on the provided configuration.
447
// It initializes the source configuration, generates the required sources, and combines them into a single,
448
// deduplicated source. Returns the combined source or an error if source creation fails.
449
func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, error) {
14✔
450
        sourceCfg := source.NewSourceConfig(cfg)
14✔
451
        sources, err := source.ByNames(ctx, &source.SingletonClientGenerator{
14✔
452
                KubeConfig:   cfg.KubeConfig,
14✔
453
                APIServerURL: cfg.APIServerURL,
14✔
454
                RequestTimeout: func() time.Duration {
28✔
455
                        if cfg.UpdateEvents {
16✔
456
                                return 0
2✔
457
                        }
2✔
458
                        return cfg.RequestTimeout
12✔
459
                }(),
460
        }, cfg.Sources, sourceCfg)
461
        if err != nil {
16✔
462
                return nil, err
2✔
463
        }
2✔
464
        // Combine multiple sources into a single, deduplicated source.
465
        combinedSource := wrappers.NewDedupSource(wrappers.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
12✔
466
        cfg.AddSourceWrapper("dedup")
12✔
467
        combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks)
12✔
468
        cfg.AddSourceWrapper("nat64")
12✔
469
        // Filter targets
12✔
470
        targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
12✔
471
        if targetFilter.IsEnabled() {
13✔
472
                combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter)
1✔
473
                cfg.AddSourceWrapper("target-filter")
1✔
474
        }
1✔
475
        return combinedSource, nil
12✔
476
}
477

478
// RegexDomainFilter overrides DomainFilter
479
func createDomainFilter(cfg *externaldns.Config) *endpoint.DomainFilter {
13✔
480
        if cfg.RegexDomainFilter != nil && cfg.RegexDomainFilter.String() != "" {
15✔
481
                return endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
2✔
482
        } else {
13✔
483
                return endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
11✔
484
        }
11✔
485
}
486

487
// handleSigterm listens for a SIGTERM signal and triggers the provided cancel function
488
// to gracefully terminate the application. It logs a message when the signal is received.
489
func handleSigterm(cancel func()) {
1✔
490
        signals := make(chan os.Signal, 1)
1✔
491
        signal.Notify(signals, syscall.SIGTERM)
1✔
492
        <-signals
1✔
493
        log.Info("Received SIGTERM. Terminating...")
1✔
494
        cancel()
1✔
495
        signal.Stop(signals)
1✔
496
}
1✔
497

498
// serveMetrics starts an HTTP server that serves health and metrics endpoints.
499
// The /healthz endpoint returns a 200 OK status to indicate the service is healthy.
500
// The /metrics endpoint serves Prometheus metrics.
501
// The server listens on the specified address and logs debug information about the endpoints.
502
func serveMetrics(ctx context.Context, address string) {
5✔
503
        mux := http.NewServeMux()
5✔
504

5✔
505
        mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
6✔
506
                w.WriteHeader(http.StatusOK)
1✔
507
                _, _ = w.Write([]byte("OK"))
1✔
508
        })
1✔
509

510
        log.Debugf("serving 'healthz' on '%s/healthz'", address)
5✔
511
        log.Debugf("serving 'metrics' on '%s/metrics'", address)
5✔
512
        log.Debugf("registered '%d' metrics", len(metrics.RegisterMetric.Metrics))
5✔
513

5✔
514
        mux.Handle("/metrics", promhttp.Handler())
5✔
515

5✔
516
        srv := &http.Server{Addr: address, Handler: mux}
5✔
517

5✔
518
        // Shutdown server on context cancellation
5✔
519
        go func() {
10✔
520
                <-ctx.Done()
5✔
521
                shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
5✔
522
                _ = srv.Shutdown(shutdownCtx)
5✔
523
                cancel()
5✔
524
        }()
5✔
525

526
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
5✔
NEW
527
                log.Fatal(err)
×
NEW
528
        }
×
529
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc