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

kubernetes-sigs / external-dns / 13565068475

27 Feb 2025 11:13AM UTC coverage: 70.549% (-0.7%) from 71.272%
13565068475

Pull #5081

github

ivankatliarchuk
docs(proposal): ipv6 internal node ip rollback plan

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Pull Request #5081: docs(proposal): ipv6 internal node ip rollback plan

14064 of 19935 relevant lines covered (70.55%)

664.97 hits per line

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

94.12
/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 {
91
        ttlNotConfigured := endpoint.TTL(0)
488✔
92
        ttlAnnotation, exists := annotations[ttlAnnotationKey]
488✔
93
        if !exists {
488✔
94
                return ttlNotConfigured
937✔
95
        }
449✔
96
        ttlValue, err := parseTTL(ttlAnnotation)
449✔
97
        if err != nil {
39✔
98
                log.Warnf("%s: \"%v\" is not a valid TTL value: %v", resource, ttlAnnotation, err)
44✔
99
                return ttlNotConfigured
5✔
100
        }
5✔
101
        if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
5✔
102
                log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
37✔
103
                return ttlNotConfigured
3✔
104
        }
3✔
105
        return endpoint.TTL(ttlValue)
3✔
106
}
31✔
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) {
115
        ttlDuration, errDuration := time.ParseDuration(s)
39✔
116
        if errDuration != nil {
39✔
117
                ttlInt, err := strconv.ParseInt(s, 10, 64)
71✔
118
                if err != nil {
32✔
119
                        return 0, errDuration
37✔
120
                }
5✔
121
                return ttlInt, nil
5✔
122
        }
27✔
123

124
        return int64(ttlDuration.Seconds()), nil
125
}
7✔
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) {
133
        var buf bytes.Buffer
56✔
134
        if err := tmpl.Execute(&buf, obj); err != nil {
56✔
135
                kind := obj.GetObjectKind().GroupVersionKind().Kind
57✔
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(), ",") {
1✔
139
                name = strings.TrimFunc(name, unicode.IsSpace)
128✔
140
                name = strings.TrimSuffix(name, ".")
73✔
141
                hostnames = append(hostnames, name)
73✔
142
        }
73✔
143
        return hostnames, nil
73✔
144
}
55✔
145

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

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

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

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

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

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

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

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

474✔
192
        v, exists := annotations[CloudflareProxiedKey]
474✔
193
        if exists {
478✔
194
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
4✔
195
                        Name:  CloudflareProxiedKey,
4✔
196
                        Value: v,
4✔
197
                })
4✔
198
        }
4✔
199
        if getAliasFromAnnotations(annotations) {
476✔
200
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
2✔
201
                        Name:  "alias",
2✔
202
                        Value: "true",
2✔
203
                })
2✔
204
        }
2✔
205
        setIdentifier := ""
478✔
206
        for k, v := range annotations {
4✔
207
                if k == SetIdentifierKey {
4✔
208
                        setIdentifier = v
4✔
209
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
4✔
210
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
4✔
211
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
474✔
212
                                Name:  fmt.Sprintf("aws/%s", attr),
904✔
213
                                Value: v,
441✔
214
                        })
11✔
215
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") {
436✔
216
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-")
6✔
217
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
6✔
218
                                Name:  fmt.Sprintf("scw/%s", attr),
6✔
219
                                Value: v,
6✔
220
                        })
6✔
221
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") {
421✔
222
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-")
2✔
223
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
2✔
224
                                Name:  fmt.Sprintf("ibmcloud-%s", attr),
2✔
225
                                Value: v,
2✔
226
                        })
2✔
227
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/webhook-") {
415✔
228
                        // Support for wildcard annotations for webhook providers
2✔
229
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/webhook-")
2✔
230
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
2✔
231
                                Name:  fmt.Sprintf("webhook/%s", attr),
2✔
232
                                Value: v,
2✔
233
                        })
413✔
234
                }
2✔
235
        }
2✔
236
        return providerSpecificAnnotations, setIdentifier
2✔
237
}
2✔
238

2✔
239
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
2✔
240
// Returns empty endpoints array if none are found.
2✔
241
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
242
        var targets endpoint.Targets
474✔
243

244
        // Get the desired hostname of the ingress from the annotation.
245
        targetAnnotation, exists := annotations[targetAnnotationKey]
246
        if exists && targetAnnotation != "" {
247
                // splits the hostname annotation and removes the trailing periods
656✔
248
                targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
656✔
249
                for _, targetHostname := range targetsList {
656✔
250
                        targetHostname = strings.TrimSuffix(targetHostname, ".")
656✔
251
                        targets = append(targets, targetHostname)
656✔
252
                }
768✔
253
        }
112✔
254
        return targets
112✔
255
}
227✔
256

115✔
257
// suitableType returns the DNS resource record type suitable for the target.
115✔
258
// In this case type A/AAAA for IPs and type CNAME for everything else.
115✔
259
func suitableType(target string) string {
260
        netIP, err := netip.ParseAddr(target)
656✔
261
        if err == nil && netIP.Is4() {
262
                return endpoint.RecordTypeA
263
        } else if err == nil && netIP.Is6() {
264
                return endpoint.RecordTypeAAAA
265
        }
786✔
266
        return endpoint.RecordTypeCNAME
786✔
267
}
1,233✔
268

447✔
269
// endpointsForHostname returns the endpoint objects for each host-target combination.
882✔
270
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
96✔
271
        var endpoints []*endpoint.Endpoint
96✔
272

243✔
273
        var aTargets endpoint.Targets
274
        var aaaaTargets endpoint.Targets
275
        var cnameTargets endpoint.Targets
276

493✔
277
        for _, t := range targets {
493✔
278
                switch suitableType(t) {
493✔
279
                case endpoint.RecordTypeA:
493✔
280
                        aTargets = append(aTargets, t)
493✔
281
                case endpoint.RecordTypeAAAA:
493✔
282
                        aaaaTargets = append(aaaaTargets, t)
493✔
283
                default:
1,082✔
284
                        cnameTargets = append(cnameTargets, t)
589✔
285
                }
320✔
286
        }
320✔
287

28✔
288
        if len(aTargets) > 0 {
28✔
289
                epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
241✔
290
                if epA != nil {
241✔
291
                        epA.ProviderSpecific = providerSpecific
292
                        epA.SetIdentifier = setIdentifier
293
                        if resource != "" {
294
                                epA.Labels[endpoint.ResourceLabelKey] = resource
764✔
295
                        }
271✔
296
                        endpoints = append(endpoints, epA)
541✔
297
                }
270✔
298
        }
270✔
299

530✔
300
        if len(aaaaTargets) > 0 {
260✔
301
                epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
260✔
302
                if epAAAA != nil {
270✔
303
                        epAAAA.ProviderSpecific = providerSpecific
304
                        epAAAA.SetIdentifier = setIdentifier
305
                        if resource != "" {
306
                                epAAAA.Labels[endpoint.ResourceLabelKey] = resource
510✔
307
                        }
17✔
308
                        endpoints = append(endpoints, epAAAA)
34✔
309
                }
17✔
310
        }
17✔
311

32✔
312
        if len(cnameTargets) > 0 {
15✔
313
                epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
15✔
314
                if epCNAME != nil {
17✔
315
                        epCNAME.ProviderSpecific = providerSpecific
316
                        epCNAME.SetIdentifier = setIdentifier
317
                        if resource != "" {
318
                                epCNAME.Labels[endpoint.ResourceLabelKey] = resource
715✔
319
                        }
222✔
320
                        endpoints = append(endpoints, epCNAME)
444✔
321
                }
222✔
322
        }
222✔
323

440✔
324
        return endpoints
218✔
325
}
218✔
326

222✔
327
func getLabelSelector(annotationFilter string) (labels.Selector, error) {
328
        labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
329
        if err != nil {
330
                return nil, err
493✔
331
        }
332
        return metav1.LabelSelectorAsSelector(labelSelector)
333
}
118✔
334

118✔
335
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
119✔
336
        annotations := labels.Set(srcAnnotations)
1✔
337
        return selector.Matches(annotations)
1✔
338
}
117✔
339

340
type eventHandlerFunc func()
341

18✔
342
func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() }
18✔
343
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{})         { fn() }
18✔
344
func (fn eventHandlerFunc) OnDelete(obj interface{})                    { fn() }
18✔
345

346
type informerFactory interface {
347
        WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
348
}
×
349

×
350
func waitForCacheSync(ctx context.Context, factory informerFactory) error {
×
351
        ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
352
        defer cancel()
353
        for typ, done := range factory.WaitForCacheSync(ctx.Done()) {
354
                if !done {
355
                        select {
356
                        case <-ctx.Done():
491✔
357
                                return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err())
491✔
358
                        default:
491✔
359
                                return fmt.Errorf("failed to sync %v", typ)
1,443✔
360
                        }
952✔
361
                }
×
362
        }
×
363
        return nil
×
364
}
×
365

×
366
type dynamicInformerFactory interface {
367
        WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
368
}
369

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