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

kubernetes-sigs / external-dns / 13242993566

10 Feb 2025 02:20PM UTC coverage: 71.363% (+0.03%) from 71.329%
13242993566

Pull #4928

github

Dadeos-Menlo
Address markdown linting issues.
Pull Request #4928: feat(Google CloudDNS): add routing policy support

278 of 352 new or added lines in 5 files covered. (78.98%)

8 existing lines in 2 files now uncovered.

14204 of 19904 relevant lines covered (71.36%)

662.19 hits per line

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

84.23
/source/source.go
1
/*
2
Copyright 2017 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 source
18

19
import (
20
        "bytes"
21
        "context"
22
        "fmt"
23
        "math"
24
        "net/netip"
25
        "reflect"
26
        "strconv"
27
        "strings"
28
        "text/template"
29
        "time"
30
        "unicode"
31

32
        log "github.com/sirupsen/logrus"
33
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34
        "k8s.io/apimachinery/pkg/labels"
35
        "k8s.io/apimachinery/pkg/runtime"
36
        "k8s.io/apimachinery/pkg/runtime/schema"
37

38
        "sigs.k8s.io/external-dns/endpoint"
39
)
40

41
const (
42
        // The annotation used for figuring out which controller is responsible
43
        controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
44
        // The annotation used for defining the desired hostname
45
        hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
46
        // The annotation used for specifying whether the public or private interface address is used
47
        accessAnnotationKey = "external-dns.alpha.kubernetes.io/access"
48
        // The annotation used for specifying the type of endpoints to use for headless services
49
        endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type"
50
        // The annotation used for defining the desired ingress/service target
51
        targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
52
        // The annotation used for defining the desired DNS record TTL
53
        ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl"
54
        // The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME
55
        aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
56
        // The annotation used to determine the source of hostnames for ingresses.  This is an optional field - all
57
        // available hostname sources are used if not specified.
58
        ingressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
59
        // The value of the controller annotation so that we feel responsible
60
        controllerAnnotationValue = "dns-controller"
61
        // The annotation used for defining the desired hostname
62
        internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
63
)
64

65
const (
66
        EndpointsTypeNodeExternalIP = "NodeExternalIP"
67
        EndpointsTypeHostIP         = "HostIP"
68
)
69

70
// Provider-specific annotations
71
const (
72
        // The annotation used for determining if traffic will go through Cloudflare
73
        CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
74

75
        SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
76
)
77

78
const (
79
        ttlMinimum = 1
80
        ttlMaximum = math.MaxInt32
81
)
82

83
// Source defines the interface Endpoint sources should implement.
84
type Source interface {
85
        Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
86
        // AddEventHandler adds an event handler that should be triggered if something in source changes
87
        AddEventHandler(context.Context, func())
88
}
89

90
func getTTLFromAnnotations(annotations map[string]string, resource string) endpoint.TTL {
486✔
91
        ttlNotConfigured := endpoint.TTL(0)
486✔
92
        ttlAnnotation, exists := annotations[ttlAnnotationKey]
486✔
93
        if !exists {
933✔
94
                return ttlNotConfigured
447✔
95
        }
447✔
96
        ttlValue, err := parseTTL(ttlAnnotation)
39✔
97
        if err != nil {
44✔
98
                log.Warnf("%s: \"%v\" is not a valid TTL value: %v", resource, ttlAnnotation, err)
5✔
99
                return ttlNotConfigured
5✔
100
        }
5✔
101
        if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
37✔
102
                log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
3✔
103
                return ttlNotConfigured
3✔
104
        }
3✔
105
        return endpoint.TTL(ttlValue)
31✔
106
}
107

108
// parseTTL parses TTL from string, returning duration in seconds.
109
// parseTTL supports both integers like "600" and durations based
110
// on Go Duration like "10m", hence "600" and "10m" represent the same value.
111
//
112
// Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second
113
// for the example).
114
func parseTTL(s string) (ttlSeconds int64, err error) {
39✔
115
        ttlDuration, errDuration := time.ParseDuration(s)
39✔
116
        if errDuration != nil {
71✔
117
                ttlInt, err := strconv.ParseInt(s, 10, 64)
32✔
118
                if err != nil {
37✔
119
                        return 0, errDuration
5✔
120
                }
5✔
121
                return ttlInt, nil
27✔
122
        }
123

124
        return int64(ttlDuration.Seconds()), nil
7✔
125
}
126

127
type kubeObject interface {
128
        runtime.Object
129
        metav1.Object
130
}
131

132
func execTemplate(tmpl *template.Template, obj kubeObject) (hostnames []string, err error) {
56✔
133
        var buf bytes.Buffer
56✔
134
        if err := tmpl.Execute(&buf, obj); err != nil {
57✔
135
                kind := obj.GetObjectKind().GroupVersionKind().Kind
1✔
136
                return nil, fmt.Errorf("failed to apply template on %s %s/%s: %w", kind, obj.GetNamespace(), obj.GetName(), err)
1✔
137
        }
1✔
138
        for _, name := range strings.Split(buf.String(), ",") {
128✔
139
                name = strings.TrimFunc(name, unicode.IsSpace)
73✔
140
                name = strings.TrimSuffix(name, ".")
73✔
141
                hostnames = append(hostnames, name)
73✔
142
        }
73✔
143
        return hostnames, nil
55✔
144
}
145

146
func parseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) {
396✔
147
        if fqdnTemplate == "" {
658✔
148
                return nil, nil
262✔
149
        }
262✔
150
        funcs := template.FuncMap{
134✔
151
                "trimPrefix": strings.TrimPrefix,
134✔
152
        }
134✔
153
        return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate)
134✔
154
}
155

156
func getHostnamesFromAnnotations(annotations map[string]string) []string {
393✔
157
        hostnameAnnotation, exists := annotations[hostnameAnnotationKey]
393✔
158
        if !exists {
655✔
159
                return nil
262✔
160
        }
262✔
161
        return splitHostnameAnnotation(hostnameAnnotation)
131✔
162
}
163

164
func getAccessFromAnnotations(annotations map[string]string) string {
9✔
165
        return annotations[accessAnnotationKey]
9✔
166
}
9✔
167

168
func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
25✔
169
        return annotations[endpointsTypeAnnotationKey]
25✔
170
}
25✔
171

172
func getInternalHostnamesFromAnnotations(annotations map[string]string) []string {
112✔
173
        internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey]
112✔
174
        if !exists {
221✔
175
                return nil
109✔
176
        }
109✔
177
        return splitHostnameAnnotation(internalHostnameAnnotation)
3✔
178
}
179

180
func splitHostnameAnnotation(annotation string) []string {
161✔
181
        return strings.Split(strings.Replace(annotation, " ", "", -1), ",")
161✔
182
}
161✔
183

184
func getAliasFromAnnotations(annotations map[string]string) bool {
459✔
185
        aliasAnnotation, exists := annotations[aliasAnnotationKey]
459✔
186
        return exists && aliasAnnotation == "true"
459✔
187
}
459✔
188

189
func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
459✔
190
        providerSpecificAnnotations := endpoint.ProviderSpecific{}
459✔
191

459✔
192
        v, exists := annotations[CloudflareProxiedKey]
459✔
193
        if exists {
460✔
194
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
1✔
195
                        Name:  CloudflareProxiedKey,
1✔
196
                        Value: v,
1✔
197
                })
1✔
198
        }
1✔
199
        if getAliasFromAnnotations(annotations) {
461✔
200
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
2✔
201
                        Name:  "alias",
2✔
202
                        Value: "true",
2✔
203
                })
2✔
204
        }
2✔
205
        setIdentifier := ""
459✔
206
        for k, v := range annotations {
861✔
207
                if k == SetIdentifierKey {
409✔
208
                        setIdentifier = v
7✔
209
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
406✔
210
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
4✔
211
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
4✔
212
                                Name:  fmt.Sprintf("aws/%s", attr),
4✔
213
                                Value: v,
4✔
214
                        })
4✔
215
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/google-") {
395✔
NEW
216
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/")
×
NEW
217
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
×
NEW
218
                                Name:  attr,
×
NEW
219
                                Value: v,
×
NEW
220
                        })
×
221
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") {
391✔
222
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-")
×
223
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
×
224
                                Name:  fmt.Sprintf("scw/%s", attr),
×
225
                                Value: v,
×
226
                        })
×
227
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") {
391✔
228
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-")
×
229
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
×
230
                                Name:  fmt.Sprintf("ibmcloud-%s", attr),
×
231
                                Value: v,
×
232
                        })
×
233
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/webhook-") {
391✔
234
                        // Support for wildcard annotations for webhook providers
×
235
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/webhook-")
×
236
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
×
237
                                Name:  fmt.Sprintf("webhook/%s", attr),
×
238
                                Value: v,
×
239
                        })
×
240
                }
×
241
        }
242
        return providerSpecificAnnotations, setIdentifier
459✔
243
}
244

245
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
246
// Returns empty endpoints array if none are found.
247
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
654✔
248
        var targets endpoint.Targets
654✔
249

654✔
250
        // Get the desired hostname of the ingress from the annotation.
654✔
251
        targetAnnotation, exists := annotations[targetAnnotationKey]
654✔
252
        if exists && targetAnnotation != "" {
766✔
253
                // splits the hostname annotation and removes the trailing periods
112✔
254
                targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
112✔
255
                for _, targetHostname := range targetsList {
227✔
256
                        targetHostname = strings.TrimSuffix(targetHostname, ".")
115✔
257
                        targets = append(targets, targetHostname)
115✔
258
                }
115✔
259
        }
260
        return targets
654✔
261
}
262

263
// suitableType returns the DNS resource record type suitable for the target.
264
// In this case type A/AAAA for IPs and type CNAME for everything else.
265
func suitableType(target string) string {
784✔
266
        netIP, err := netip.ParseAddr(target)
784✔
267
        if err == nil && netIP.Is4() {
1,229✔
268
                return endpoint.RecordTypeA
445✔
269
        } else if err == nil && netIP.Is6() {
880✔
270
                return endpoint.RecordTypeAAAA
96✔
271
        }
96✔
272
        return endpoint.RecordTypeCNAME
243✔
273
}
274

275
// endpointsForHostname returns the endpoint objects for each host-target combination.
276
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
491✔
277
        var endpoints []*endpoint.Endpoint
491✔
278

491✔
279
        var aTargets endpoint.Targets
491✔
280
        var aaaaTargets endpoint.Targets
491✔
281
        var cnameTargets endpoint.Targets
491✔
282

491✔
283
        for _, t := range targets {
1,078✔
284
                switch suitableType(t) {
587✔
285
                case endpoint.RecordTypeA:
318✔
286
                        aTargets = append(aTargets, t)
318✔
287
                case endpoint.RecordTypeAAAA:
28✔
288
                        aaaaTargets = append(aaaaTargets, t)
28✔
289
                default:
241✔
290
                        cnameTargets = append(cnameTargets, t)
241✔
291
                }
292
        }
293

294
        if len(aTargets) > 0 {
760✔
295
                epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
269✔
296
                if epA != nil {
537✔
297
                        epA.ProviderSpecific = providerSpecific
268✔
298
                        epA.SetIdentifier = setIdentifier
268✔
299
                        if resource != "" {
526✔
300
                                epA.Labels[endpoint.ResourceLabelKey] = resource
258✔
301
                        }
258✔
302
                        endpoints = append(endpoints, epA)
268✔
303
                }
304
        }
305

306
        if len(aaaaTargets) > 0 {
508✔
307
                epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
17✔
308
                if epAAAA != nil {
34✔
309
                        epAAAA.ProviderSpecific = providerSpecific
17✔
310
                        epAAAA.SetIdentifier = setIdentifier
17✔
311
                        if resource != "" {
32✔
312
                                epAAAA.Labels[endpoint.ResourceLabelKey] = resource
15✔
313
                        }
15✔
314
                        endpoints = append(endpoints, epAAAA)
17✔
315
                }
316
        }
317

318
        if len(cnameTargets) > 0 {
713✔
319
                epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
222✔
320
                if epCNAME != nil {
444✔
321
                        epCNAME.ProviderSpecific = providerSpecific
222✔
322
                        epCNAME.SetIdentifier = setIdentifier
222✔
323
                        if resource != "" {
440✔
324
                                epCNAME.Labels[endpoint.ResourceLabelKey] = resource
218✔
325
                        }
218✔
326
                        endpoints = append(endpoints, epCNAME)
222✔
327
                }
328
        }
329

330
        return endpoints
491✔
331
}
332

333
func getLabelSelector(annotationFilter string) (labels.Selector, error) {
116✔
334
        labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
116✔
335
        if err != nil {
117✔
336
                return nil, err
1✔
337
        }
1✔
338
        return metav1.LabelSelectorAsSelector(labelSelector)
115✔
339
}
340

341
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
18✔
342
        annotations := labels.Set(srcAnnotations)
18✔
343
        return selector.Matches(annotations)
18✔
344
}
18✔
345

346
type eventHandlerFunc func()
347

348
func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() }
×
349
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{})         { fn() }
×
350
func (fn eventHandlerFunc) OnDelete(obj interface{})                    { fn() }
×
351

352
type informerFactory interface {
353
        WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
354
}
355

356
func waitForCacheSync(ctx context.Context, factory informerFactory) error {
491✔
357
        ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
491✔
358
        defer cancel()
491✔
359
        for typ, done := range factory.WaitForCacheSync(ctx.Done()) {
1,393✔
360
                if !done {
902✔
361
                        select {
×
362
                        case <-ctx.Done():
×
363
                                return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err())
×
364
                        default:
×
365
                                return fmt.Errorf("failed to sync %v", typ)
×
366
                        }
367
                }
368
        }
369
        return nil
491✔
370
}
371

372
type dynamicInformerFactory interface {
373
        WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
374
}
375

376
func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error {
115✔
377
        ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
115✔
378
        defer cancel()
115✔
379
        for typ, done := range factory.WaitForCacheSync(ctx.Done()) {
409✔
380
                if !done {
294✔
381
                        select {
×
382
                        case <-ctx.Done():
×
383
                                return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err())
×
384
                        default:
×
385
                                return fmt.Errorf("failed to sync %v", typ)
×
386
                        }
387
                }
388
        }
389
        return nil
115✔
390
}
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