main.go 12.6 KB
Newer Older
ideahitme's avatar
ideahitme committed
1 2
/*
Copyright 2017 The Kubernetes Authors.
3

ideahitme's avatar
ideahitme committed
4 5 6
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
7

ideahitme's avatar
ideahitme committed
8
    http://www.apache.org/licenses/LICENSE-2.0
9

ideahitme's avatar
ideahitme committed
10 11 12 13 14 15 16
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.
*/

17 18 19
package main

import (
20
	"context"
21 22 23 24
	"net/http"
	"os"
	"os/signal"
	"syscall"
25
	"time"
26

27
	"github.com/prometheus/client_golang/prometheus/promhttp"
28
	log "github.com/sirupsen/logrus"
29

30
	_ "k8s.io/client-go/plugin/pkg/client/auth"
31 32 33 34 35 36 37 38 39 40 41 42 43
	"sigs.k8s.io/external-dns/provider/akamai"
	"sigs.k8s.io/external-dns/provider/alibabacloud"
	"sigs.k8s.io/external-dns/provider/aws"
	"sigs.k8s.io/external-dns/provider/awssd"
	"sigs.k8s.io/external-dns/provider/azure"
	"sigs.k8s.io/external-dns/provider/cloudflare"
	"sigs.k8s.io/external-dns/provider/coredns"
	"sigs.k8s.io/external-dns/provider/designate"
	"sigs.k8s.io/external-dns/provider/digitalocean"
	"sigs.k8s.io/external-dns/provider/dnsimple"
	"sigs.k8s.io/external-dns/provider/dyn"
	"sigs.k8s.io/external-dns/provider/exoscale"
	"sigs.k8s.io/external-dns/provider/google"
Vladimir Smagin's avatar
Vladimir Smagin committed
44
	"sigs.k8s.io/external-dns/provider/hetzner"
45 46 47 48 49 50 51 52 53 54 55 56 57
	"sigs.k8s.io/external-dns/provider/infoblox"
	"sigs.k8s.io/external-dns/provider/inmemory"
	"sigs.k8s.io/external-dns/provider/linode"
	"sigs.k8s.io/external-dns/provider/ns1"
	"sigs.k8s.io/external-dns/provider/oci"
	"sigs.k8s.io/external-dns/provider/ovh"
	"sigs.k8s.io/external-dns/provider/pdns"
	"sigs.k8s.io/external-dns/provider/rcode0"
	"sigs.k8s.io/external-dns/provider/rdns"
	"sigs.k8s.io/external-dns/provider/rfc2136"
	"sigs.k8s.io/external-dns/provider/transip"
	"sigs.k8s.io/external-dns/provider/vinyldns"
	"sigs.k8s.io/external-dns/provider/vultr"
58

59
	"sigs.k8s.io/external-dns/controller"
60
	"sigs.k8s.io/external-dns/endpoint"
61 62 63 64 65 66
	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
	"sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
	"sigs.k8s.io/external-dns/plan"
	"sigs.k8s.io/external-dns/provider"
	"sigs.k8s.io/external-dns/registry"
	"sigs.k8s.io/external-dns/source"
67 68
)

69
func main() {
70
	cfg := externaldns.NewConfig()
71
	if err := cfg.ParseFlags(os.Args[1:]); err != nil {
ideahitme's avatar
ideahitme committed
72 73
		log.Fatalf("flag parsing error: %v", err)
	}
jvassev's avatar
jvassev committed
74
	log.Infof("config: %s", cfg)
Henning Jacobs's avatar
Henning Jacobs committed
75

76
	if err := validation.ValidateConfig(cfg); err != nil {
77
		log.Fatalf("config validation failed: %v", err)
78 79
	}

ideahitme's avatar
ideahitme committed
80
	if cfg.LogFormat == "json" {
81 82
		log.SetFormatter(&log.JSONFormatter{})
	}
83
	if cfg.DryRun {
ideahitme's avatar
ideahitme committed
84
		log.Info("running in dry-run mode. No changes to DNS records will be made.")
85
	}
86 87 88 89

	ll, err := log.ParseLevel(cfg.LogLevel)
	if err != nil {
		log.Fatalf("failed to parse log level: %v", err)
90
	}
91
	log.SetLevel(ll)
92

93
	ctx, cancel := context.WithCancel(context.Background())
94

95
	go serveMetrics(cfg.MetricsAddress)
96
	go handleSigterm(cancel)
97

98 99
	// Create a source.Config from the flags passed by the user.
	sourceCfg := &source.Config{
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
		Namespace:                      cfg.Namespace,
		AnnotationFilter:               cfg.AnnotationFilter,
		FQDNTemplate:                   cfg.FQDNTemplate,
		CombineFQDNAndAnnotation:       cfg.CombineFQDNAndAnnotation,
		IgnoreHostnameAnnotation:       cfg.IgnoreHostnameAnnotation,
		Compatibility:                  cfg.Compatibility,
		PublishInternal:                cfg.PublishInternal,
		PublishHostIP:                  cfg.PublishHostIP,
		AlwaysPublishNotReadyAddresses: cfg.AlwaysPublishNotReadyAddresses,
		ConnectorServer:                cfg.ConnectorSourceServer,
		CRDSourceAPIVersion:            cfg.CRDSourceAPIVersion,
		CRDSourceKind:                  cfg.CRDSourceKind,
		KubeConfig:                     cfg.KubeConfig,
		KubeMaster:                     cfg.Master,
		ServiceTypeFilter:              cfg.ServiceTypeFilter,
		CFAPIEndpoint:                  cfg.CFAPIEndpoint,
		CFUsername:                     cfg.CFUsername,
		CFPassword:                     cfg.CFPassword,
		ContourLoadBalancerService:     cfg.ContourLoadBalancerService,
119 120
		SkipperRouteGroupVersion:       cfg.SkipperRouteGroupVersion,
		RequestTimeout:                 cfg.RequestTimeout,
121
	}
122

123 124
	// Lookup all the selected sources by names and pass them the desired configuration.
	sources, err := source.ByNames(&source.SingletonClientGenerator{
125 126 127 128 129 130 131 132 133
		KubeConfig: cfg.KubeConfig,
		KubeMaster: cfg.Master,
		// If update events are enabled, disable timeout.
		RequestTimeout: func() time.Duration {
			if cfg.UpdateEvents {
				return 0
			}
			return cfg.RequestTimeout
		}(),
134
	}, cfg.Sources, sourceCfg)
135 136 137 138
	if err != nil {
		log.Fatal(err)
	}

139
	// Combine multiple sources into a single, deduplicated source.
140
	endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
141

142
	domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
143
	zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
144
	zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
Cesar Wong's avatar
Cesar Wong committed
145
	zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
146

147 148
	var p provider.Provider
	switch cfg.Provider {
k.siemer's avatar
k.siemer committed
149
	case "akamai":
150 151
		p = akamai.NewAkamaiProvider(
			akamai.AkamaiConfig{
k.siemer's avatar
k.siemer committed
152 153 154 155 156 157 158 159 160
				DomainFilter:          domainFilter,
				ZoneIDFilter:          zoneIDFilter,
				ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
				ClientToken:           cfg.AkamaiClientToken,
				ClientSecret:          cfg.AkamaiClientSecret,
				AccessToken:           cfg.AkamaiAccessToken,
				DryRun:                cfg.DryRun,
			},
		)
Li Yi's avatar
Li Yi committed
161
	case "alibabacloud":
162
		p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
163
	case "aws":
164 165
		p, err = aws.NewAWSProvider(
			aws.AWSConfig{
166 167 168
				DomainFilter:         domainFilter,
				ZoneIDFilter:         zoneIDFilter,
				ZoneTypeFilter:       zoneTypeFilter,
Cesar Wong's avatar
Cesar Wong committed
169
				ZoneTagFilter:        zoneTagFilter,
170 171 172 173
				BatchChangeSize:      cfg.AWSBatchChangeSize,
				BatchChangeInterval:  cfg.AWSBatchChangeInterval,
				EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
				AssumeRole:           cfg.AWSAssumeRole,
Corey O'Brien's avatar
Corey O'Brien committed
174
				APIRetries:           cfg.AWSAPIRetries,
175
				PreferCNAME:          cfg.AWSPreferCNAME,
176
				DryRun:               cfg.DryRun,
177 178
			},
		)
179 180
	case "aws-sd":
		// Check that only compatible Registry is used with AWS-SD
181
		if cfg.Registry != "noop" && cfg.Registry != "aws-sd" {
182
			log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
183
			cfg.Registry = "aws-sd"
184
		}
185
		p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
186
	case "azure-dns", "azure":
187
		p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
188
	case "azure-private-dns":
189
		p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
Dave Grizzanti's avatar
Dave Grizzanti committed
190
	case "vinyldns":
191
		p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
David Dymko's avatar
David Dymko committed
192
	case "vultr":
193
		p, err = vultr.NewVultrProvider(domainFilter, cfg.DryRun)
194
	case "cloudflare":
195
		p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
Dimitrij Klesev's avatar
Dimitrij Klesev committed
196
	case "rcodezero":
197
		p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
Dimitrij Klesev's avatar
Dimitrij Klesev committed
198
	case "google":
199
		p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
200
	case "digitalocean":
201
		p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
Vladimir Smagin's avatar
Vladimir Smagin committed
202 203
	case "hetzner":
		p, err = hetzner.NewHetznerProvider(ctx, domainFilter, cfg.DryRun)
Hugome's avatar
Hugome committed
204
	case "ovh":
Hugome's avatar
Hugome committed
205
		p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun)
cliedeman's avatar
cliedeman committed
206
	case "linode":
207
		p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
208
	case "dnsimple":
209
		p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
210
	case "infoblox":
211 212
		p, err = infoblox.NewInfobloxProvider(
			infoblox.InfobloxConfig{
213
				DomainFilter: domainFilter,
214
				ZoneIDFilter: zoneIDFilter,
215 216 217 218 219 220
				Host:         cfg.InfobloxGridHost,
				Port:         cfg.InfobloxWapiPort,
				Username:     cfg.InfobloxWapiUsername,
				Password:     cfg.InfobloxWapiPassword,
				Version:      cfg.InfobloxWapiVersion,
				SSLVerify:    cfg.InfobloxSSLVerify,
221
				View:         cfg.InfobloxView,
222
				MaxResults:   cfg.InfobloxMaxResults,
223 224 225
				DryRun:       cfg.DryRun,
			},
		)
Julian Vassev's avatar
Julian Vassev committed
226
	case "dyn":
227 228
		p, err = dyn.NewDynProvider(
			dyn.DynConfig{
229 230 231 232 233 234 235 236
				DomainFilter:  domainFilter,
				ZoneIDFilter:  zoneIDFilter,
				DryRun:        cfg.DryRun,
				CustomerName:  cfg.DynCustomerName,
				Username:      cfg.DynUsername,
				Password:      cfg.DynPassword,
				MinTTLSeconds: cfg.DynMinTTLSeconds,
				AppVersion:    externaldns.Version,
Julian Vassev's avatar
Julian Vassev committed
237 238
			},
		)
Stan Lagun's avatar
Stan Lagun committed
239
	case "coredns", "skydns":
240
		p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
Jason-ZW's avatar
Jason-ZW committed
241
	case "rdns":
242 243
		p, err = rdns.NewRDNSProvider(
			rdns.RDNSConfig{
Jason-ZW's avatar
Jason-ZW committed
244 245 246 247
				DomainFilter: domainFilter,
				DryRun:       cfg.DryRun,
			},
		)
248
	case "exoscale":
249
		p, err = exoscale.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, exoscale.ExoscaleWithDomain(domainFilter), exoscale.ExoscaleWithLogging()), nil
250
	case "inmemory":
251
		p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil
Stan Lagun's avatar
Stan Lagun committed
252
	case "designate":
253
		p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun)
Anhad Jai Singh's avatar
Anhad Jai Singh committed
254
	case "pdns":
255
		p, err = pdns.NewPDNSProvider(
256
			ctx,
257
			pdns.PDNSConfig{
Jason Hoch's avatar
Jason Hoch committed
258
				DomainFilter: domainFilter,
Jason Hoch's avatar
gofmt  
Jason Hoch committed
259 260 261
				DryRun:       cfg.DryRun,
				Server:       cfg.PDNSServer,
				APIKey:       cfg.PDNSAPIKey,
262
				TLSConfig: pdns.TLSConfig{
Jason Hoch's avatar
gofmt  
Jason Hoch committed
263 264 265
					TLSEnabled:            cfg.PDNSTLSEnabled,
					CAFilePath:            cfg.TLSCA,
					ClientCertFilePath:    cfg.TLSClientCert,
Jason Hoch's avatar
Jason Hoch committed
266 267 268 269
					ClientCertKeyFilePath: cfg.TLSClientCertKey,
				},
			},
		)
270
	case "oci":
271 272
		var config *oci.OCIConfig
		config, err = oci.LoadOCIConfig(cfg.OCIConfigFile)
Andrew Pryde's avatar
Andrew Pryde committed
273
		if err == nil {
274
			p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
Andrew Pryde's avatar
Andrew Pryde committed
275
		}
Vladislav Troinich's avatar
Vladislav Troinich committed
276
	case "rfc2136":
277
		p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
278
	case "ns1":
279 280
		p, err = ns1.NewNS1Provider(
			ns1.NS1Config{
281 282
				DomainFilter: domainFilter,
				ZoneIDFilter: zoneIDFilter,
283 284
				NS1Endpoint:  cfg.NS1Endpoint,
				NS1IgnoreSSL: cfg.NS1IgnoreSSL,
285
				DryRun:       cfg.DryRun,
286 287
			},
		)
Reinier Schoof's avatar
Reinier Schoof committed
288
	case "transip":
289
		p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
290
	default:
291
		log.Fatalf("unknown dns provider: %s", cfg.Provider)
292
	}
293 294 295 296
	if err != nil {
		log.Fatal(err)
	}

Yerken's avatar
Yerken committed
297 298 299 300 301
	var r registry.Registry
	switch cfg.Registry {
	case "noop":
		r, err = registry.NewNoopRegistry(p)
	case "txt":
302
		r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
303
	case "aws-sd":
304
		r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
Yerken's avatar
Yerken committed
305 306 307 308
	default:
		log.Fatalf("unknown registry: %s", cfg.Registry)
	}

Yerken's avatar
Yerken committed
309 310 311 312
	if err != nil {
		log.Fatal(err)
	}

313 314 315 316 317
	policy, exists := plan.Policies[cfg.Policy]
	if !exists {
		log.Fatalf("unknown policy: %s", cfg.Policy)
	}

318
	ctrl := controller.Controller{
319 320 321 322 323
		Source:       endpointsSource,
		Registry:     r,
		Policy:       policy,
		Interval:     cfg.Interval,
		DomainFilter: domainFilter,
324 325
	}

326
	if cfg.Once {
327
		err := ctrl.RunOnce(ctx)
328 329 330 331 332
		if err != nil {
			log.Fatal(err)
		}

		os.Exit(0)
333
	}
334 335 336 337 338 339 340 341 342 343

	if cfg.UpdateEvents {
		// Add RunOnce as the handler function that will be called when ingress/service sources have changed.
		// Note that k8s Informers will perform an initial list operation, which results in the handler
		// function initially being called for every Service/Ingress that exists
		ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
	}

	ctrl.ScheduleRunOnce(time.Now())
	ctrl.Run(ctx)
344 345
}

346
func handleSigterm(cancel func()) {
347 348 349
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGTERM)
	<-signals
Yerken's avatar
Yerken committed
350
	log.Info("Received SIGTERM. Terminating...")
351
	cancel()
352
}
353

354 355 356 357 358 359 360 361 362 363
func serveMetrics(address string) {
	http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	})

	http.Handle("/metrics", promhttp.Handler())

	log.Fatal(http.ListenAndServe(address, nil))
}