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 }