sigs.k8s.io/external-dns@v0.14.1/main.go (about)

     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 main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"os"
    24  	"os/signal"
    25  	"syscall"
    26  	"time"
    27  
    28  	awsSDK "github.com/aws/aws-sdk-go/aws"
    29  	"github.com/aws/aws-sdk-go/aws/session"
    30  	"github.com/aws/aws-sdk-go/service/dynamodb"
    31  	"github.com/aws/aws-sdk-go/service/route53"
    32  	sd "github.com/aws/aws-sdk-go/service/servicediscovery"
    33  	"github.com/go-logr/logr"
    34  	"github.com/prometheus/client_golang/prometheus/promhttp"
    35  	log "github.com/sirupsen/logrus"
    36  	"k8s.io/apimachinery/pkg/labels"
    37  	_ "k8s.io/client-go/plugin/pkg/client/auth"
    38  	"k8s.io/klog/v2"
    39  
    40  	"sigs.k8s.io/external-dns/controller"
    41  	"sigs.k8s.io/external-dns/endpoint"
    42  	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
    43  	"sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
    44  	"sigs.k8s.io/external-dns/plan"
    45  	"sigs.k8s.io/external-dns/provider"
    46  	"sigs.k8s.io/external-dns/provider/akamai"
    47  	"sigs.k8s.io/external-dns/provider/alibabacloud"
    48  	"sigs.k8s.io/external-dns/provider/aws"
    49  	"sigs.k8s.io/external-dns/provider/awssd"
    50  	"sigs.k8s.io/external-dns/provider/azure"
    51  	"sigs.k8s.io/external-dns/provider/bluecat"
    52  	"sigs.k8s.io/external-dns/provider/civo"
    53  	"sigs.k8s.io/external-dns/provider/cloudflare"
    54  	"sigs.k8s.io/external-dns/provider/coredns"
    55  	"sigs.k8s.io/external-dns/provider/designate"
    56  	"sigs.k8s.io/external-dns/provider/digitalocean"
    57  	"sigs.k8s.io/external-dns/provider/dnsimple"
    58  	"sigs.k8s.io/external-dns/provider/dyn"
    59  	"sigs.k8s.io/external-dns/provider/exoscale"
    60  	"sigs.k8s.io/external-dns/provider/gandi"
    61  	"sigs.k8s.io/external-dns/provider/godaddy"
    62  	"sigs.k8s.io/external-dns/provider/google"
    63  	"sigs.k8s.io/external-dns/provider/ibmcloud"
    64  	"sigs.k8s.io/external-dns/provider/infoblox"
    65  	"sigs.k8s.io/external-dns/provider/inmemory"
    66  	"sigs.k8s.io/external-dns/provider/linode"
    67  	"sigs.k8s.io/external-dns/provider/ns1"
    68  	"sigs.k8s.io/external-dns/provider/oci"
    69  	"sigs.k8s.io/external-dns/provider/ovh"
    70  	"sigs.k8s.io/external-dns/provider/pdns"
    71  	"sigs.k8s.io/external-dns/provider/pihole"
    72  	"sigs.k8s.io/external-dns/provider/plural"
    73  	"sigs.k8s.io/external-dns/provider/rcode0"
    74  	"sigs.k8s.io/external-dns/provider/rdns"
    75  	"sigs.k8s.io/external-dns/provider/rfc2136"
    76  	"sigs.k8s.io/external-dns/provider/safedns"
    77  	"sigs.k8s.io/external-dns/provider/scaleway"
    78  	"sigs.k8s.io/external-dns/provider/tencentcloud"
    79  	"sigs.k8s.io/external-dns/provider/transip"
    80  	"sigs.k8s.io/external-dns/provider/ultradns"
    81  	"sigs.k8s.io/external-dns/provider/vinyldns"
    82  	"sigs.k8s.io/external-dns/provider/vultr"
    83  	"sigs.k8s.io/external-dns/provider/webhook"
    84  	webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
    85  	"sigs.k8s.io/external-dns/registry"
    86  	"sigs.k8s.io/external-dns/source"
    87  )
    88  
    89  func main() {
    90  	cfg := externaldns.NewConfig()
    91  	if err := cfg.ParseFlags(os.Args[1:]); err != nil {
    92  		log.Fatalf("flag parsing error: %v", err)
    93  	}
    94  	if cfg.LogFormat == "json" {
    95  		log.SetFormatter(&log.JSONFormatter{})
    96  	}
    97  	log.Infof("config: %s", cfg)
    98  
    99  	if err := validation.ValidateConfig(cfg); err != nil {
   100  		log.Fatalf("config validation failed: %v", err)
   101  	}
   102  
   103  	if cfg.DryRun {
   104  		log.Info("running in dry-run mode. No changes to DNS records will be made.")
   105  	}
   106  
   107  	ll, err := log.ParseLevel(cfg.LogLevel)
   108  	if err != nil {
   109  		log.Fatalf("failed to parse log level: %v", err)
   110  	}
   111  	log.SetLevel(ll)
   112  
   113  	// Klog V2 is used by k8s.io/apimachinery/pkg/labels and can throw (a lot) of irrelevant logs
   114  	// See https://github.com/kubernetes-sigs/external-dns/issues/2348
   115  	defer klog.ClearLogger()
   116  	klog.SetLogger(logr.Discard())
   117  
   118  	ctx, cancel := context.WithCancel(context.Background())
   119  
   120  	go serveMetrics(cfg.MetricsAddress)
   121  	go handleSigterm(cancel)
   122  
   123  	// error is explicitly ignored because the filter is already validated in validation.ValidateConfig
   124  	labelSelector, _ := labels.Parse(cfg.LabelFilter)
   125  
   126  	// Create a source.Config from the flags passed by the user.
   127  	sourceCfg := &source.Config{
   128  		Namespace:                      cfg.Namespace,
   129  		AnnotationFilter:               cfg.AnnotationFilter,
   130  		LabelFilter:                    labelSelector,
   131  		IngressClassNames:              cfg.IngressClassNames,
   132  		FQDNTemplate:                   cfg.FQDNTemplate,
   133  		CombineFQDNAndAnnotation:       cfg.CombineFQDNAndAnnotation,
   134  		IgnoreHostnameAnnotation:       cfg.IgnoreHostnameAnnotation,
   135  		IgnoreIngressTLSSpec:           cfg.IgnoreIngressTLSSpec,
   136  		IgnoreIngressRulesSpec:         cfg.IgnoreIngressRulesSpec,
   137  		GatewayNamespace:               cfg.GatewayNamespace,
   138  		GatewayLabelFilter:             cfg.GatewayLabelFilter,
   139  		Compatibility:                  cfg.Compatibility,
   140  		PublishInternal:                cfg.PublishInternal,
   141  		PublishHostIP:                  cfg.PublishHostIP,
   142  		AlwaysPublishNotReadyAddresses: cfg.AlwaysPublishNotReadyAddresses,
   143  		ConnectorServer:                cfg.ConnectorSourceServer,
   144  		CRDSourceAPIVersion:            cfg.CRDSourceAPIVersion,
   145  		CRDSourceKind:                  cfg.CRDSourceKind,
   146  		KubeConfig:                     cfg.KubeConfig,
   147  		APIServerURL:                   cfg.APIServerURL,
   148  		ServiceTypeFilter:              cfg.ServiceTypeFilter,
   149  		CFAPIEndpoint:                  cfg.CFAPIEndpoint,
   150  		CFUsername:                     cfg.CFUsername,
   151  		CFPassword:                     cfg.CFPassword,
   152  		GlooNamespaces:                 cfg.GlooNamespaces,
   153  		SkipperRouteGroupVersion:       cfg.SkipperRouteGroupVersion,
   154  		RequestTimeout:                 cfg.RequestTimeout,
   155  		DefaultTargets:                 cfg.DefaultTargets,
   156  		OCPRouterName:                  cfg.OCPRouterName,
   157  		UpdateEvents:                   cfg.UpdateEvents,
   158  		ResolveLoadBalancerHostname:    cfg.ResolveServiceLoadBalancerHostname,
   159  		TraefikDisableLegacy:           cfg.TraefikDisableLegacy,
   160  		TraefikDisableNew:              cfg.TraefikDisableNew,
   161  	}
   162  
   163  	// Lookup all the selected sources by names and pass them the desired configuration.
   164  	sources, err := source.ByNames(ctx, &source.SingletonClientGenerator{
   165  		KubeConfig:   cfg.KubeConfig,
   166  		APIServerURL: cfg.APIServerURL,
   167  		// If update events are enabled, disable timeout.
   168  		RequestTimeout: func() time.Duration {
   169  			if cfg.UpdateEvents {
   170  				return 0
   171  			}
   172  			return cfg.RequestTimeout
   173  		}(),
   174  	}, cfg.Sources, sourceCfg)
   175  	if err != nil {
   176  		log.Fatal(err)
   177  	}
   178  
   179  	// Filter targets
   180  	targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets)
   181  
   182  	// Combine multiple sources into a single, deduplicated source.
   183  	endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets))
   184  	endpointsSource = source.NewTargetFilterSource(endpointsSource, targetFilter)
   185  
   186  	// RegexDomainFilter overrides DomainFilter
   187  	var domainFilter endpoint.DomainFilter
   188  	if cfg.RegexDomainFilter.String() != "" {
   189  		domainFilter = endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
   190  	} else {
   191  		domainFilter = endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
   192  	}
   193  	zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
   194  	zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
   195  	zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
   196  	zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
   197  
   198  	var awsSession *session.Session
   199  	if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" {
   200  		awsSession, err = aws.NewSession(
   201  			aws.AWSSessionConfig{
   202  				AssumeRole:           cfg.AWSAssumeRole,
   203  				AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
   204  				APIRetries:           cfg.AWSAPIRetries,
   205  			},
   206  		)
   207  		if err != nil {
   208  			log.Fatal(err)
   209  		}
   210  	}
   211  
   212  	var p provider.Provider
   213  	switch cfg.Provider {
   214  	case "akamai":
   215  		p, err = akamai.NewAkamaiProvider(
   216  			akamai.AkamaiConfig{
   217  				DomainFilter:          domainFilter,
   218  				ZoneIDFilter:          zoneIDFilter,
   219  				ServiceConsumerDomain: cfg.AkamaiServiceConsumerDomain,
   220  				ClientToken:           cfg.AkamaiClientToken,
   221  				ClientSecret:          cfg.AkamaiClientSecret,
   222  				AccessToken:           cfg.AkamaiAccessToken,
   223  				EdgercPath:            cfg.AkamaiEdgercPath,
   224  				EdgercSection:         cfg.AkamaiEdgercSection,
   225  				DryRun:                cfg.DryRun,
   226  			}, nil)
   227  	case "alibabacloud":
   228  		p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
   229  	case "aws":
   230  		p, err = aws.NewAWSProvider(
   231  			aws.AWSConfig{
   232  				DomainFilter:          domainFilter,
   233  				ZoneIDFilter:          zoneIDFilter,
   234  				ZoneTypeFilter:        zoneTypeFilter,
   235  				ZoneTagFilter:         zoneTagFilter,
   236  				ZoneMatchParent:       cfg.AWSZoneMatchParent,
   237  				BatchChangeSize:       cfg.AWSBatchChangeSize,
   238  				BatchChangeSizeBytes:  cfg.AWSBatchChangeSizeBytes,
   239  				BatchChangeSizeValues: cfg.AWSBatchChangeSizeValues,
   240  				BatchChangeInterval:   cfg.AWSBatchChangeInterval,
   241  				EvaluateTargetHealth:  cfg.AWSEvaluateTargetHealth,
   242  				PreferCNAME:           cfg.AWSPreferCNAME,
   243  				DryRun:                cfg.DryRun,
   244  				ZoneCacheDuration:     cfg.AWSZoneCacheDuration,
   245  			},
   246  			route53.New(awsSession),
   247  		)
   248  	case "aws-sd":
   249  		// Check that only compatible Registry is used with AWS-SD
   250  		if cfg.Registry != "noop" && cfg.Registry != "aws-sd" {
   251  			log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
   252  			cfg.Registry = "aws-sd"
   253  		}
   254  		p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(awsSession))
   255  	case "azure-dns", "azure":
   256  		p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
   257  	case "azure-private-dns":
   258  		p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
   259  	case "bluecat":
   260  		p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, cfg.TXTPrefix, cfg.TXTSuffix, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify)
   261  	case "vinyldns":
   262  		p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
   263  	case "vultr":
   264  		p, err = vultr.NewVultrProvider(ctx, domainFilter, cfg.DryRun)
   265  	case "ultradns":
   266  		p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
   267  	case "civo":
   268  		p, err = civo.NewCivoProvider(domainFilter, cfg.DryRun)
   269  	case "cloudflare":
   270  		p, err = cloudflare.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun, cfg.CloudflareDNSRecordsPerPage)
   271  	case "rcodezero":
   272  		p, err = rcode0.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
   273  	case "google":
   274  		p, err = google.NewGoogleProvider(ctx, cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.GoogleBatchChangeSize, cfg.GoogleBatchChangeInterval, cfg.GoogleZoneVisibility, cfg.DryRun)
   275  	case "digitalocean":
   276  		p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
   277  	case "ovh":
   278  		p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun)
   279  	case "linode":
   280  		p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
   281  	case "dnsimple":
   282  		p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
   283  	case "infoblox":
   284  		p, err = infoblox.NewInfobloxProvider(
   285  			infoblox.StartupConfig{
   286  				DomainFilter:  domainFilter,
   287  				ZoneIDFilter:  zoneIDFilter,
   288  				Host:          cfg.InfobloxGridHost,
   289  				Port:          cfg.InfobloxWapiPort,
   290  				Username:      cfg.InfobloxWapiUsername,
   291  				Password:      cfg.InfobloxWapiPassword,
   292  				Version:       cfg.InfobloxWapiVersion,
   293  				SSLVerify:     cfg.InfobloxSSLVerify,
   294  				View:          cfg.InfobloxView,
   295  				MaxResults:    cfg.InfobloxMaxResults,
   296  				DryRun:        cfg.DryRun,
   297  				FQDNRegEx:     cfg.InfobloxFQDNRegEx,
   298  				NameRegEx:     cfg.InfobloxNameRegEx,
   299  				CreatePTR:     cfg.InfobloxCreatePTR,
   300  				CacheDuration: cfg.InfobloxCacheDuration,
   301  			},
   302  		)
   303  	case "dyn":
   304  		p, err = dyn.NewDynProvider(
   305  			dyn.DynConfig{
   306  				DomainFilter:  domainFilter,
   307  				ZoneIDFilter:  zoneIDFilter,
   308  				DryRun:        cfg.DryRun,
   309  				CustomerName:  cfg.DynCustomerName,
   310  				Username:      cfg.DynUsername,
   311  				Password:      cfg.DynPassword,
   312  				MinTTLSeconds: cfg.DynMinTTLSeconds,
   313  				AppVersion:    externaldns.Version,
   314  			},
   315  		)
   316  	case "coredns", "skydns":
   317  		p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
   318  	case "rdns":
   319  		p, err = rdns.NewRDNSProvider(
   320  			rdns.RDNSConfig{
   321  				DomainFilter: domainFilter,
   322  				DryRun:       cfg.DryRun,
   323  			},
   324  		)
   325  	case "exoscale":
   326  		p, err = exoscale.NewExoscaleProvider(
   327  			cfg.ExoscaleAPIEnvironment,
   328  			cfg.ExoscaleAPIZone,
   329  			cfg.ExoscaleAPIKey,
   330  			cfg.ExoscaleAPISecret,
   331  			cfg.DryRun,
   332  			exoscale.ExoscaleWithDomain(domainFilter),
   333  			exoscale.ExoscaleWithLogging(),
   334  		)
   335  	case "inmemory":
   336  		p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil
   337  	case "designate":
   338  		p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun)
   339  	case "pdns":
   340  		p, err = pdns.NewPDNSProvider(
   341  			ctx,
   342  			pdns.PDNSConfig{
   343  				DomainFilter: domainFilter,
   344  				DryRun:       cfg.DryRun,
   345  				Server:       cfg.PDNSServer,
   346  				APIKey:       cfg.PDNSAPIKey,
   347  				TLSConfig: pdns.TLSConfig{
   348  					SkipTLSVerify:         cfg.PDNSSkipTLSVerify,
   349  					CAFilePath:            cfg.TLSCA,
   350  					ClientCertFilePath:    cfg.TLSClientCert,
   351  					ClientCertKeyFilePath: cfg.TLSClientCertKey,
   352  				},
   353  			},
   354  		)
   355  	case "oci":
   356  		var config *oci.OCIConfig
   357  		// if the instance-principals flag was set, and a compartment OCID was provided, then ignore the
   358  		// OCI config file, and provide a config that uses instance principal authentication.
   359  		if cfg.OCIAuthInstancePrincipal {
   360  			if len(cfg.OCICompartmentOCID) == 0 {
   361  				err = fmt.Errorf("instance principal authentication requested, but no compartment OCID provided")
   362  			} else {
   363  				authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true}
   364  				config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID}
   365  			}
   366  		} else {
   367  			config, err = oci.LoadOCIConfig(cfg.OCIConfigFile)
   368  		}
   369  		config.ZoneCacheDuration = cfg.OCIZoneCacheDuration
   370  		if err == nil {
   371  			p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.OCIZoneScope, cfg.DryRun)
   372  		}
   373  	case "rfc2136":
   374  		tlsConfig := rfc2136.TLSConfig{
   375  			UseTLS:                cfg.RFC2136UseTLS,
   376  			SkipTLSVerify:         cfg.RFC2136SkipTLSVerify,
   377  			CAFilePath:            cfg.TLSCA,
   378  			ClientCertFilePath:    cfg.TLSClientCert,
   379  			ClientCertKeyFilePath: cfg.TLSClientCertKey,
   380  			ServerName:            "",
   381  		}
   382  		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, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, tlsConfig, nil)
   383  	case "ns1":
   384  		p, err = ns1.NewNS1Provider(
   385  			ns1.NS1Config{
   386  				DomainFilter:  domainFilter,
   387  				ZoneIDFilter:  zoneIDFilter,
   388  				NS1Endpoint:   cfg.NS1Endpoint,
   389  				NS1IgnoreSSL:  cfg.NS1IgnoreSSL,
   390  				DryRun:        cfg.DryRun,
   391  				MinTTLSeconds: cfg.NS1MinTTLSeconds,
   392  			},
   393  		)
   394  	case "transip":
   395  		p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
   396  	case "scaleway":
   397  		p, err = scaleway.NewScalewayProvider(ctx, domainFilter, cfg.DryRun)
   398  	case "godaddy":
   399  		p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
   400  	case "gandi":
   401  		p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun)
   402  	case "pihole":
   403  		p, err = pihole.NewPiholeProvider(
   404  			pihole.PiholeConfig{
   405  				Server:                cfg.PiholeServer,
   406  				Password:              cfg.PiholePassword,
   407  				TLSInsecureSkipVerify: cfg.PiholeTLSInsecureSkipVerify,
   408  				DomainFilter:          domainFilter,
   409  				DryRun:                cfg.DryRun,
   410  			},
   411  		)
   412  	case "ibmcloud":
   413  		p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun)
   414  	case "safedns":
   415  		p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun)
   416  	case "plural":
   417  		p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider)
   418  	case "tencentcloud":
   419  		p, err = tencentcloud.NewTencentCloudProvider(domainFilter, zoneIDFilter, cfg.TencentCloudConfigFile, cfg.TencentCloudZoneType, cfg.DryRun)
   420  	case "webhook":
   421  		p, err = webhook.NewWebhookProvider(cfg.WebhookProviderURL)
   422  	default:
   423  		log.Fatalf("unknown dns provider: %s", cfg.Provider)
   424  	}
   425  	if err != nil {
   426  		log.Fatal(err)
   427  	}
   428  
   429  	if cfg.WebhookServer {
   430  		webhookapi.StartHTTPApi(p, nil, cfg.WebhookProviderReadTimeout, cfg.WebhookProviderWriteTimeout, "127.0.0.1:8888")
   431  		os.Exit(0)
   432  	}
   433  
   434  	var r registry.Registry
   435  	switch cfg.Registry {
   436  	case "dynamodb":
   437  		config := awsSDK.NewConfig()
   438  		if cfg.AWSDynamoDBRegion != "" {
   439  			config = config.WithRegion(cfg.AWSDynamoDBRegion)
   440  		}
   441  		r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(awsSession, config), cfg.AWSDynamoDBTable, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, []byte(cfg.TXTEncryptAESKey), cfg.TXTCacheInterval)
   442  	case "noop":
   443  		r, err = registry.NewNoopRegistry(p)
   444  	case "txt":
   445  		r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey))
   446  	case "aws-sd":
   447  		r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
   448  	default:
   449  		log.Fatalf("unknown registry: %s", cfg.Registry)
   450  	}
   451  
   452  	if err != nil {
   453  		log.Fatal(err)
   454  	}
   455  
   456  	policy, exists := plan.Policies[cfg.Policy]
   457  	if !exists {
   458  		log.Fatalf("unknown policy: %s", cfg.Policy)
   459  	}
   460  
   461  	ctrl := controller.Controller{
   462  		Source:               endpointsSource,
   463  		Registry:             r,
   464  		Policy:               policy,
   465  		Interval:             cfg.Interval,
   466  		DomainFilter:         domainFilter,
   467  		ManagedRecordTypes:   cfg.ManagedDNSRecordTypes,
   468  		ExcludeRecordTypes:   cfg.ExcludeDNSRecordTypes,
   469  		MinEventSyncInterval: cfg.MinEventSyncInterval,
   470  	}
   471  
   472  	if cfg.Once {
   473  		err := ctrl.RunOnce(ctx)
   474  		if err != nil {
   475  			log.Fatal(err)
   476  		}
   477  
   478  		os.Exit(0)
   479  	}
   480  
   481  	if cfg.UpdateEvents {
   482  		// Add RunOnce as the handler function that will be called when ingress/service sources have changed.
   483  		// Note that k8s Informers will perform an initial list operation, which results in the handler
   484  		// function initially being called for every Service/Ingress that exists
   485  		ctrl.Source.AddEventHandler(ctx, func() { ctrl.ScheduleRunOnce(time.Now()) })
   486  	}
   487  
   488  	ctrl.ScheduleRunOnce(time.Now())
   489  	ctrl.Run(ctx)
   490  }
   491  
   492  func handleSigterm(cancel func()) {
   493  	signals := make(chan os.Signal, 1)
   494  	signal.Notify(signals, syscall.SIGTERM)
   495  	<-signals
   496  	log.Info("Received SIGTERM. Terminating...")
   497  	cancel()
   498  }
   499  
   500  func serveMetrics(address string) {
   501  	http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
   502  		w.WriteHeader(http.StatusOK)
   503  		w.Write([]byte("OK"))
   504  	})
   505  
   506  	http.Handle("/metrics", promhttp.Handler())
   507  
   508  	log.Fatal(http.ListenAndServe(address, nil))
   509  }