sigs.k8s.io/external-dns@v0.14.1/source/store.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 source
    18  
    19  import (
    20  	"context"
    21  	"net/http"
    22  	"os"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/cloudfoundry-community/go-cfclient"
    28  	"github.com/linki/instrumented_http"
    29  	openshift "github.com/openshift/client-go/route/clientset/versioned"
    30  	"github.com/pkg/errors"
    31  	log "github.com/sirupsen/logrus"
    32  	istioclient "istio.io/client-go/pkg/clientset/versioned"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/client-go/dynamic"
    35  	"k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/rest"
    37  	"k8s.io/client-go/tools/clientcmd"
    38  	gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
    39  )
    40  
    41  // ErrSourceNotFound is returned when a requested source doesn't exist.
    42  var ErrSourceNotFound = errors.New("source not found")
    43  
    44  // Config holds shared configuration options for all Sources.
    45  type Config struct {
    46  	Namespace                      string
    47  	AnnotationFilter               string
    48  	LabelFilter                    labels.Selector
    49  	IngressClassNames              []string
    50  	FQDNTemplate                   string
    51  	CombineFQDNAndAnnotation       bool
    52  	IgnoreHostnameAnnotation       bool
    53  	IgnoreIngressTLSSpec           bool
    54  	IgnoreIngressRulesSpec         bool
    55  	GatewayNamespace               string
    56  	GatewayLabelFilter             string
    57  	Compatibility                  string
    58  	PublishInternal                bool
    59  	PublishHostIP                  bool
    60  	AlwaysPublishNotReadyAddresses bool
    61  	ConnectorServer                string
    62  	CRDSourceAPIVersion            string
    63  	CRDSourceKind                  string
    64  	KubeConfig                     string
    65  	APIServerURL                   string
    66  	ServiceTypeFilter              []string
    67  	CFAPIEndpoint                  string
    68  	CFUsername                     string
    69  	CFPassword                     string
    70  	GlooNamespaces                 []string
    71  	SkipperRouteGroupVersion       string
    72  	RequestTimeout                 time.Duration
    73  	DefaultTargets                 []string
    74  	OCPRouterName                  string
    75  	UpdateEvents                   bool
    76  	ResolveLoadBalancerHostname    bool
    77  	TraefikDisableLegacy           bool
    78  	TraefikDisableNew              bool
    79  }
    80  
    81  // ClientGenerator provides clients
    82  type ClientGenerator interface {
    83  	KubeClient() (kubernetes.Interface, error)
    84  	GatewayClient() (gateway.Interface, error)
    85  	IstioClient() (istioclient.Interface, error)
    86  	CloudFoundryClient(cfAPPEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error)
    87  	DynamicKubernetesClient() (dynamic.Interface, error)
    88  	OpenShiftClient() (openshift.Interface, error)
    89  }
    90  
    91  // SingletonClientGenerator stores provider clients and guarantees that only one instance of client
    92  // will be generated
    93  type SingletonClientGenerator struct {
    94  	KubeConfig      string
    95  	APIServerURL    string
    96  	RequestTimeout  time.Duration
    97  	kubeClient      kubernetes.Interface
    98  	gatewayClient   gateway.Interface
    99  	istioClient     *istioclient.Clientset
   100  	cfClient        *cfclient.Client
   101  	dynKubeClient   dynamic.Interface
   102  	openshiftClient openshift.Interface
   103  	kubeOnce        sync.Once
   104  	gatewayOnce     sync.Once
   105  	istioOnce       sync.Once
   106  	cfOnce          sync.Once
   107  	dynCliOnce      sync.Once
   108  	openshiftOnce   sync.Once
   109  }
   110  
   111  // KubeClient generates a kube client if it was not created before
   112  func (p *SingletonClientGenerator) KubeClient() (kubernetes.Interface, error) {
   113  	var err error
   114  	p.kubeOnce.Do(func() {
   115  		p.kubeClient, err = NewKubeClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
   116  	})
   117  	return p.kubeClient, err
   118  }
   119  
   120  // GatewayClient generates a gateway client if it was not created before
   121  func (p *SingletonClientGenerator) GatewayClient() (gateway.Interface, error) {
   122  	var err error
   123  	p.gatewayOnce.Do(func() {
   124  		p.gatewayClient, err = newGatewayClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
   125  	})
   126  	return p.gatewayClient, err
   127  }
   128  
   129  func newGatewayClient(kubeConfig, apiServerURL string, requestTimeout time.Duration) (gateway.Interface, error) {
   130  	config, err := instrumentedRESTConfig(kubeConfig, apiServerURL, requestTimeout)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	client, err := gateway.NewForConfig(config)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	log.Infof("Created GatewayAPI client %s", config.Host)
   139  	return client, nil
   140  }
   141  
   142  // IstioClient generates an istio go client if it was not created before
   143  func (p *SingletonClientGenerator) IstioClient() (istioclient.Interface, error) {
   144  	var err error
   145  	p.istioOnce.Do(func() {
   146  		p.istioClient, err = NewIstioClient(p.KubeConfig, p.APIServerURL)
   147  	})
   148  	return p.istioClient, err
   149  }
   150  
   151  // CloudFoundryClient generates a cf client if it was not created before
   152  func (p *SingletonClientGenerator) CloudFoundryClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) {
   153  	var err error
   154  	p.cfOnce.Do(func() {
   155  		p.cfClient, err = NewCFClient(cfAPIEndpoint, cfUsername, cfPassword)
   156  	})
   157  	return p.cfClient, err
   158  }
   159  
   160  // NewCFClient return a new CF client object.
   161  func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) {
   162  	c := &cfclient.Config{
   163  		ApiAddress: "https://" + cfAPIEndpoint,
   164  		Username:   cfUsername,
   165  		Password:   cfPassword,
   166  	}
   167  	client, err := cfclient.NewClient(c)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return client, nil
   173  }
   174  
   175  // DynamicKubernetesClient generates a dynamic client if it was not created before
   176  func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) {
   177  	var err error
   178  	p.dynCliOnce.Do(func() {
   179  		p.dynKubeClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
   180  	})
   181  	return p.dynKubeClient, err
   182  }
   183  
   184  // OpenShiftClient generates an openshift client if it was not created before
   185  func (p *SingletonClientGenerator) OpenShiftClient() (openshift.Interface, error) {
   186  	var err error
   187  	p.openshiftOnce.Do(func() {
   188  		p.openshiftClient, err = NewOpenShiftClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
   189  	})
   190  	return p.openshiftClient, err
   191  }
   192  
   193  // ByNames returns multiple Sources given multiple names.
   194  func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config) ([]Source, error) {
   195  	sources := []Source{}
   196  	for _, name := range names {
   197  		source, err := BuildWithConfig(ctx, name, p, cfg)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		sources = append(sources, source)
   202  	}
   203  
   204  	return sources, nil
   205  }
   206  
   207  // BuildWithConfig allows to generate a Source implementation from the shared config
   208  func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) {
   209  	switch source {
   210  	case "node":
   211  		client, err := p.KubeClient()
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter)
   216  	case "service":
   217  		client, err := p.KubeClient()
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname)
   222  	case "ingress":
   223  		client, err := p.KubeClient()
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames)
   228  	case "pod":
   229  		client, err := p.KubeClient()
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  		return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility)
   234  	case "gateway-httproute":
   235  		return NewGatewayHTTPRouteSource(p, cfg)
   236  	case "gateway-grpcroute":
   237  		return NewGatewayGRPCRouteSource(p, cfg)
   238  	case "gateway-tlsroute":
   239  		return NewGatewayTLSRouteSource(p, cfg)
   240  	case "gateway-tcproute":
   241  		return NewGatewayTCPRouteSource(p, cfg)
   242  	case "gateway-udproute":
   243  		return NewGatewayUDPRouteSource(p, cfg)
   244  	case "istio-gateway":
   245  		kubernetesClient, err := p.KubeClient()
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  		istioClient, err := p.IstioClient()
   250  		if err != nil {
   251  			return nil, err
   252  		}
   253  		return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
   254  	case "istio-virtualservice":
   255  		kubernetesClient, err := p.KubeClient()
   256  		if err != nil {
   257  			return nil, err
   258  		}
   259  		istioClient, err := p.IstioClient()
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  		return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
   264  	case "cloudfoundry":
   265  		cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  		return NewCloudFoundrySource(cfClient)
   270  	case "ambassador-host":
   271  		kubernetesClient, err := p.KubeClient()
   272  		if err != nil {
   273  			return nil, err
   274  		}
   275  		dynamicClient, err := p.DynamicKubernetesClient()
   276  		if err != nil {
   277  			return nil, err
   278  		}
   279  		return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace)
   280  	case "contour-httpproxy":
   281  		dynamicClient, err := p.DynamicKubernetesClient()
   282  		if err != nil {
   283  			return nil, err
   284  		}
   285  		return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
   286  	case "gloo-proxy":
   287  		kubernetesClient, err := p.KubeClient()
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  		dynamicClient, err := p.DynamicKubernetesClient()
   292  		if err != nil {
   293  			return nil, err
   294  		}
   295  		return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces)
   296  	case "traefik-proxy":
   297  		kubernetesClient, err := p.KubeClient()
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  		dynamicClient, err := p.DynamicKubernetesClient()
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew)
   306  	case "openshift-route":
   307  		ocpClient, err := p.OpenShiftClient()
   308  		if err != nil {
   309  			return nil, err
   310  		}
   311  		return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName)
   312  	case "fake":
   313  		return NewFakeSource(cfg.FQDNTemplate)
   314  	case "connector":
   315  		return NewConnectorSource(cfg.ConnectorServer)
   316  	case "crd":
   317  		client, err := p.KubeClient()
   318  		if err != nil {
   319  			return nil, err
   320  		}
   321  		crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind)
   322  		if err != nil {
   323  			return nil, err
   324  		}
   325  		return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents)
   326  	case "skipper-routegroup":
   327  		apiServerURL := cfg.APIServerURL
   328  		tokenPath := ""
   329  		token := ""
   330  		restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.APIServerURL)
   331  		if err == nil {
   332  			apiServerURL = restConfig.Host
   333  			tokenPath = restConfig.BearerTokenFile
   334  			token = restConfig.BearerToken
   335  		}
   336  		return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
   337  	case "kong-tcpingress":
   338  		kubernetesClient, err := p.KubeClient()
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  		dynamicClient, err := p.DynamicKubernetesClient()
   343  		if err != nil {
   344  			return nil, err
   345  		}
   346  		return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation)
   347  	case "f5-virtualserver":
   348  		kubernetesClient, err := p.KubeClient()
   349  		if err != nil {
   350  			return nil, err
   351  		}
   352  		dynamicClient, err := p.DynamicKubernetesClient()
   353  		if err != nil {
   354  			return nil, err
   355  		}
   356  		return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter)
   357  	}
   358  
   359  	return nil, ErrSourceNotFound
   360  }
   361  
   362  func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time.Duration) (*rest.Config, error) {
   363  	config, err := GetRestConfig(kubeConfig, apiServerURL)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
   368  		return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{
   369  			PathProcessor: func(path string) string {
   370  				parts := strings.Split(path, "/")
   371  				return parts[len(parts)-1]
   372  			},
   373  		})
   374  	}
   375  	config.Timeout = requestTimeout
   376  	return config, nil
   377  }
   378  
   379  // GetRestConfig returns the rest clients config to get automatically
   380  // data if you run inside a cluster or by passing flags.
   381  func GetRestConfig(kubeConfig, apiServerURL string) (*rest.Config, error) {
   382  	if kubeConfig == "" {
   383  		if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
   384  			kubeConfig = clientcmd.RecommendedHomeFile
   385  		}
   386  	}
   387  	log.Debugf("apiServerURL: %s", apiServerURL)
   388  	log.Debugf("kubeConfig: %s", kubeConfig)
   389  
   390  	// evaluate whether to use kubeConfig-file or serviceaccount-token
   391  	var (
   392  		config *rest.Config
   393  		err    error
   394  	)
   395  	if kubeConfig == "" {
   396  		log.Infof("Using inCluster-config based on serviceaccount-token")
   397  		config, err = rest.InClusterConfig()
   398  	} else {
   399  		log.Infof("Using kubeConfig")
   400  		config, err = clientcmd.BuildConfigFromFlags(apiServerURL, kubeConfig)
   401  	}
   402  	if err != nil {
   403  		return nil, err
   404  	}
   405  
   406  	return config, nil
   407  }
   408  
   409  // NewKubeClient returns a new Kubernetes client object. It takes a Config and
   410  // uses APIServerURL and KubeConfig attributes to connect to the cluster. If
   411  // KubeConfig isn't provided it defaults to using the recommended default.
   412  func NewKubeClient(kubeConfig, apiServerURL string, requestTimeout time.Duration) (*kubernetes.Clientset, error) {
   413  	log.Infof("Instantiating new Kubernetes client")
   414  	config, err := instrumentedRESTConfig(kubeConfig, apiServerURL, requestTimeout)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	client, err := kubernetes.NewForConfig(config)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  	log.Infof("Created Kubernetes client %s", config.Host)
   423  	return client, nil
   424  }
   425  
   426  // NewIstioClient returns a new Istio client object. It uses the configured
   427  // KubeConfig attribute to connect to the cluster. If KubeConfig isn't provided
   428  // it defaults to using the recommended default.
   429  // NB: Istio controls the creation of the underlying Kubernetes client, so we
   430  // have no ability to tack on transport wrappers (e.g., Prometheus request
   431  // wrappers) to the client's config at this level. Furthermore, the Istio client
   432  // constructor does not expose the ability to override the Kubernetes API server endpoint,
   433  // so the apiServerURL config attribute has no effect.
   434  func NewIstioClient(kubeConfig string, apiServerURL string) (*istioclient.Clientset, error) {
   435  	if kubeConfig == "" {
   436  		if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
   437  			kubeConfig = clientcmd.RecommendedHomeFile
   438  		}
   439  	}
   440  
   441  	restCfg, err := clientcmd.BuildConfigFromFlags(apiServerURL, kubeConfig)
   442  	if err != nil {
   443  		return nil, err
   444  	}
   445  
   446  	ic, err := istioclient.NewForConfig(restCfg)
   447  	if err != nil {
   448  		return nil, errors.Wrap(err, "Failed to create istio client")
   449  	}
   450  
   451  	return ic, nil
   452  }
   453  
   454  // NewDynamicKubernetesClient returns a new Dynamic Kubernetes client object. It takes a Config and
   455  // uses APIServerURL and KubeConfig attributes to connect to the cluster. If
   456  // KubeConfig isn't provided it defaults to using the recommended default.
   457  func NewDynamicKubernetesClient(kubeConfig, apiServerURL string, requestTimeout time.Duration) (dynamic.Interface, error) {
   458  	config, err := instrumentedRESTConfig(kubeConfig, apiServerURL, requestTimeout)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  	client, err := dynamic.NewForConfig(config)
   463  	if err != nil {
   464  		return nil, err
   465  	}
   466  	log.Infof("Created Dynamic Kubernetes client %s", config.Host)
   467  	return client, nil
   468  }
   469  
   470  // NewOpenShiftClient returns a new Openshift client object. It takes a Config and
   471  // uses APIServerURL and KubeConfig attributes to connect to the cluster. If
   472  // KubeConfig isn't provided it defaults to using the recommended default.
   473  func NewOpenShiftClient(kubeConfig, apiServerURL string, requestTimeout time.Duration) (*openshift.Clientset, error) {
   474  	config, err := instrumentedRESTConfig(kubeConfig, apiServerURL, requestTimeout)
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  	client, err := openshift.NewForConfig(config)
   479  	if err != nil {
   480  		return nil, err
   481  	}
   482  	log.Infof("Created OpenShift client %s", config.Host)
   483  	return client, nil
   484  }