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  }