main.go 11 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
	"sigs.k8s.io/external-dns/controller"
	"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"
39 40
)

41
func main() {
42
	cfg := externaldns.NewConfig()
43
	if err := cfg.ParseFlags(os.Args[1:]); err != nil {
ideahitme's avatar
ideahitme committed
44 45
		log.Fatalf("flag parsing error: %v", err)
	}
jvassev's avatar
jvassev committed
46
	log.Infof("config: %s", cfg)
Henning Jacobs's avatar
Henning Jacobs committed
47

48
	if err := validation.ValidateConfig(cfg); err != nil {
49
		log.Fatalf("config validation failed: %v", err)
50 51
	}

ideahitme's avatar
ideahitme committed
52
	if cfg.LogFormat == "json" {
53 54
		log.SetFormatter(&log.JSONFormatter{})
	}
55
	if cfg.DryRun {
ideahitme's avatar
ideahitme committed
56
		log.Info("running in dry-run mode. No changes to DNS records will be made.")
57
	}
58 59 60 61

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

65 66
	ctx := context.Background()

67 68
	stopChan := make(chan struct{}, 1)

69
	go serveMetrics(cfg.MetricsAddress)
70 71
	go handleSigterm(stopChan)

72 73
	// Create a source.Config from the flags passed by the user.
	sourceCfg := &source.Config{
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
		Namespace:                   cfg.Namespace,
		AnnotationFilter:            cfg.AnnotationFilter,
		FQDNTemplate:                cfg.FQDNTemplate,
		CombineFQDNAndAnnotation:    cfg.CombineFQDNAndAnnotation,
		IgnoreHostnameAnnotation:    cfg.IgnoreHostnameAnnotation,
		Compatibility:               cfg.Compatibility,
		PublishInternal:             cfg.PublishInternal,
		PublishHostIP:               cfg.PublishHostIP,
		ConnectorServer:             cfg.ConnectorSourceServer,
		CRDSourceAPIVersion:         cfg.CRDSourceAPIVersion,
		CRDSourceKind:               cfg.CRDSourceKind,
		KubeConfig:                  cfg.KubeConfig,
		KubeMaster:                  cfg.Master,
		ServiceTypeFilter:           cfg.ServiceTypeFilter,
		IstioIngressGatewayServices: cfg.IstioIngressGatewayServices,
Dave Grizzanti's avatar
Dave Grizzanti committed
89 90 91
		CFAPIEndpoint:               cfg.CFAPIEndpoint,
		CFUsername:                  cfg.CFUsername,
		CFPassword:                  cfg.CFPassword,
92
		ContourLoadBalancerService:  cfg.ContourLoadBalancerService,
93
	}
94

95 96
	// Lookup all the selected sources by names and pass them the desired configuration.
	sources, err := source.ByNames(&source.SingletonClientGenerator{
97 98 99 100 101 102 103 104 105
		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
		}(),
106
	}, cfg.Sources, sourceCfg)
107 108 109 110
	if err != nil {
		log.Fatal(err)
	}

111
	// Combine multiple sources into a single, deduplicated source.
112
	endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
113

114
	domainFilter := provider.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
115
	zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
116
	zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
Cesar Wong's avatar
Cesar Wong committed
117
	zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
118

119 120
	var p provider.Provider
	switch cfg.Provider {
k.siemer's avatar
k.siemer committed
121 122 123 124 125 126 127 128 129 130 131 132
	case "akamai":
		p = provider.NewAkamaiProvider(
			provider.AkamaiConfig{
				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
133 134
	case "alibabacloud":
		p, err = provider.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
135
	case "aws":
136 137
		p, err = provider.NewAWSProvider(
			provider.AWSConfig{
138 139 140
				DomainFilter:         domainFilter,
				ZoneIDFilter:         zoneIDFilter,
				ZoneTypeFilter:       zoneTypeFilter,
Cesar Wong's avatar
Cesar Wong committed
141
				ZoneTagFilter:        zoneTagFilter,
142 143 144 145
				BatchChangeSize:      cfg.AWSBatchChangeSize,
				BatchChangeInterval:  cfg.AWSBatchChangeInterval,
				EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
				AssumeRole:           cfg.AWSAssumeRole,
Corey O'Brien's avatar
Corey O'Brien committed
146
				APIRetries:           cfg.AWSAPIRetries,
147
				PreferCNAME:          cfg.AWSPreferCNAME,
148
				DryRun:               cfg.DryRun,
149 150
			},
		)
151 152
	case "aws-sd":
		// Check that only compatible Registry is used with AWS-SD
153
		if cfg.Registry != "noop" && cfg.Registry != "aws-sd" {
154
			log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
155
			cfg.Registry = "aws-sd"
156
		}
157
		p, err = provider.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
158
	case "azure-dns", "azure":
159
		p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
160 161
	case "azure-private-dns":
		p, err = provider.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
Dave Grizzanti's avatar
Dave Grizzanti committed
162
	case "vinyldns":
Dave Grizzanti's avatar
Dave Grizzanti committed
163
		p, err = provider.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
164
	case "cloudflare":
njuettner's avatar
njuettner committed
165
		p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
Dimitrij Klesev's avatar
Dimitrij Klesev committed
166 167 168
	case "rcodezero":
		p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
	case "google":
169
		p, err = provider.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.DryRun)
170
	case "digitalocean":
171
		p, err = provider.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun)
cliedeman's avatar
cliedeman committed
172
	case "linode":
173
		p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
174
	case "dnsimple":
175
		p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
176 177 178 179
	case "infoblox":
		p, err = provider.NewInfobloxProvider(
			provider.InfobloxConfig{
				DomainFilter: domainFilter,
180
				ZoneIDFilter: zoneIDFilter,
181 182 183 184 185 186
				Host:         cfg.InfobloxGridHost,
				Port:         cfg.InfobloxWapiPort,
				Username:     cfg.InfobloxWapiUsername,
				Password:     cfg.InfobloxWapiPassword,
				Version:      cfg.InfobloxWapiVersion,
				SSLVerify:    cfg.InfobloxSSLVerify,
187
				View:         cfg.InfobloxView,
188
				MaxResults:   cfg.InfobloxMaxResults,
189 190 191
				DryRun:       cfg.DryRun,
			},
		)
Julian Vassev's avatar
Julian Vassev committed
192 193 194
	case "dyn":
		p, err = provider.NewDynProvider(
			provider.DynConfig{
195 196 197 198 199 200 201 202
				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
203 204
			},
		)
Stan Lagun's avatar
Stan Lagun committed
205
	case "coredns", "skydns":
206
		p, err = provider.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
Jason-ZW's avatar
Jason-ZW committed
207 208 209 210 211 212 213
	case "rdns":
		p, err = provider.NewRDNSProvider(
			provider.RDNSConfig{
				DomainFilter: domainFilter,
				DryRun:       cfg.DryRun,
			},
		)
214
	case "exoscale":
Christopher Schmidt's avatar
Christopher Schmidt committed
215
		p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil
216
	case "inmemory":
Anhad Jai Singh's avatar
Anhad Jai Singh committed
217
		p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
Stan Lagun's avatar
Stan Lagun committed
218 219
	case "designate":
		p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
Anhad Jai Singh's avatar
Anhad Jai Singh committed
220
	case "pdns":
Jason Hoch's avatar
Jason Hoch committed
221
		p, err = provider.NewPDNSProvider(
222
			ctx,
Jason Hoch's avatar
Jason Hoch committed
223 224
			provider.PDNSConfig{
				DomainFilter: domainFilter,
Jason Hoch's avatar
gofmt  
Jason Hoch committed
225 226 227
				DryRun:       cfg.DryRun,
				Server:       cfg.PDNSServer,
				APIKey:       cfg.PDNSAPIKey,
Jason Hoch's avatar
Jason Hoch committed
228
				TLSConfig: provider.TLSConfig{
Jason Hoch's avatar
gofmt  
Jason Hoch committed
229 230 231
					TLSEnabled:            cfg.PDNSTLSEnabled,
					CAFilePath:            cfg.TLSCA,
					ClientCertFilePath:    cfg.TLSClientCert,
Jason Hoch's avatar
Jason Hoch committed
232 233 234 235
					ClientCertKeyFilePath: cfg.TLSClientCertKey,
				},
			},
		)
236
	case "oci":
Andrew Pryde's avatar
Andrew Pryde committed
237 238 239 240 241
		var config *provider.OCIConfig
		config, err = provider.LoadOCIConfig(cfg.OCIConfigFile)
		if err == nil {
			p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
		}
Vladislav Troinich's avatar
Vladislav Troinich committed
242
	case "rfc2136":
243
		p, err = provider.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
244 245 246
	case "ns1":
		p, err = provider.NewNS1Provider(
			provider.NS1Config{
247 248
				DomainFilter: domainFilter,
				ZoneIDFilter: zoneIDFilter,
249 250
				NS1Endpoint:  cfg.NS1Endpoint,
				NS1IgnoreSSL: cfg.NS1IgnoreSSL,
251
				DryRun:       cfg.DryRun,
252 253
			},
		)
Reinier Schoof's avatar
Reinier Schoof committed
254 255
	case "transip":
		p, err = provider.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
256
	default:
257
		log.Fatalf("unknown dns provider: %s", cfg.Provider)
258
	}
259 260 261 262
	if err != nil {
		log.Fatal(err)
	}

Yerken's avatar
Yerken committed
263 264 265 266 267
	var r registry.Registry
	switch cfg.Registry {
	case "noop":
		r, err = registry.NewNoopRegistry(p)
	case "txt":
268
		r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
269
	case "aws-sd":
270
		r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID)
Yerken's avatar
Yerken committed
271 272 273 274
	default:
		log.Fatalf("unknown registry: %s", cfg.Registry)
	}

Yerken's avatar
Yerken committed
275 276 277 278
	if err != nil {
		log.Fatal(err)
	}

279 280 281 282 283
	policy, exists := plan.Policies[cfg.Policy]
	if !exists {
		log.Fatalf("unknown policy: %s", cfg.Policy)
	}

284
	ctrl := controller.Controller{
285
		Source:   endpointsSource,
Yerken's avatar
Yerken committed
286
		Registry: r,
287
		Policy:   policy,
288
		Interval: cfg.Interval,
289 290
	}

291 292 293 294 295 296 297
	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 limted by minInterval.
		ctrl.Source.AddEventHandler(func() error { return ctrl.RunOnce(ctx) }, stopChan, 1*time.Minute)
	}

298
	if cfg.Once {
299
		err := ctrl.RunOnce(ctx)
300 301 302 303 304
		if err != nil {
			log.Fatal(err)
		}

		os.Exit(0)
305
	}
306
	ctrl.Run(ctx, stopChan)
307 308 309 310 311 312
}

func handleSigterm(stopChan chan struct{}) {
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGTERM)
	<-signals
Yerken's avatar
Yerken committed
313
	log.Info("Received SIGTERM. Terminating...")
314 315
	close(stopChan)
}
316

317 318 319 320 321 322 323 324 325 326
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))
}