-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
endpoint.go
398 lines (341 loc) · 12.3 KB
/
endpoint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package endpoint
import (
"fmt"
"net/netip"
"sort"
"strings"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// RecordTypeA is a RecordType enum value
RecordTypeA = "A"
// RecordTypeAAAA is a RecordType enum value
RecordTypeAAAA = "AAAA"
// RecordTypeCNAME is a RecordType enum value
RecordTypeCNAME = "CNAME"
// RecordTypeTXT is a RecordType enum value
RecordTypeTXT = "TXT"
// RecordTypeSRV is a RecordType enum value
RecordTypeSRV = "SRV"
// RecordTypeNS is a RecordType enum value
RecordTypeNS = "NS"
// RecordTypePTR is a RecordType enum value
RecordTypePTR = "PTR"
// RecordTypeMX is a RecordType enum value
RecordTypeMX = "MX"
// RecordTypeNAPTR is a RecordType enum value
RecordTypeNAPTR = "NAPTR"
)
// TTL is a structure defining the TTL of a DNS record
type TTL int64
// IsConfigured returns true if TTL is configured, false otherwise
func (ttl TTL) IsConfigured() bool {
return ttl > 0
}
// Targets is a representation of a list of targets for an endpoint.
type Targets []string
// NewTargets is a convenience method to create a new Targets object from a vararg of strings
func NewTargets(target ...string) Targets {
t := make(Targets, 0, len(target))
t = append(t, target...)
return t
}
func (t Targets) String() string {
return strings.Join(t, ";")
}
func (t Targets) Len() int {
return len(t)
}
func (t Targets) Less(i, j int) bool {
ipi, err := netip.ParseAddr(t[i])
if err != nil {
return t[i] < t[j]
}
ipj, err := netip.ParseAddr(t[j])
if err != nil {
return t[i] < t[j]
}
return ipi.String() < ipj.String()
}
func (t Targets) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
// Same compares to Targets and returns true if they are identical (case-insensitive)
func (t Targets) Same(o Targets) bool {
if len(t) != len(o) {
return false
}
sort.Stable(t)
sort.Stable(o)
for i, e := range t {
if !strings.EqualFold(e, o[i]) {
// IPv6 can be shortened, so it should be parsed for equality checking
ipA, err := netip.ParseAddr(e)
if err != nil {
log.WithFields(log.Fields{
"targets": t,
"comparisonTargets": o,
}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
}
ipB, err := netip.ParseAddr(o[i])
if err != nil {
log.WithFields(log.Fields{
"targets": t,
"comparisonTargets": o,
}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
}
// IPv6 Address Shortener == IPv6 Address Expander
if ipA.IsValid() && ipB.IsValid() {
return ipA.String() == ipB.String()
}
return false
}
}
return true
}
// IsLess should fulfill the requirement to compare two targets and choose the 'lesser' one.
// In the past target was a simple string so simple string comparison could be used. Now we define 'less'
// as either being the shorter list of targets or where the first entry is less.
// FIXME We really need to define under which circumstances a list Targets is considered 'less'
// than another.
func (t Targets) IsLess(o Targets) bool {
if len(t) < len(o) {
return true
}
if len(t) > len(o) {
return false
}
sort.Sort(t)
sort.Sort(o)
for i, e := range t {
if e != o[i] {
// Explicitly prefers IP addresses (e.g. A records) over FQDNs (e.g. CNAMEs).
// This prevents behavior like `1-2-3-4.example.com` being "less" than `1.2.3.4` when doing lexicographical string comparison.
ipA, err := netip.ParseAddr(e)
if err != nil {
// Ignoring parsing errors is fine due to the empty netip.Addr{} type being an invalid IP,
// which is checked by IsValid() below. However, still log them in case a provider is experiencing
// non-obvious issues with the records being created.
log.WithFields(log.Fields{
"targets": t,
"comparisonTargets": o,
}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
}
ipB, err := netip.ParseAddr(o[i])
if err != nil {
log.WithFields(log.Fields{
"targets": t,
"comparisonTargets": o,
}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
}
// If both targets are valid IP addresses, use the built-in Less() function to do the comparison.
// If one is a valid IP and the other is not, prefer the IP address (consider it "less").
// If neither is a valid IP, use lexicographical string comparison to determine which string sorts first alphabetically.
switch {
case ipA.IsValid() && ipB.IsValid():
return ipA.Less(ipB)
case ipA.IsValid() && !ipB.IsValid():
return true
case !ipA.IsValid() && ipB.IsValid():
return false
default:
return e < o[i]
}
}
}
return false
}
// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers
type ProviderSpecificProperty struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}
// ProviderSpecific holds configuration which is specific to individual DNS providers
type ProviderSpecific []ProviderSpecificProperty
// EndpointKey is the type of a map key for separating endpoints or targets.
type EndpointKey struct {
DNSName string
RecordType string
SetIdentifier string
}
// Endpoint is a high-level way of a connection between a service and an IP
type Endpoint struct {
// The hostname of the DNS record
DNSName string `json:"dnsName,omitempty"`
// The targets the DNS record points to
Targets Targets `json:"targets,omitempty"`
// RecordType type of record, e.g. CNAME, A, AAAA, SRV, TXT etc
RecordType string `json:"recordType,omitempty"`
// Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple')
SetIdentifier string `json:"setIdentifier,omitempty"`
// TTL for the record
RecordTTL TTL `json:"recordTTL,omitempty"`
// Labels stores labels defined for the Endpoint
// +optional
Labels Labels `json:"labels,omitempty"`
// ProviderSpecific stores provider specific config
// +optional
ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
}
// NewEndpoint initialization method to be used to create an endpoint
func NewEndpoint(dnsName, recordType string, targets ...string) *Endpoint {
return NewEndpointWithTTL(dnsName, recordType, TTL(0), targets...)
}
// NewEndpointWithTTL initialization method to be used to create an endpoint with a TTL struct
func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) *Endpoint {
cleanTargets := make([]string, len(targets))
for idx, target := range targets {
cleanTargets[idx] = strings.TrimSuffix(target, ".")
}
for _, label := range strings.Split(dnsName, ".") {
if len(label) > 63 {
log.Errorf("label %s in %s is longer than 63 characters. Cannot create endpoint", label, dnsName)
return nil
}
}
return &Endpoint{
DNSName: strings.TrimSuffix(dnsName, "."),
Targets: cleanTargets,
RecordType: recordType,
Labels: NewLabels(),
RecordTTL: ttl,
}
}
// WithSetIdentifier applies the given set identifier to the endpoint.
func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
e.SetIdentifier = setIdentifier
return e
}
// WithProviderSpecific attaches a key/value pair to the Endpoint and returns the Endpoint.
// This can be used to pass additional data through the stages of ExternalDNS's Endpoint processing.
// The assumption is that most of the time this will be provider specific metadata that doesn't
// warrant its own field on the Endpoint object itself. It differs from Labels in the fact that it's
// not persisted in the Registry but only kept in memory during a single record synchronization.
func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint {
e.SetProviderSpecificProperty(key, value)
return e
}
// GetProviderSpecificProperty returns the value of a ProviderSpecificProperty if the property exists.
func (e *Endpoint) GetProviderSpecificProperty(key string) (string, bool) {
for _, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
return providerSpecific.Value, true
}
}
return "", false
}
// SetProviderSpecificProperty sets the value of a ProviderSpecificProperty.
func (e *Endpoint) SetProviderSpecificProperty(key string, value string) {
for i, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
e.ProviderSpecific[i] = ProviderSpecificProperty{
Name: key,
Value: value,
}
return
}
}
e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value})
}
// DeleteProviderSpecificProperty deletes any ProviderSpecificProperty of the specified name.
func (e *Endpoint) DeleteProviderSpecificProperty(key string) {
for i, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
e.ProviderSpecific = append(e.ProviderSpecific[:i], e.ProviderSpecific[i+1:]...)
return
}
}
}
// Key returns the EndpointKey of the Endpoint.
func (e *Endpoint) Key() EndpointKey {
return EndpointKey{
DNSName: e.DNSName,
RecordType: e.RecordType,
SetIdentifier: e.SetIdentifier,
}
}
// IsOwnedBy returns true if the endpoint owner label matches the given ownerID, false otherwise
func (e *Endpoint) IsOwnedBy(ownerID string) bool {
endpointOwner, ok := e.Labels[OwnerLabelKey]
return ok && endpointOwner == ownerID
}
func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
}
// Apply filter to slice of endpoints and return new filtered slice that includes
// only endpoints that match.
func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint {
filtered := []*Endpoint{}
for _, ep := range eps {
if endpointOwner, ok := ep.Labels[OwnerLabelKey]; !ok || endpointOwner != ownerID {
log.Debugf(`Skipping endpoint %v because owner id does not match, found: "%s", required: "%s"`, ep, endpointOwner, ownerID)
} else {
filtered = append(filtered, ep)
}
}
return filtered
}
// DNSEndpointSpec defines the desired state of DNSEndpoint
type DNSEndpointSpec struct {
Endpoints []*Endpoint `json:"endpoints,omitempty"`
}
// DNSEndpointStatus defines the observed state of DNSEndpoint
type DNSEndpointStatus struct {
// The generation observed by the external-dns controller.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns.
// The user-specified CRD should also have the status sub-resource.
// +k8s:openapi-gen=true
// +groupName=externaldns.k8s.io
// +kubebuilder:resource:path=dnsendpoints
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:metadata:annotations="api-approved.kubernetes.io=https://github.com/kubernetes-sigs/external-dns/pull/2007"
// +versionName=v1alpha1
type DNSEndpoint struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DNSEndpointSpec `json:"spec,omitempty"`
Status DNSEndpointStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// DNSEndpointList is a list of DNSEndpoint objects
type DNSEndpointList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DNSEndpoint `json:"items"`
}
// RemoveDuplicates returns a slice holding the unique endpoints.
// This function doesn't contemplate the Targets of an Endpoint
// as part of the primary Key
func RemoveDuplicates(endpoints []*Endpoint) []*Endpoint {
visited := make(map[EndpointKey]struct{})
result := []*Endpoint{}
for _, ep := range endpoints {
key := ep.Key()
if _, found := visited[key]; !found {
result = append(result, ep)
visited[key] = struct{}{}
} else {
log.Debugf(`Skipping duplicated endpoint: %v`, ep)
}
}
return result
}