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

kubernetes-sigs / external-dns / 15805587787

22 Jun 2025 10:22AM UTC coverage: 76.985% (+0.3%) from 76.647%
15805587787

push

github

web-flow
improve cloudflare regional hostname implementation (#5309)

- add flag to enable regional hostname feature
- support deletion of regional hostname on annotation edit
- correctly support differences detection with cloudflare state
- increased tests coverage

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

152 of 159 new or added lines in 4 files covered. (95.6%)

1 existing line in 1 file now uncovered.

14447 of 18766 relevant lines covered (76.98%)

757.13 hits per line

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

42.27
/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/metrics"
40
        "sigs.k8s.io/external-dns/plan"
41
        "sigs.k8s.io/external-dns/provider"
42
        "sigs.k8s.io/external-dns/provider/akamai"
43
        "sigs.k8s.io/external-dns/provider/alibabacloud"
44
        "sigs.k8s.io/external-dns/provider/aws"
45
        "sigs.k8s.io/external-dns/provider/awssd"
46
        "sigs.k8s.io/external-dns/provider/azure"
47
        "sigs.k8s.io/external-dns/provider/civo"
48
        "sigs.k8s.io/external-dns/provider/cloudflare"
49
        "sigs.k8s.io/external-dns/provider/coredns"
50
        "sigs.k8s.io/external-dns/provider/digitalocean"
51
        "sigs.k8s.io/external-dns/provider/dnsimple"
52
        "sigs.k8s.io/external-dns/provider/exoscale"
53
        "sigs.k8s.io/external-dns/provider/gandi"
54
        "sigs.k8s.io/external-dns/provider/godaddy"
55
        "sigs.k8s.io/external-dns/provider/google"
56
        "sigs.k8s.io/external-dns/provider/inmemory"
57
        "sigs.k8s.io/external-dns/provider/linode"
58
        "sigs.k8s.io/external-dns/provider/ns1"
59
        "sigs.k8s.io/external-dns/provider/oci"
60
        "sigs.k8s.io/external-dns/provider/ovh"
61
        "sigs.k8s.io/external-dns/provider/pdns"
62
        "sigs.k8s.io/external-dns/provider/pihole"
63
        "sigs.k8s.io/external-dns/provider/plural"
64
        "sigs.k8s.io/external-dns/provider/rfc2136"
65
        "sigs.k8s.io/external-dns/provider/scaleway"
66
        "sigs.k8s.io/external-dns/provider/transip"
67
        "sigs.k8s.io/external-dns/provider/webhook"
68
        webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
69
        "sigs.k8s.io/external-dns/registry"
70
        "sigs.k8s.io/external-dns/source"
71
)
72

73
func Execute() {
×
74
        cfg := externaldns.NewConfig()
×
75
        if err := cfg.ParseFlags(os.Args[1:]); err != nil {
×
76
                log.Fatalf("flag parsing error: %v", err)
×
77
        }
×
78
        log.Infof("config: %s", cfg)
×
79
        if err := validation.ValidateConfig(cfg); err != nil {
×
80
                log.Fatalf("config validation failed: %v", err)
×
81
        }
×
82

83
        configureLogger(cfg)
×
84

×
85
        if cfg.DryRun {
×
86
                log.Info("running in dry-run mode. No changes to DNS records will be made.")
×
87
        }
×
88

89
        if log.GetLevel() < log.DebugLevel {
×
90
                // Klog V2 is used by k8s.io/apimachinery/pkg/labels and can throw (a lot) of irrelevant logs
×
91
                // See https://github.com/kubernetes-sigs/external-dns/issues/2348
×
92
                defer klog.ClearLogger()
×
93
                klog.SetLogger(logr.Discard())
×
94
        }
×
95

96
        log.Info(externaldns.Banner())
×
97

×
98
        ctx, cancel := context.WithCancel(context.Background())
×
99

×
100
        go serveMetrics(cfg.MetricsAddress)
×
101
        go handleSigterm(cancel)
×
102

×
103
        endpointsSource, err := buildSource(ctx, cfg)
×
104
        if err != nil {
×
105
                log.Fatal(err)
×
106
        }
×
107

108
        domainFilter := createDomainFilter(cfg)
×
109

×
110
        prvdr, err := buildProvider(ctx, cfg, domainFilter)
×
111
        if err != nil {
×
112
                log.Fatal(err)
×
113
        }
×
114

115
        if cfg.WebhookServer {
×
116
                webhookapi.StartHTTPApi(prvdr, nil, cfg.WebhookProviderReadTimeout, cfg.WebhookProviderWriteTimeout, "127.0.0.1:8888")
×
117
                os.Exit(0)
×
118
        }
×
119

120
        ctrl, err := buildController(cfg, endpointsSource, prvdr, domainFilter)
×
121
        if err != nil {
×
122
                log.Fatal(err)
×
123
        }
×
124

125
        if cfg.Once {
×
126
                err := ctrl.RunOnce(ctx)
×
127
                if err != nil {
×
128
                        log.Fatal(err)
×
129
                }
×
130

131
                os.Exit(0)
×
132
        }
133

134
        if cfg.UpdateEvents {
×
135
                // Add RunOnce as the handler function that will be called when ingress/service sources have changed.
×
136
                // Note that k8s Informers will perform an initial list operation, which results in the handler
×
137
                // function initially being called for every Service/Ingress that exists
×
138
                ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
×
139
        }
140

141
        ctrl.ScheduleRunOnce(time.Now())
×
142
        ctrl.Run(ctx)
×
143
}
144

145
func buildProvider(
146
        ctx context.Context,
147
        cfg *externaldns.Config,
148
        domainFilter *endpoint.DomainFilter,
149
) (provider.Provider, error) {
9✔
150
        var p provider.Provider
9✔
151
        var err error
9✔
152

9✔
153
        zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
9✔
154
        zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
9✔
155
        zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
9✔
156
        zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
9✔
157

9✔
158
        switch cfg.Provider {
9✔
159
        case "akamai":
×
160
                p, err = akamai.NewAkamaiProvider(
×
161
                        akamai.AkamaiConfig{
×
162
                                DomainFilter:          domainFilter,
×
163
                                ZoneIDFilter:          zoneIDFilter,
×
164
                                ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
×
165
                                ClientToken:           cfg.AkamaiClientToken,
×
166
                                ClientSecret:          cfg.AkamaiClientSecret,
×
167
                                AccessToken:           cfg.AkamaiAccessToken,
×
168
                                EdgercPath:            cfg.AkamaiEdgercPath,
×
169
                                EdgercSection:         cfg.AkamaiEdgercSection,
×
170
                                DryRun:                cfg.DryRun,
×
171
                        }, nil)
×
172
        case "alibabacloud":
×
173
                p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
×
174
        case "aws":
1✔
175
                configs := aws.CreateV2Configs(cfg)
1✔
176
                clients := make(map[string]aws.Route53API, len(configs))
1✔
177
                for profile, config := range configs {
2✔
178
                        clients[profile] = route53.NewFromConfig(config)
1✔
179
                }
1✔
180

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

345
func buildController(cfg *externaldns.Config, src source.Source, p provider.Provider, filter *endpoint.DomainFilter) (*Controller, error) {
×
346
        policy, ok := plan.Policies[cfg.Policy]
×
347
        if !ok {
×
348
                return nil, fmt.Errorf("unknown policy: %s", cfg.Policy)
×
349
        }
×
350
        reg, err := selectRegistry(cfg, p)
×
351
        if err != nil {
×
352
                return nil, err
×
353
        }
×
354
        return &Controller{
×
355
                Source:               src,
×
356
                Registry:             reg,
×
357
                Policy:               policy,
×
358
                Interval:             cfg.Interval,
×
359
                DomainFilter:         filter,
×
360
                ManagedRecordTypes:   cfg.ManagedDNSRecordTypes,
×
361
                ExcludeRecordTypes:   cfg.ExcludeDNSRecordTypes,
×
362
                MinEventSyncInterval: cfg.MinEventSyncInterval,
×
363
        }, nil
×
364
}
365

366
// This function configures the logger format and level based on the provided configuration.
367
func configureLogger(cfg *externaldns.Config) {
3✔
368
        if cfg.LogFormat == "json" {
4✔
369
                log.SetFormatter(&log.JSONFormatter{})
1✔
370
        }
1✔
371
        ll, err := log.ParseLevel(cfg.LogLevel)
3✔
372
        if err != nil {
4✔
373
                log.Fatalf("failed to parse log level: %v", err)
1✔
374
        }
1✔
375
        log.SetLevel(ll)
3✔
376
}
377

378
// selectRegistry selects the appropriate registry implementation based on the configuration in cfg.
379
// It initializes and returns a registry along with any error encountered during setup.
380
// Supported registry types include: dynamodb, noop, txt, and aws-sd.
381
func selectRegistry(cfg *externaldns.Config, p provider.Provider) (registry.Registry, error) {
5✔
382
        var r registry.Registry
5✔
383
        var err error
5✔
384
        switch cfg.Registry {
5✔
385
        case "dynamodb":
1✔
386
                var dynamodbOpts []func(*dynamodb.Options)
1✔
387
                if cfg.AWSDynamoDBRegion != "" {
2✔
388
                        dynamodbOpts = []func(*dynamodb.Options){
1✔
389
                                func(opts *dynamodb.Options) {
2✔
390
                                        opts.Region = cfg.AWSDynamoDBRegion
1✔
391
                                },
1✔
392
                        }
393
                }
394
                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)
1✔
395
        case "noop":
1✔
396
                r, err = registry.NewNoopRegistry(p)
1✔
397
        case "txt":
1✔
398
                r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey), cfg.TXTNewFormatOnly)
1✔
399
        case "aws-sd":
1✔
400
                r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
1✔
401
        default:
1✔
402
                log.Fatalf("unknown registry: %s", cfg.Registry)
1✔
403
        }
404
        return r, err
5✔
405
}
406

407
// buildSource creates and configures the source(s) for endpoint discovery based on the provided configuration.
408
// It initializes the source configuration, generates the required sources, and combines them into a single,
409
// deduplicated source. Returns the combined source or an error if source creation fails.
410
func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, error) {
3✔
411
        sourceCfg := source.NewSourceConfig(cfg)
3✔
412
        sources, err := source.ByNames(ctx, &source.SingletonClientGenerator{
3✔
413
                KubeConfig:   cfg.KubeConfig,
3✔
414
                APIServerURL: cfg.APIServerURL,
3✔
415
                RequestTimeout: func() time.Duration {
6✔
416
                        if cfg.UpdateEvents {
4✔
417
                                return 0
1✔
418
                        }
1✔
419
                        return cfg.RequestTimeout
2✔
420
                }(),
421
        }, cfg.Sources, sourceCfg)
422
        if err != nil {
4✔
423
                return nil, err
1✔
424
        }
1✔
425
        // Combine multiple sources into a single, deduplicated source.
426
        combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets))
2✔
427
        // Filter targets
2✔
428
        targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
2✔
429
        combinedSource = source.NewNAT64Source(combinedSource, cfg.NAT64Networks)
2✔
430
        combinedSource = source.NewTargetFilterSource(combinedSource, targetFilter)
2✔
431
        return combinedSource, nil
2✔
432
}
433

434
// RegexDomainFilter overrides DomainFilter
435
func createDomainFilter(cfg *externaldns.Config) *endpoint.DomainFilter {
5✔
436
        if cfg.RegexDomainFilter != nil && cfg.RegexDomainFilter.String() != "" {
7✔
437
                return endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
2✔
438
        } else {
5✔
439
                return endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
3✔
440
        }
3✔
441
}
442

443
// handleSigterm listens for a SIGTERM signal and triggers the provided cancel function
444
// to gracefully terminate the application. It logs a message when the signal is received.
445
func handleSigterm(cancel func()) {
1✔
446
        signals := make(chan os.Signal, 1)
1✔
447
        signal.Notify(signals, syscall.SIGTERM)
1✔
448
        <-signals
1✔
449
        log.Info("Received SIGTERM. Terminating...")
1✔
450
        cancel()
1✔
451
}
1✔
452

453
// serveMetrics starts an HTTP server that serves health and metrics endpoints.
454
// The /healthz endpoint returns a 200 OK status to indicate the service is healthy.
455
// The /metrics endpoint serves Prometheus metrics.
456
// The server listens on the specified address and logs debug information about the endpoints.
457
func serveMetrics(address string) {
1✔
458
        http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
2✔
459
                w.WriteHeader(http.StatusOK)
1✔
460
                _, _ = w.Write([]byte("OK"))
1✔
461
        })
1✔
462

463
        log.Debugf("serving 'healthz' on '%s/healthz'", address)
1✔
464
        log.Debugf("serving 'metrics' on '%s/metrics'", address)
1✔
465
        log.Debugf("registered '%d' metrics", len(metrics.RegisterMetric.Metrics))
1✔
466

1✔
467
        http.Handle("/metrics", promhttp.Handler())
1✔
468

1✔
469
        log.Fatal(http.ListenAndServe(address, nil))
1✔
470
}
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

© 2025 Coveralls, Inc