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

kubernetes-sigs / external-dns / 17796151056

17 Sep 2025 11:29AM UTC coverage: 78.577% (+0.5%) from 78.06%
17796151056

Pull #5798

github

TobyTheHutt
fix(controller): platform-agnostic termination sig

Signed-off-by: Tobias Harnickell <tobias.harnickell@bedag.ch>
Pull Request #5798: refactor(controller): signal handling and shutdown

33 of 33 new or added lines in 1 file covered. (100.0%)

45 existing lines in 5 files now uncovered.

15765 of 20063 relevant lines covered (78.58%)

753.9 hits per line

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

66.75
/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
        "runtime"
26
        "syscall"
27
        "time"
28

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

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

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

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

86
func terminationSignals() []os.Signal {
13✔
87
        signals := []os.Signal{os.Interrupt}
13✔
88
        if runtime.GOOS != "windows" {
26✔
89
                signals = append(signals, syscall.SIGTERM)
13✔
90
        }
13✔
91
        return signals
13✔
92
}
93

94
func Execute() {
12✔
95
        cfg := externaldns.NewConfig()
12✔
96
        if err := cfg.ParseFlags(os.Args[1:]); err != nil {
15✔
97
                log.Fatalf("flag parsing error: %v", err)
3✔
98
        }
3✔
99
        log.Infof("config: %s", cfg)
9✔
100
        if err := validation.ValidateConfig(cfg); err != nil {
10✔
101
                log.Fatalf("config validation failed: %v", err)
1✔
102
        }
1✔
103

104
        configureLogger(cfg)
8✔
105

8✔
106
        if cfg.DryRun {
10✔
107
                log.Info("running in dry-run mode. No changes to DNS records will be made.")
2✔
108
        }
2✔
109

110
        if log.GetLevel() < log.DebugLevel {
16✔
111
                // Klog V2 is used by k8s.io/apimachinery/pkg/labels and can throw (a lot) of irrelevant logs
8✔
112
                // See https://github.com/kubernetes-sigs/external-dns/issues/2348
8✔
113
                defer klog.ClearLogger()
8✔
114
                klog.SetLogger(logr.Discard())
8✔
115
        }
8✔
116

117
        log.Info(externaldns.Banner())
8✔
118

8✔
119
        ctx, cancel := context.WithCancel(context.Background())
8✔
120

8✔
121
        // Connect global SIGTERM capture to this run's context cancellation.
8✔
122
        go func() {
12✔
123
                <-sigtermSignals
4✔
124
                log.Info("Received termination signal. Terminating...")
4✔
125
                cancel()
4✔
126
        }()
4✔
127

128
        go serveMetrics(ctx, cfg.MetricsAddress)
8✔
129

8✔
130
        endpointsSource, err := buildSource(ctx, cfg)
8✔
131
        if err != nil {
9✔
132
                log.Fatal(err)
1✔
133
        }
1✔
134

135
        domainFilter := createDomainFilter(cfg)
7✔
136

7✔
137
        prvdr, err := buildProvider(ctx, cfg, domainFilter)
7✔
138
        if err != nil {
8✔
139
                log.Fatal(err)
1✔
140
        }
1✔
141

142
        if cfg.WebhookServer {
7✔
143
                webhookapi.StartHTTPApi(prvdr, nil, cfg.WebhookProviderReadTimeout, cfg.WebhookProviderWriteTimeout, "127.0.0.1:8888")
1✔
144
                os.Exit(0)
1✔
145
        }
1✔
146

147
        ctrl, err := buildController(ctx, cfg, endpointsSource, prvdr, domainFilter)
5✔
148
        if err != nil {
6✔
149
                log.Fatal(err)
1✔
150
        }
1✔
151

152
        if cfg.Once {
6✔
153
                err := ctrl.RunOnce(ctx)
2✔
154
                if err != nil {
3✔
155
                        log.Fatal(err)
1✔
156
                }
1✔
157

158
                os.Exit(0)
1✔
159
        }
160

161
        if cfg.UpdateEvents {
3✔
162
                // Add RunOnce as the handler function that will be called when ingress/service sources have changed.
1✔
163
                // Note that k8s Informers will perform an initial list operation, which results in the handler
1✔
164
                // function initially being called for every Service/Ingress that exists
1✔
165
                ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
1✔
166
        }
167

168
        ctrl.ScheduleRunOnce(time.Now())
2✔
169
        ctrl.Run(ctx)
2✔
170
}
171

172
func buildProvider(
173
        ctx context.Context,
174
        cfg *externaldns.Config,
175
        domainFilter *endpoint.DomainFilter,
176
) (provider.Provider, error) {
17✔
177
        var p provider.Provider
17✔
178
        var err error
17✔
179

17✔
180
        zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
17✔
181
        zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
17✔
182
        zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
17✔
183
        zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
17✔
184

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

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

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

401
        return &Controller{
5✔
402
                Source:               src,
5✔
403
                Registry:             reg,
5✔
404
                Policy:               policy,
5✔
405
                Interval:             cfg.Interval,
5✔
406
                DomainFilter:         filter,
5✔
407
                ManagedRecordTypes:   cfg.ManagedDNSRecordTypes,
5✔
408
                ExcludeRecordTypes:   cfg.ExcludeDNSRecordTypes,
5✔
409
                MinEventSyncInterval: cfg.MinEventSyncInterval,
5✔
410
                EventEmitter:         eventEmitter,
5✔
411
        }, nil
5✔
412
}
413

414
// This function configures the logger format and level based on the provided configuration.
415
func configureLogger(cfg *externaldns.Config) {
11✔
416
        if cfg.LogFormat == "json" {
12✔
417
                log.SetFormatter(&log.JSONFormatter{})
1✔
418
        }
1✔
419
        ll, err := log.ParseLevel(cfg.LogLevel)
11✔
420
        if err != nil {
12✔
421
                log.Fatalf("failed to parse log level: %v", err)
1✔
422
        }
1✔
423
        log.SetLevel(ll)
11✔
424
}
425

426
// selectRegistry selects the appropriate registry implementation based on the configuration in cfg.
427
// It initializes and returns a registry along with any error encountered during setup.
428
// Supported registry types include: dynamodb, noop, txt, and aws-sd.
429
func selectRegistry(cfg *externaldns.Config, p provider.Provider) (registry.Registry, error) {
11✔
430
        var r registry.Registry
11✔
431
        var err error
11✔
432
        switch cfg.Registry {
11✔
433
        case "dynamodb":
2✔
434
                var dynamodbOpts []func(*dynamodb.Options)
2✔
435
                if cfg.AWSDynamoDBRegion != "" {
3✔
436
                        dynamodbOpts = []func(*dynamodb.Options){
1✔
437
                                func(opts *dynamodb.Options) {
2✔
438
                                        opts.Region = cfg.AWSDynamoDBRegion
1✔
439
                                },
1✔
440
                        }
441
                }
442
                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✔
443
        case "noop":
1✔
444
                r, err = registry.NewNoopRegistry(p)
1✔
445
        case "txt":
6✔
446
                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✔
447
        case "aws-sd":
1✔
448
                r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
1✔
449
        default:
1✔
450
                log.Fatalf("unknown registry: %s", cfg.Registry)
1✔
451
        }
452
        return r, err
11✔
453
}
454

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

1✔
487
// RegexDomainFilter overrides DomainFilter
1✔
488
func createDomainFilter(cfg *externaldns.Config) *endpoint.DomainFilter {
1✔
489
        if cfg.RegexDomainFilter != nil && cfg.RegexDomainFilter.String() != "" {
13✔
490
                return endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
13✔
491
        } else {
492
                return endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
493
        }
494
}
13✔
495

15✔
496
// handleSigterm listens for termination signals and triggers the provided cancel function
2✔
497
// to gracefully terminate the application. It logs a message when the signal is received.
13✔
498
func handleSigterm(cancel func()) {
11✔
499
        signals := make(chan os.Signal, 1)
11✔
500
        signal.Notify(signals, terminationSignals()...)
501
        <-signals
502
        log.Info("Received termination signal. Terminating...")
503
        cancel()
504
        signal.Stop(signals)
1✔
505
}
1✔
506

1✔
507
// serveMetrics starts an HTTP server that serves health and metrics endpoints.
1✔
508
// The /healthz endpoint returns a 200 OK status to indicate the service is healthy.
1✔
509
// The /metrics endpoint serves Prometheus metrics.
1✔
510
// The server listens on the specified address and logs debug information about the endpoints.
1✔
511
func serveMetrics(ctx context.Context, address string) {
1✔
512
        mux := http.NewServeMux()
513

514
        mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
515
                w.WriteHeader(http.StatusOK)
516
                _, _ = w.Write([]byte("OK"))
517
        })
5✔
518

5✔
519
        log.Debugf("serving 'healthz' on '%s/healthz'", address)
5✔
520
        log.Debugf("serving 'metrics' on '%s/metrics'", address)
6✔
521
        log.Debugf("registered '%d' metrics", len(metrics.RegisterMetric.Metrics))
1✔
522

1✔
523
        mux.Handle("/metrics", promhttp.Handler())
1✔
524

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

5✔
527
        // Shutdown server on context cancellation
5✔
528
        go func() {
5✔
529
                <-ctx.Done()
5✔
530
                shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
5✔
531
                _ = srv.Shutdown(shutdownCtx)
5✔
532
                cancel()
5✔
533
        }()
5✔
534

10✔
535
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
5✔
536
                log.Fatal(err)
5✔
537
        }
5✔
538
}
5✔
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