sigs.k8s.io/external-dns@v0.14.1/source/istio_gateway.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  	"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  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	kubeinformers "k8s.io/client-go/informers"
    34  	coreinformers "k8s.io/client-go/informers/core/v1"
    35  	"k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/tools/cache"
    37  
    38  	"sigs.k8s.io/external-dns/endpoint"
    39  )
    40  
    41  // IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
    42  // instead of a standard LoadBalancer service type
    43  const IstioGatewayIngressSource = "external-dns.alpha.kubernetes.io/ingress"
    44  
    45  // gatewaySource is an implementation of Source for Istio Gateway objects.
    46  // The gateway implementation uses the spec.servers.hosts values for the hostnames.
    47  // Use targetAnnotationKey to explicitly set Endpoint.
    48  type gatewaySource 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  	gatewayInformer          networkingv1alpha3informer.GatewayInformer
    58  }
    59  
    60  // NewIstioGatewaySource creates a new gatewaySource with the given config.
    61  func NewIstioGatewaySource(
    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.NewSharedInformerFactory(istioClient, 0)
    81  	gatewayInformer := istioInformerFactory.Networking().V1alpha3().Gateways()
    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  	gatewayInformer.Informer().AddEventHandler(
    93  		cache.ResourceEventHandlerFuncs{
    94  			AddFunc: func(obj interface{}) {
    95  				log.Debug("gateway 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 &gatewaySource{
   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  		gatewayInformer:          gatewayInformer,
   121  	}, nil
   122  }
   123  
   124  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   125  // Retrieves all gateway resources in the source's namespace(s).
   126  func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   127  	gwList, err := sc.istioClient.NetworkingV1alpha3().Gateways(sc.namespace).List(ctx, metav1.ListOptions{})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	gateways := gwList.Items
   133  	gateways, err = sc.filterByAnnotations(gateways)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	var endpoints []*endpoint.Endpoint
   139  
   140  	for _, gateway := range gateways {
   141  		// Check controller annotation to see if we are responsible.
   142  		controller, ok := gateway.Annotations[controllerAnnotationKey]
   143  		if ok && controller != controllerAnnotationValue {
   144  			log.Debugf("Skipping gateway %s/%s because controller value does not match, found: %s, required: %s",
   145  				gateway.Namespace, gateway.Name, controller, controllerAnnotationValue)
   146  			continue
   147  		}
   148  
   149  		gwHostnames, err := sc.hostNamesFromGateway(gateway)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  
   154  		// apply template if host is missing on gateway
   155  		if (sc.combineFQDNAnnotation || len(gwHostnames) == 0) && sc.fqdnTemplate != nil {
   156  			iHostnames, err := execTemplate(sc.fqdnTemplate, gateway)
   157  			if err != nil {
   158  				return nil, err
   159  			}
   160  
   161  			if sc.combineFQDNAnnotation {
   162  				gwHostnames = append(gwHostnames, iHostnames...)
   163  			} else {
   164  				gwHostnames = iHostnames
   165  			}
   166  		}
   167  
   168  		if len(gwHostnames) == 0 {
   169  			log.Debugf("No hostnames could be generated from gateway %s/%s", gateway.Namespace, gateway.Name)
   170  			continue
   171  		}
   172  
   173  		gwEndpoints, err := sc.endpointsFromGateway(ctx, gwHostnames, gateway)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  
   178  		if len(gwEndpoints) == 0 {
   179  			log.Debugf("No endpoints could be generated from gateway %s/%s", gateway.Namespace, gateway.Name)
   180  			continue
   181  		}
   182  
   183  		log.Debugf("Endpoints generated from gateway: %s/%s: %v", gateway.Namespace, gateway.Name, gwEndpoints)
   184  		endpoints = append(endpoints, gwEndpoints...)
   185  	}
   186  
   187  	for _, ep := range endpoints {
   188  		sort.Sort(ep.Targets)
   189  	}
   190  
   191  	return endpoints, nil
   192  }
   193  
   194  // AddEventHandler adds an event handler that should be triggered if the watched Istio Gateway changes.
   195  func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
   196  	log.Debug("Adding event handler for Istio Gateway")
   197  
   198  	sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
   199  }
   200  
   201  // filterByAnnotations filters a list of configs by a given annotation selector.
   202  func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) {
   203  	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	// empty filter returns original list
   213  	if selector.Empty() {
   214  		return gateways, nil
   215  	}
   216  
   217  	var filteredList []*networkingv1alpha3.Gateway
   218  
   219  	for _, gw := range gateways {
   220  		// convert the annotations to an equivalent label selector
   221  		annotations := labels.Set(gw.Annotations)
   222  
   223  		// include if the annotations match the selector
   224  		if selector.Matches(annotations) {
   225  			filteredList = append(filteredList, gw)
   226  		}
   227  	}
   228  
   229  	return filteredList, nil
   230  }
   231  
   232  func parseIngress(ingress string) (namespace, name string, err error) {
   233  	parts := strings.Split(ingress, "/")
   234  	if len(parts) == 2 {
   235  		namespace, name = parts[0], parts[1]
   236  	} else if len(parts) == 1 {
   237  		name = parts[0]
   238  	} else {
   239  		err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress)
   240  	}
   241  
   242  	return
   243  }
   244  
   245  func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
   246  	namespace, name, err := parseIngress(ingressStr)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
   249  	}
   250  	if namespace == "" {
   251  		namespace = gateway.Namespace
   252  	}
   253  
   254  	ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})
   255  	if err != nil {
   256  		log.Error(err)
   257  		return
   258  	}
   259  	for _, lb := range ingress.Status.LoadBalancer.Ingress {
   260  		if lb.IP != "" {
   261  			targets = append(targets, lb.IP)
   262  		} else if lb.Hostname != "" {
   263  			targets = append(targets, lb.Hostname)
   264  		}
   265  	}
   266  	return
   267  }
   268  
   269  func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
   270  	targets = getTargetsFromTargetAnnotation(gateway.Annotations)
   271  	if len(targets) > 0 {
   272  		return
   273  	}
   274  
   275  	ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
   276  	if ok && ingressStr != "" {
   277  		targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway)
   278  		return
   279  	}
   280  
   281  	services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything())
   282  	if err != nil {
   283  		log.Error(err)
   284  		return
   285  	}
   286  
   287  	for _, service := range services {
   288  		if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
   289  			continue
   290  		}
   291  
   292  		if len(service.Spec.ExternalIPs) > 0 {
   293  			targets = append(targets, service.Spec.ExternalIPs...)
   294  			continue
   295  		}
   296  
   297  		for _, lb := range service.Status.LoadBalancer.Ingress {
   298  			if lb.IP != "" {
   299  				targets = append(targets, lb.IP)
   300  			} else if lb.Hostname != "" {
   301  				targets = append(targets, lb.Hostname)
   302  			}
   303  		}
   304  	}
   305  
   306  	return
   307  }
   308  
   309  // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
   310  func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []string, gateway *networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) {
   311  	var endpoints []*endpoint.Endpoint
   312  	var err error
   313  
   314  	resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name)
   315  
   316  	annotations := gateway.Annotations
   317  	ttl := getTTLFromAnnotations(annotations, resource)
   318  
   319  	targets := getTargetsFromTargetAnnotation(annotations)
   320  	if len(targets) == 0 {
   321  		targets, err = sc.targetsFromGateway(ctx, gateway)
   322  		if err != nil {
   323  			return nil, err
   324  		}
   325  	}
   326  
   327  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations)
   328  
   329  	for _, host := range hostnames {
   330  		endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   331  	}
   332  
   333  	return endpoints, nil
   334  }
   335  
   336  func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gateway) ([]string, error) {
   337  	var hostnames []string
   338  	for _, server := range gateway.Spec.Servers {
   339  		for _, host := range server.Hosts {
   340  			if host == "" {
   341  				continue
   342  			}
   343  
   344  			parts := strings.Split(host, "/")
   345  
   346  			// If the input hostname is of the form my-namespace/foo.bar.com, remove the namespace
   347  			// before appending it to the list of endpoints to create
   348  			if len(parts) == 2 {
   349  				host = parts[1]
   350  			}
   351  
   352  			if host != "*" {
   353  				hostnames = append(hostnames, host)
   354  			}
   355  		}
   356  	}
   357  
   358  	if !sc.ignoreHostnameAnnotation {
   359  		hostnames = append(hostnames, getHostnamesFromAnnotations(gateway.Annotations)...)
   360  	}
   361  
   362  	return hostnames, nil
   363  }
   364  
   365  func gatewaySelectorMatchesServiceSelector(gwSelector, svcSelector map[string]string) bool {
   366  	for k, v := range gwSelector {
   367  		if lbl, ok := svcSelector[k]; !ok || lbl != v {
   368  			return false
   369  		}
   370  	}
   371  	return true
   372  }