-
Notifications
You must be signed in to change notification settings - Fork 33
/
pod.go
615 lines (555 loc) · 19.6 KB
/
pod.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
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
/*
Copyright 2021.
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 externaldnscontroller
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
configv1 "github.com/openshift/api/config/v1"
operatorv1alpha1 "github.com/openshift/external-dns-operator/api/v1alpha1"
controller "github.com/openshift/external-dns-operator/pkg/operator/controller"
"github.com/openshift/external-dns-operator/pkg/utils"
)
const (
defaultMetricsAddress = "127.0.0.1"
defaultOwnerPrefix = "external-dns"
defaultMetricsStartPort = 7979
defaultConfigMountPath = "/etc/kubernetes"
defaultTXTRecordPrefix = "external-dns-"
providerArg = "--provider="
httpProxyEnvVar = "HTTP_PROXY"
httpsProxyEnvVar = "HTTPS_PROXY"
noProxyEnvVar = "NO_PROXY"
trustedCAVolumeName = "trusted-ca"
trustedCAFileName = "tls-ca-bundle.pem"
trustedCAFileKey = "ca-bundle.crt"
trustedCAExtractedPEMDir = "/etc/pki/ca-trust/extracted/pem"
//
// AWS
//
awsAccessKeyIDEnvVar = "AWS_ACCESS_KEY_ID"
awsAccessKeySecretEnvVar = "AWS_SECRET_ACCESS_KEY"
awsAccessKeyIDKey = "aws_access_key_id"
awsAccessKeySecretKey = "aws_secret_access_key"
//
// Azure
//
azureConfigVolumeName = "azure-config-file"
azureConfigMountPath = defaultConfigMountPath
azureConfigFileName = "azure.json"
azureConfigFileKey = "azure.json"
//
// GCP
//
gcpCredentialsVolumeName = "gcp-credentials-file"
gcpCredentialsMountPath = defaultConfigMountPath
gcpCredentialsFileKey = "gcp-credentials.json"
gcpCredentialsFileName = "gcp-credentials.json"
gcpAppCredentialsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
//
// BlueCat
//
blueCatConfigVolumeName = "bluecat-config-file"
blueCatConfigMountPath = defaultConfigMountPath
blueCatConfigFileName = "bluecat.json"
blueCatConfigFileKey = "bluecat.json"
//
// Infoblox
//
infobloxWAPIUsernameEnvVar = "EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME"
infobloxWAPIPasswordEnvVar = "EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD"
infobloxWAPIUsernameKey = "EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME"
infobloxWAPIPasswordKey = "EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD"
)
// externalDNSContainerBuilder builds the definition of the containers for ExternalDNS POD
type externalDNSContainerBuilder struct {
image string
provider string
source string
volumes []corev1.Volume
secretName string
externalDNS *operatorv1alpha1.ExternalDNS
isOpenShift bool
platformStatus *configv1.PlatformStatus
counter int
}
// build returns the definition of a single container for the given DNS zone with unique metrics port
func (b *externalDNSContainerBuilder) build(zone string) (*corev1.Container, error) {
seq := b.counter
b.counter++
return b.buildSeq(seq, zone)
}
// buildSeq returns the definition of a single container for the given DNS zone
// sequence param is used to create the unique metrics port
func (b *externalDNSContainerBuilder) buildSeq(seq int, zone string) (*corev1.Container, error) {
container := b.defaultContainer(controller.ExternalDNSContainerName(zone))
err := b.fillProviderAgnosticFields(seq, zone, container)
if err != nil {
return nil, err
}
b.fillProviderSpecificFields(zone, container)
return container, nil
}
// defaultContainer returns the initial definition of any container of ExternalDNS POD
func (b *externalDNSContainerBuilder) defaultContainer(name string) *corev1.Container {
return &corev1.Container{
Name: name,
Image: b.image,
Args: []string{},
ImagePullPolicy: corev1.PullIfNotPresent,
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
}
}
// fillProviderAgnosticFields fills the given container with the data agnostic to any provider
func (b *externalDNSContainerBuilder) fillProviderAgnosticFields(seq int, zone string, container *corev1.Container) error {
//
// ARGS
//
args := []string{
fmt.Sprintf("--metrics-address=%s:%d", defaultMetricsAddress, defaultMetricsStartPort+seq),
fmt.Sprintf("--txt-owner-id=%s-%s", defaultOwnerPrefix, b.externalDNS.Name),
fmt.Sprintf("--provider=%s", b.provider),
fmt.Sprintf("--source=%s", b.source),
"--policy=sync",
"--registry=txt",
"--log-level=debug",
}
if zone != "" {
args = append(args, fmt.Sprintf("--zone-id-filter=%s", zone))
}
if b.externalDNS.Spec.Source.LabelFilter != nil {
args = append(args, fmt.Sprintf("--label-filter=%s", metav1.FormatLabelSelector(b.externalDNS.Spec.Source.LabelFilter)))
}
if b.externalDNS.Spec.Source.Service != nil && len(b.externalDNS.Spec.Source.Service.ServiceType) > 0 {
publishInternal := false
for _, serviceType := range b.externalDNS.Spec.Source.Service.ServiceType {
args = append(args, fmt.Sprintf("--service-type-filter=%s", string(serviceType)))
if serviceType == corev1.ServiceTypeClusterIP {
publishInternal = true
}
}
// legacy option before the service-type-filter was introduced
// must be there though, ClusterIP endpoints won't be added without it
if publishInternal {
args = append(args, "--publish-internal-services")
}
}
if b.externalDNS.Spec.Source.HostnameAnnotationPolicy == operatorv1alpha1.HostnameAnnotationPolicyIgnore {
args = append(args, "--ignore-hostname-annotation")
}
if len(b.externalDNS.Spec.Source.FQDNTemplate) > 0 {
args = append(args, fmt.Sprintf("--fqdn-template=%s", strings.Join(b.externalDNS.Spec.Source.FQDNTemplate, ",")))
} else {
// ExternalDNS needs FQDNTemplate if the hostname annotation is ignored even for Route source.
// However it doesn't make much sense as the hostname is retrieved from the route's spec.
// Feeding ExternalDNS with some dummy template just to pass the validation.
if b.externalDNS.Spec.Source.HostnameAnnotationPolicy == operatorv1alpha1.HostnameAnnotationPolicyIgnore &&
b.externalDNS.Spec.Source.Type == operatorv1alpha1.SourceTypeRoute {
args = append(args, "--fqdn-template={{\"\"}}")
}
}
if b.externalDNS.Spec.Source.OpenShiftRoute != nil && len(b.externalDNS.Spec.Source.OpenShiftRoute.RouterName) > 0 {
args = append(args, fmt.Sprintf("--openshift-router-name=%s", b.externalDNS.Spec.Source.OpenShiftRoute.RouterName))
}
filterArgs, err := b.domainFilters()
if err != nil {
return err
}
container.Args = append(container.Args, filterArgs...)
container.Args = append(container.Args, args...)
//
// ENV
//
if utils.EnvProxySupportedProvider(b.externalDNS) {
if val := os.Getenv(httpProxyEnvVar); val != "" {
container.Env = append(container.Env, corev1.EnvVar{Name: httpProxyEnvVar, Value: val})
}
if val := os.Getenv(httpsProxyEnvVar); val != "" {
container.Env = append(container.Env, corev1.EnvVar{Name: httpsProxyEnvVar, Value: val})
}
if val := os.Getenv(noProxyEnvVar); val != "" {
container.Env = append(container.Env, corev1.EnvVar{Name: noProxyEnvVar, Value: val})
}
}
//
// VOLUME MOUNTS
//
for _, v := range b.volumes {
// if trustedCA volume was added
if v.Name == trustedCAVolumeName {
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: v.Name,
MountPath: trustedCAExtractedPEMDir,
ReadOnly: true,
})
}
}
return nil
}
func (b *externalDNSContainerBuilder) domainFilters() ([]string, error) {
var args, includePatterns, excludePatterns []string
for _, d := range b.externalDNS.Spec.Domains {
switch d.FilterType {
case operatorv1alpha1.FilterTypeInclude:
switch d.MatchType {
case operatorv1alpha1.DomainMatchTypeExact:
if d.Name == nil {
return nil, fmt.Errorf("name for domain cannot be empty")
}
args = append(args, fmt.Sprintf("--domain-filter=%s", *d.Name))
case operatorv1alpha1.DomainMatchTypeRegex:
if d.Pattern == nil {
return nil, fmt.Errorf("pattern for domain cannot be empty")
}
_, err := regexp.Compile(*d.Pattern)
if err != nil {
return nil, fmt.Errorf("input pattern %s is invalid: %w", *d.Pattern, err)
}
includePatterns = append(includePatterns, *d.Pattern)
default:
return nil, fmt.Errorf("unknown match type in domains: %s", d.MatchType)
}
case operatorv1alpha1.FilterTypeExclude:
switch d.MatchType {
case operatorv1alpha1.DomainMatchTypeExact:
if d.Name == nil {
return nil, fmt.Errorf("name for domain cannot be empty")
}
args = append(args, fmt.Sprintf("--exclude-domains=%s", *d.Name))
case operatorv1alpha1.DomainMatchTypeRegex:
if d.Pattern == nil {
return nil, fmt.Errorf("pattern for domain cannot be empty")
}
_, err := regexp.Compile(*d.Pattern)
if err != nil {
return nil, fmt.Errorf("exclude pattern %s is invalid: %w", *d.Pattern, err)
}
excludePatterns = append(excludePatterns, *d.Pattern)
default:
return nil, fmt.Errorf("unknown match type in domains: %s", d.MatchType)
}
}
}
if len(includePatterns) > 0 {
args = append(args, fmt.Sprintf("--regex-domain-filter=%s", combineRegexps(includePatterns)))
}
if len(excludePatterns) > 0 {
args = append(args, fmt.Sprintf("--regex-domain-exclusion=%s", combineRegexps(excludePatterns)))
}
return args, nil
}
func combineRegexps(patterns []string) string {
if len(patterns) == 1 {
return patterns[0]
}
parenthesisedPatterns := make([]string, len(patterns))
for i, p := range patterns {
parenthesisedPatterns[i] = fmt.Sprintf("(%s)", p)
}
return strings.Join(parenthesisedPatterns, "|")
}
// fillProviderSpecificFields fills the fields specific to the provider of given ExternalDNS
func (b *externalDNSContainerBuilder) fillProviderSpecificFields(zone string, container *corev1.Container) {
switch b.provider {
case externalDNSProviderTypeAWS:
b.fillAWSFields(container)
case externalDNSProviderTypeAzure, externalDNSProviderTypeAzurePrivate:
b.fillAzureFields(zone, container)
case externalDNSProviderTypeGCP:
b.fillGCPFields(container)
case externalDNSProviderTypeBlueCat:
b.fillBlueCatFields(container)
case externalDNSProviderTypeInfoblox:
b.fillInfobloxFields(container)
}
}
// fillAWSFields fills the given container with the data specific to AWS provider
func (b *externalDNSContainerBuilder) fillAWSFields(container *corev1.Container) {
container.Args = addTXTPrefixFlag(container.Args)
// don't add empty credentials environment variables if no secret was given
if len(b.secretName) == 0 {
return
}
env := []corev1.EnvVar{
{
Name: awsAccessKeyIDEnvVar,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: b.secretName,
},
Key: awsAccessKeyIDKey,
},
},
},
{
Name: awsAccessKeySecretEnvVar,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: b.secretName,
},
Key: awsAccessKeySecretKey,
},
},
},
}
container.Env = append(container.Env, env...)
}
// fillAzureFields fills the given container with the data specific to Azure provider
func (b *externalDNSContainerBuilder) fillAzureFields(zone string, container *corev1.Container) {
// https://github.com/kubernetes-sigs/external-dns/issues/2082
container.Args = addTXTPrefixFlag(container.Args)
// check the zone field for the keyword 'privatednszones', this ensures that the
// provider 'azure-private-dns' is passed to the container
// to set the operand provider correctly
if strings.Contains(strings.ToLower(zone), azurePrivateDNSZonesResourceSubStr) {
for i, x := range container.Args {
if strings.Contains(x, providerArg) {
container.Args[i] = providerArg + externalDNSProviderTypeAzurePrivate
break
}
}
}
// no volume mounts will be added if there is no config volume added before
for _, v := range b.volumes {
// config volume
if v.Name == azureConfigVolumeName {
container.Args = append(container.Args, fmt.Sprintf("--azure-config-file=%s/%s", azureConfigMountPath, azureConfigFileName))
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: v.Name,
MountPath: azureConfigMountPath,
ReadOnly: true,
})
}
}
}
// fillGCPFields fills the given container with the data specific to Google provider
func (b *externalDNSContainerBuilder) fillGCPFields(container *corev1.Container) {
// https://github.com/kubernetes-sigs/external-dns/issues/262
container.Args = addTXTPrefixFlag(container.Args)
if !b.isOpenShift {
// don't add empty args if GCP provider is not given
if b.externalDNS.Spec.Provider.GCP == nil {
return
}
if b.externalDNS.Spec.Provider.GCP.Project != nil && len(*b.externalDNS.Spec.Provider.GCP.Project) > 0 {
container.Args = append(container.Args, fmt.Sprintf("--google-project=%s", *b.externalDNS.Spec.Provider.GCP.Project))
}
} else {
if b.platformStatus != nil && b.platformStatus.GCP != nil && len(b.platformStatus.GCP.ProjectID) > 0 {
container.Args = append(container.Args, fmt.Sprintf("--google-project=%s", b.platformStatus.GCP.ProjectID))
}
}
for _, v := range b.volumes {
// credentials volume
if v.Name == gcpCredentialsVolumeName {
container.Env = append(container.Env, corev1.EnvVar{Name: gcpAppCredentialsEnvVar, Value: filepath.Join(gcpCredentialsMountPath, gcpCredentialsFileKey)})
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: v.Name,
MountPath: gcpCredentialsMountPath,
ReadOnly: true,
})
}
}
}
// fillBlueCatFields fills the given container with the data specific to BlueCat provider
func (b *externalDNSContainerBuilder) fillBlueCatFields(container *corev1.Container) {
// only standard CNAME records are supported
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/ENUM-number-generic-methods/9.2.0
container.Args = addTXTPrefixFlag(container.Args)
// no volume mounts will be added if there is no config volume added before
for _, v := range b.volumes {
// config volume
if v.Name == blueCatConfigVolumeName {
container.Args = append(container.Args, fmt.Sprintf("--bluecat-config-file=%s/%s", blueCatConfigMountPath, blueCatConfigFileName))
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: v.Name,
MountPath: blueCatConfigMountPath,
ReadOnly: true,
})
}
}
}
// fillInfobloxFields fills the given container with the data specific to Infoblox provider
func (b *externalDNSContainerBuilder) fillInfobloxFields(container *corev1.Container) {
// don't add empty args or env vars if secret or infoblox provider is not given
if len(b.secretName) == 0 || b.externalDNS.Spec.Provider.Infoblox == nil {
return
}
args := []string{
fmt.Sprintf("--infoblox-wapi-port=%d", b.externalDNS.Spec.Provider.Infoblox.WAPIPort),
}
if len(b.externalDNS.Spec.Provider.Infoblox.GridHost) > 0 {
args = append(args, fmt.Sprintf("--infoblox-grid-host=%s", b.externalDNS.Spec.Provider.Infoblox.GridHost))
}
if len(b.externalDNS.Spec.Provider.Infoblox.WAPIVersion) > 0 {
args = append(args, fmt.Sprintf("--infoblox-wapi-version=%s", b.externalDNS.Spec.Provider.Infoblox.WAPIVersion))
}
env := []corev1.EnvVar{
{
Name: infobloxWAPIUsernameEnvVar,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: b.secretName,
},
Key: infobloxWAPIUsernameKey,
},
},
},
{
Name: infobloxWAPIPasswordEnvVar,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: b.secretName,
},
Key: infobloxWAPIPasswordKey,
},
},
},
}
container.Args = append(container.Args, args...)
container.Env = append(container.Env, env...)
}
// externalDNSVolumeBuilder builds the definition of the volumes for ExternalDNS POD
type externalDNSVolumeBuilder struct {
provider string
secretName string
trustedCAConfigMapName string
}
// newExternalDNSVolumeBuilder returns an instance of volume builder
func newExternalDNSVolumeBuilder(provider, secretName, trustedCAConfigMapName string) *externalDNSVolumeBuilder {
return &externalDNSVolumeBuilder{
provider: provider,
secretName: secretName,
trustedCAConfigMapName: trustedCAConfigMapName,
}
}
// build returns the definition of all the volumes
func (b *externalDNSVolumeBuilder) build() []corev1.Volume {
volumes := b.providerAgnosticVolumes()
return append(volumes, b.providerSpecificVolumes()...)
}
// providerAgnosticVolumes returns the volumes ...
func (b *externalDNSVolumeBuilder) providerAgnosticVolumes() []corev1.Volume {
if len(b.trustedCAConfigMapName) > 0 {
return []corev1.Volume{
{
Name: trustedCAVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: b.trustedCAConfigMapName,
},
Items: []corev1.KeyToPath{
{
Key: trustedCAFileKey,
Path: trustedCAFileName,
},
},
},
},
},
}
}
return nil
}
// providerSpecificVolumes returns the volumes specific to the provider of given External DNS
func (b *externalDNSVolumeBuilder) providerSpecificVolumes() []corev1.Volume {
switch b.provider {
case externalDNSProviderTypeAzure:
return b.azureVolumes()
case externalDNSProviderTypeGCP:
return b.gcpVolumes()
case externalDNSProviderTypeBlueCat:
return b.bluecatVolumes()
}
return nil
}
// azureVolumes returns volumes needed for Azure provider
func (b *externalDNSVolumeBuilder) azureVolumes() []corev1.Volume {
if len(b.secretName) == 0 {
return nil
}
return []corev1.Volume{
{
Name: azureConfigVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: b.secretName,
Items: []corev1.KeyToPath{
{
Key: azureConfigFileKey,
Path: azureConfigFileName,
},
},
},
},
},
}
}
// gcpVolumes returns volumes needed for Google provider
func (b *externalDNSVolumeBuilder) gcpVolumes() []corev1.Volume {
if len(b.secretName) == 0 {
return nil
}
return []corev1.Volume{
{
Name: gcpCredentialsVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: b.secretName,
Items: []corev1.KeyToPath{
{
Key: gcpCredentialsFileKey,
Path: gcpCredentialsFileName,
},
},
},
},
},
}
}
// bluecatVolumes returns volumes needed for BlueCat provider
func (b *externalDNSVolumeBuilder) bluecatVolumes() []corev1.Volume {
if len(b.secretName) == 0 {
return nil
}
return []corev1.Volume{
{
Name: blueCatConfigVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: b.secretName,
Items: []corev1.KeyToPath{
{
Key: blueCatConfigFileKey,
Path: blueCatConfigFileName,
},
},
},
},
},
}
}
// addTXTPrefixFlag adds the txt prefix flag with default value
// needed if CNAME records are used: https://github.com/kubernetes-sigs/external-dns#note
func addTXTPrefixFlag(args []string) []string {
return append(args, fmt.Sprintf("--txt-prefix=%s", defaultTXTRecordPrefix))
}