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

kubernetes-sigs / external-dns / 15063173426

16 May 2025 07:32AM UTC coverage: 73.121% (+0.8%) from 72.283%
15063173426

Pull #5353

github

ivankatliarchuk
chore(docs): update aws permissions

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Pull Request #5353: chore(docs): update aws role requirements with conditions

14859 of 20321 relevant lines covered (73.12%)

694.09 hits per line

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

80.77
/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"
56✔
67
        EndpointsTypeHostIP         = "HostIP"
56✔
68
)
57✔
69

1✔
70
// Provider-specific annotations
1✔
71
const (
1✔
72
        // The annotation used for determining if traffic will go through Cloudflare
128✔
73
        CloudflareProxiedKey        = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
73✔
74
        CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
73✔
75
        CloudflareRegionKey         = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
73✔
76

73✔
77
        SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
55✔
78
)
79

80
const (
9✔
81
        ttlMinimum = 1
9✔
82
        ttlMaximum = math.MaxInt32
9✔
83
)
84

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

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

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

126
        return int64(ttlDuration.Seconds()), nil
127
}
128

129
type kubeObject interface {
130
        runtime.Object
115✔
131
        metav1.Object
115✔
132
}
115✔
133

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

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

158
func getHostnamesFromAnnotations(annotations map[string]string) []string {
159
        hostnameAnnotation, exists := annotations[hostnameAnnotationKey]
160
        if !exists {
161
                return nil
162
        }
163
        return splitHostnameAnnotation(hostnameAnnotation)
164
}
165

166
func getAccessFromAnnotations(annotations map[string]string) string {
167
        return annotations[accessAnnotationKey]
168
}
169

170
func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
171
        return annotations[endpointsTypeAnnotationKey]
172
}
173

174
func getInternalHostnamesFromAnnotations(annotations map[string]string) []string {
175
        internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey]
176
        if !exists {
177
                return nil
178
        }
179
        return splitHostnameAnnotation(internalHostnameAnnotation)
180
}
181

182
func splitHostnameAnnotation(annotation string) []string {
183
        return strings.Split(strings.ReplaceAll(annotation, " ", ""), ",")
184
}
185

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

191
func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
192
        providerSpecificAnnotations := endpoint.ProviderSpecific{}
193

194
        if v, exists := annotations[CloudflareProxiedKey]; exists {
195
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
196
                        Name:  CloudflareProxiedKey,
197
                        Value: v,
198
                })
199
        }
200
        if v, exists := annotations[CloudflareCustomHostnameKey]; exists {
201
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
202
                        Name:  CloudflareCustomHostnameKey,
203
                        Value: v,
204
                })
205
        }
206
        if v, exists := annotations[CloudflareRegionKey]; exists {
207
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
208
                        Name:  CloudflareRegionKey,
209
                        Value: v,
210
                })
211
        }
212
        if getAliasFromAnnotations(annotations) {
213
                providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
214
                        Name:  "alias",
215
                        Value: "true",
216
                })
217
        }
218
        setIdentifier := ""
219
        for k, v := range annotations {
220
                if k == SetIdentifierKey {
221
                        setIdentifier = v
222
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
223
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
224
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
225
                                Name:  fmt.Sprintf("aws/%s", attr),
226
                                Value: v,
227
                        })
228
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") {
229
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-")
230
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
231
                                Name:  fmt.Sprintf("scw/%s", attr),
232
                                Value: v,
233
                        })
234
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") {
235
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-")
236
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
237
                                Name:  fmt.Sprintf("ibmcloud-%s", attr),
238
                                Value: v,
239
                        })
240
                } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/webhook-") {
241
                        // Support for wildcard annotations for webhook providers
242
                        attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/webhook-")
243
                        providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
244
                                Name:  fmt.Sprintf("webhook/%s", attr),
245
                                Value: v,
246
                        })
247
                }
248
        }
249
        return providerSpecificAnnotations, setIdentifier
250
}
251

252
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
253
// Returns empty endpoints array if none are found.
254
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
255
        var targets endpoint.Targets
256

257
        // Get the desired hostname of the ingress from the annotation.
258
        targetAnnotation, exists := annotations[targetAnnotationKey]
259
        if exists && targetAnnotation != "" {
260
                // splits the hostname annotation and removes the trailing periods
261
                targetsList := strings.Split(strings.ReplaceAll(targetAnnotation, " ", ""), ",")
262
                for _, targetHostname := range targetsList {
263
                        targetHostname = strings.TrimSuffix(targetHostname, ".")
264
                        targets = append(targets, targetHostname)
265
                }
266
        }
267
        return targets
268
}
269

270
// suitableType returns the DNS resource record type suitable for the target.
271
// In this case type A/AAAA for IPs and type CNAME for everything else.
272
func suitableType(target string) string {
273
        netIP, err := netip.ParseAddr(target)
274
        if err == nil && netIP.Is4() {
275
                return endpoint.RecordTypeA
276
        } else if err == nil && netIP.Is6() {
277
                return endpoint.RecordTypeAAAA
278
        }
279
        return endpoint.RecordTypeCNAME
280
}
281

282
// endpointsForHostname returns the endpoint objects for each host-target combination.
283
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
284
        var endpoints []*endpoint.Endpoint
285

286
        var aTargets endpoint.Targets
287
        var aaaaTargets endpoint.Targets
288
        var cnameTargets endpoint.Targets
289

290
        for _, t := range targets {
291
                switch suitableType(t) {
292
                case endpoint.RecordTypeA:
293
                        aTargets = append(aTargets, t)
294
                case endpoint.RecordTypeAAAA:
295
                        aaaaTargets = append(aaaaTargets, t)
296
                default:
297
                        cnameTargets = append(cnameTargets, t)
298
                }
299
        }
300

301
        if len(aTargets) > 0 {
302
                epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
303
                if epA != nil {
304
                        epA.ProviderSpecific = providerSpecific
305
                        epA.SetIdentifier = setIdentifier
306
                        if resource != "" {
307
                                epA.Labels[endpoint.ResourceLabelKey] = resource
308
                        }
309
                        endpoints = append(endpoints, epA)
310
                }
311
        }
312

313
        if len(aaaaTargets) > 0 {
314
                epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
315
                if epAAAA != nil {
316
                        epAAAA.ProviderSpecific = providerSpecific
317
                        epAAAA.SetIdentifier = setIdentifier
318
                        if resource != "" {
319
                                epAAAA.Labels[endpoint.ResourceLabelKey] = resource
320
                        }
321
                        endpoints = append(endpoints, epAAAA)
322
                }
323
        }
324

325
        if len(cnameTargets) > 0 {
326
                epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
327
                if epCNAME != nil {
328
                        epCNAME.ProviderSpecific = providerSpecific
329
                        epCNAME.SetIdentifier = setIdentifier
330
                        if resource != "" {
331
                                epCNAME.Labels[endpoint.ResourceLabelKey] = resource
332
                        }
333
                        endpoints = append(endpoints, epCNAME)
334
                }
335
        }
336

337
        return endpoints
338
}
339

340
func getLabelSelector(annotationFilter string) (labels.Selector, error) {
341
        labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
342
        if err != nil {
343
                return nil, err
344
        }
345
        return metav1.LabelSelectorAsSelector(labelSelector)
346
}
347

348
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
349
        annotations := labels.Set(srcAnnotations)
350
        return selector.Matches(annotations)
351
}
352

353
type eventHandlerFunc func()
354

355
func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() }
356
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{})         { fn() }
357
func (fn eventHandlerFunc) OnDelete(obj interface{})                    { fn() }
358

359
type informerFactory interface {
360
        WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
361
}
362

363
func waitForCacheSync(ctx context.Context, factory informerFactory) error {
364
        ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
365
        defer cancel()
366
        for typ, done := range factory.WaitForCacheSync(ctx.Done()) {
367
                if !done {
368
                        select {
369
                        case <-ctx.Done():
370
                                return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err())
371
                        default:
372
                                return fmt.Errorf("failed to sync %v", typ)
373
                        }
374
                }
375
        }
376
        return nil
377
}
378

379
type dynamicInformerFactory interface {
380
        WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
381
}
382

383
func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error {
384
        ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
385
        defer cancel()
386
        for typ, done := range factory.WaitForCacheSync(ctx.Done()) {
387
                if !done {
388
                        select {
389
                        case <-ctx.Done():
390
                                return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err())
391
                        default:
392
                                return fmt.Errorf("failed to sync %v", typ)
393
                        }
394
                }
395
        }
396
        return nil
397
}
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