sigs.k8s.io/external-dns@v0.14.1/source/istio_virtualservice.go (about) 1 /* 2 Copyright 2020 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 "fmt" 22 "sort" 23 "strings" 24 "text/template" 25 26 log "github.com/sirupsen/logrus" 27 networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" 28 istioclient "istio.io/client-go/pkg/clientset/versioned" 29 istioinformers "istio.io/client-go/pkg/informers/externalversions" 30 networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3" 31 "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/labels" 34 kubeinformers "k8s.io/client-go/informers" 35 coreinformers "k8s.io/client-go/informers/core/v1" 36 "k8s.io/client-go/kubernetes" 37 "k8s.io/client-go/tools/cache" 38 39 "sigs.k8s.io/external-dns/endpoint" 40 ) 41 42 // IstioMeshGateway is the built in gateway for all sidecars 43 const IstioMeshGateway = "mesh" 44 45 // virtualServiceSource is an implementation of Source for Istio VirtualService objects. 46 // The implementation uses the spec.hosts values for the hostnames. 47 // Use targetAnnotationKey to explicitly set Endpoint. 48 type virtualServiceSource struct { 49 kubeClient kubernetes.Interface 50 istioClient istioclient.Interface 51 namespace string 52 annotationFilter string 53 fqdnTemplate *template.Template 54 combineFQDNAnnotation bool 55 ignoreHostnameAnnotation bool 56 serviceInformer coreinformers.ServiceInformer 57 virtualserviceInformer networkingv1alpha3informer.VirtualServiceInformer 58 } 59 60 // NewIstioVirtualServiceSource creates a new virtualServiceSource with the given config. 61 func NewIstioVirtualServiceSource( 62 ctx context.Context, 63 kubeClient kubernetes.Interface, 64 istioClient istioclient.Interface, 65 namespace string, 66 annotationFilter string, 67 fqdnTemplate string, 68 combineFQDNAnnotation bool, 69 ignoreHostnameAnnotation bool, 70 ) (Source, error) { 71 tmpl, err := parseTemplate(fqdnTemplate) 72 if err != nil { 73 return nil, err 74 } 75 76 // Use shared informers to listen for add/update/delete of services/pods/nodes in the specified namespace. 77 // Set resync period to 0, to prevent processing when nothing has changed 78 informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) 79 serviceInformer := informerFactory.Core().V1().Services() 80 istioInformerFactory := istioinformers.NewSharedInformerFactoryWithOptions(istioClient, 0, istioinformers.WithNamespace(namespace)) 81 virtualServiceInformer := istioInformerFactory.Networking().V1alpha3().VirtualServices() 82 83 // Add default resource event handlers to properly initialize informer. 84 serviceInformer.Informer().AddEventHandler( 85 cache.ResourceEventHandlerFuncs{ 86 AddFunc: func(obj interface{}) { 87 log.Debug("service added") 88 }, 89 }, 90 ) 91 92 virtualServiceInformer.Informer().AddEventHandler( 93 cache.ResourceEventHandlerFuncs{ 94 AddFunc: func(obj interface{}) { 95 log.Debug("virtual service added") 96 }, 97 }, 98 ) 99 100 informerFactory.Start(ctx.Done()) 101 istioInformerFactory.Start(ctx.Done()) 102 103 // wait for the local cache to be populated. 104 if err := waitForCacheSync(context.Background(), informerFactory); err != nil { 105 return nil, err 106 } 107 if err := waitForCacheSync(context.Background(), istioInformerFactory); err != nil { 108 return nil, err 109 } 110 111 return &virtualServiceSource{ 112 kubeClient: kubeClient, 113 istioClient: istioClient, 114 namespace: namespace, 115 annotationFilter: annotationFilter, 116 fqdnTemplate: tmpl, 117 combineFQDNAnnotation: combineFQDNAnnotation, 118 ignoreHostnameAnnotation: ignoreHostnameAnnotation, 119 serviceInformer: serviceInformer, 120 virtualserviceInformer: virtualServiceInformer, 121 }, nil 122 } 123 124 // Endpoints returns endpoint objects for each host-target combination that should be processed. 125 // Retrieves all VirtualService resources in the source's namespace(s). 126 func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { 127 virtualServices, err := sc.virtualserviceInformer.Lister().VirtualServices(sc.namespace).List(labels.Everything()) 128 if err != nil { 129 return nil, err 130 } 131 virtualServices, err = sc.filterByAnnotations(virtualServices) 132 if err != nil { 133 return nil, err 134 } 135 136 var endpoints []*endpoint.Endpoint 137 138 for _, virtualService := range virtualServices { 139 // Check controller annotation to see if we are responsible. 140 controller, ok := virtualService.Annotations[controllerAnnotationKey] 141 if ok && controller != controllerAnnotationValue { 142 log.Debugf("Skipping VirtualService %s/%s because controller value does not match, found: %s, required: %s", 143 virtualService.Namespace, virtualService.Name, controller, controllerAnnotationValue) 144 continue 145 } 146 147 gwEndpoints, err := sc.endpointsFromVirtualService(ctx, virtualService) 148 if err != nil { 149 return nil, err 150 } 151 152 // apply template if host is missing on VirtualService 153 if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil { 154 iEndpoints, err := sc.endpointsFromTemplate(ctx, virtualService) 155 if err != nil { 156 return nil, err 157 } 158 159 if sc.combineFQDNAnnotation { 160 gwEndpoints = append(gwEndpoints, iEndpoints...) 161 } else { 162 gwEndpoints = iEndpoints 163 } 164 } 165 166 if len(gwEndpoints) == 0 { 167 log.Debugf("No endpoints could be generated from VirtualService %s/%s", virtualService.Namespace, virtualService.Name) 168 continue 169 } 170 171 log.Debugf("Endpoints generated from VirtualService: %s/%s: %v", virtualService.Namespace, virtualService.Name, gwEndpoints) 172 endpoints = append(endpoints, gwEndpoints...) 173 } 174 175 for _, ep := range endpoints { 176 sort.Sort(ep.Targets) 177 } 178 179 return endpoints, nil 180 } 181 182 // AddEventHandler adds an event handler that should be triggered if the watched Istio VirtualService changes. 183 func (sc *virtualServiceSource) AddEventHandler(ctx context.Context, handler func()) { 184 log.Debug("Adding event handler for Istio VirtualService") 185 186 sc.virtualserviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) 187 } 188 189 func (sc *virtualServiceSource) getGateway(ctx context.Context, gatewayStr string, virtualService *networkingv1alpha3.VirtualService) (*networkingv1alpha3.Gateway, error) { 190 if gatewayStr == "" || gatewayStr == IstioMeshGateway { 191 // This refers to "all sidecars in the mesh"; ignore. 192 return nil, nil 193 } 194 195 namespace, name, err := parseGateway(gatewayStr) 196 if err != nil { 197 log.Debugf("Failed parsing gatewayStr %s of VirtualService %s/%s", gatewayStr, virtualService.Namespace, virtualService.Name) 198 return nil, err 199 } 200 if namespace == "" { 201 namespace = virtualService.Namespace 202 } 203 204 gateway, err := sc.istioClient.NetworkingV1alpha3().Gateways(namespace).Get(ctx, name, metav1.GetOptions{}) 205 if errors.IsNotFound(err) { 206 log.Warnf("VirtualService (%s/%s) references non-existent gateway: %s ", virtualService.Namespace, virtualService.Name, gatewayStr) 207 return nil, nil 208 } else if err != nil { 209 log.Errorf("Failed retrieving gateway %s referenced by VirtualService %s/%s: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err) 210 return nil, err 211 } 212 if gateway == nil { 213 log.Debugf("Gateway %s referenced by VirtualService %s/%s not found: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err) 214 return nil, nil 215 } 216 return gateway, nil 217 } 218 219 func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtualService *networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) { 220 hostnames, err := execTemplate(sc.fqdnTemplate, virtualService) 221 if err != nil { 222 return nil, err 223 } 224 225 resource := fmt.Sprintf("virtualservice/%s/%s", virtualService.Namespace, virtualService.Name) 226 227 ttl := getTTLFromAnnotations(virtualService.Annotations, resource) 228 229 providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualService.Annotations) 230 231 var endpoints []*endpoint.Endpoint 232 for _, hostname := range hostnames { 233 targets, err := sc.targetsFromVirtualService(ctx, virtualService, hostname) 234 if err != nil { 235 return endpoints, err 236 } 237 endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) 238 } 239 return endpoints, nil 240 } 241 242 // filterByAnnotations filters a list of configs by a given annotation selector. 243 func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) { 244 labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) 245 if err != nil { 246 return nil, err 247 } 248 selector, err := metav1.LabelSelectorAsSelector(labelSelector) 249 if err != nil { 250 return nil, err 251 } 252 253 // empty filter returns original list 254 if selector.Empty() { 255 return virtualservices, nil 256 } 257 258 var filteredList []*networkingv1alpha3.VirtualService 259 260 for _, virtualservice := range virtualservices { 261 // convert the annotations to an equivalent label selector 262 annotations := labels.Set(virtualservice.Annotations) 263 264 // include if the annotations match the selector 265 if selector.Matches(annotations) { 266 filteredList = append(filteredList, virtualservice) 267 } 268 } 269 270 return filteredList, nil 271 } 272 273 // append a target to the list of targets unless it's already in the list 274 func appendUnique(targets []string, target string) []string { 275 for _, element := range targets { 276 if element == target { 277 return targets 278 } 279 } 280 return append(targets, target) 281 } 282 283 func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, virtualService *networkingv1alpha3.VirtualService, vsHost string) ([]string, error) { 284 var targets []string 285 // for each host we need to iterate through the gateways because each host might match for only one of the gateways 286 for _, gateway := range virtualService.Spec.Gateways { 287 gateway, err := sc.getGateway(ctx, gateway, virtualService) 288 if err != nil { 289 return nil, err 290 } 291 if gateway == nil { 292 continue 293 } 294 if !virtualServiceBindsToGateway(virtualService, gateway, vsHost) { 295 continue 296 } 297 tgs, err := sc.targetsFromGateway(ctx, gateway) 298 if err != nil { 299 return targets, err 300 } 301 for _, target := range tgs { 302 targets = appendUnique(targets, target) 303 } 304 } 305 306 return targets, nil 307 } 308 309 // endpointsFromVirtualService extracts the endpoints from an Istio VirtualService Config object 310 func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, virtualservice *networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) { 311 var endpoints []*endpoint.Endpoint 312 var err error 313 314 resource := fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name) 315 316 ttl := getTTLFromAnnotations(virtualservice.Annotations, resource) 317 318 targetsFromAnnotation := getTargetsFromTargetAnnotation(virtualservice.Annotations) 319 320 providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualservice.Annotations) 321 322 for _, host := range virtualservice.Spec.Hosts { 323 if host == "" || host == "*" { 324 continue 325 } 326 327 parts := strings.Split(host, "/") 328 329 // If the input hostname is of the form my-namespace/foo.bar.com, remove the namespace 330 // before appending it to the list of endpoints to create 331 if len(parts) == 2 { 332 host = parts[1] 333 } 334 335 targets := targetsFromAnnotation 336 if len(targets) == 0 { 337 targets, err = sc.targetsFromVirtualService(ctx, virtualservice, host) 338 if err != nil { 339 return endpoints, err 340 } 341 } 342 343 endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) 344 } 345 346 // Skip endpoints if we do not want entries from annotations 347 if !sc.ignoreHostnameAnnotation { 348 hostnameList := getHostnamesFromAnnotations(virtualservice.Annotations) 349 for _, hostname := range hostnameList { 350 targets := targetsFromAnnotation 351 if len(targets) == 0 { 352 targets, err = sc.targetsFromVirtualService(ctx, virtualservice, hostname) 353 if err != nil { 354 return endpoints, err 355 } 356 } 357 endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) 358 } 359 } 360 361 return endpoints, nil 362 } 363 364 // checks if the given VirtualService should actually bind to the given gateway 365 // see requirements here: https://istio.io/docs/reference/config/networking/gateway/#Server 366 func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualService, gateway *networkingv1alpha3.Gateway, vsHost string) bool { 367 isValid := false 368 if len(virtualService.Spec.ExportTo) == 0 { 369 isValid = true 370 } else { 371 for _, ns := range virtualService.Spec.ExportTo { 372 if ns == "*" || ns == gateway.Namespace || (ns == "." && gateway.Namespace == virtualService.Namespace) { 373 isValid = true 374 } 375 } 376 } 377 if !isValid { 378 return false 379 } 380 381 for _, server := range gateway.Spec.Servers { 382 for _, host := range server.Hosts { 383 namespace := "*" 384 parts := strings.Split(host, "/") 385 if len(parts) == 2 { 386 namespace = parts[0] 387 host = parts[1] 388 } else if len(parts) != 1 { 389 log.Debugf("Gateway %s/%s has invalid host %s", gateway.Namespace, gateway.Name, host) 390 continue 391 } 392 393 if namespace == "*" || namespace == virtualService.Namespace || (namespace == "." && virtualService.Namespace == gateway.Namespace) { 394 if host == "*" { 395 return true 396 } 397 398 suffixMatch := false 399 if strings.HasPrefix(host, "*.") { 400 suffixMatch = true 401 } 402 403 if host == vsHost || (suffixMatch && strings.HasSuffix(vsHost, host[1:])) { 404 return true 405 } 406 } 407 } 408 } 409 410 return false 411 } 412 413 func parseGateway(gateway string) (namespace, name string, err error) { 414 parts := strings.Split(gateway, "/") 415 if len(parts) == 2 { 416 namespace, name = parts[0], parts[1] 417 } else if len(parts) == 1 { 418 name = parts[0] 419 } else { 420 err = fmt.Errorf("invalid gateway name (name or namespace/name) found '%v'", gateway) 421 } 422 423 return 424 } 425 426 func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { 427 namespace, name, err := parseIngress(ingressStr) 428 if err != nil { 429 return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) 430 } 431 if namespace == "" { 432 namespace = gateway.Namespace 433 } 434 435 ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{}) 436 if err != nil { 437 log.Error(err) 438 return 439 } 440 for _, lb := range ingress.Status.LoadBalancer.Ingress { 441 if lb.IP != "" { 442 targets = append(targets, lb.IP) 443 } else if lb.Hostname != "" { 444 targets = append(targets, lb.Hostname) 445 } 446 } 447 return 448 } 449 450 func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { 451 targets = getTargetsFromTargetAnnotation(gateway.Annotations) 452 if len(targets) > 0 { 453 return 454 } 455 456 ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource] 457 if ok && ingressStr != "" { 458 targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway) 459 return 460 } 461 462 services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything()) 463 if err != nil { 464 log.Error(err) 465 return 466 } 467 468 for _, service := range services { 469 if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) { 470 continue 471 } 472 473 if len(service.Spec.ExternalIPs) > 0 { 474 targets = append(targets, service.Spec.ExternalIPs...) 475 continue 476 } 477 478 for _, lb := range service.Status.LoadBalancer.Ingress { 479 if lb.IP != "" { 480 targets = append(targets, lb.IP) 481 } else if lb.Hostname != "" { 482 targets = append(targets, lb.Hostname) 483 } 484 } 485 } 486 487 return 488 }