sigs.k8s.io/external-dns@v0.14.1/source/ingress.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  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  	"text/template"
    26  
    27  	log "github.com/sirupsen/logrus"
    28  	networkv1 "k8s.io/api/networking/v1"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/selection"
    31  	kubeinformers "k8s.io/client-go/informers"
    32  	netinformers "k8s.io/client-go/informers/networking/v1"
    33  	"k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/tools/cache"
    35  
    36  	"sigs.k8s.io/external-dns/endpoint"
    37  )
    38  
    39  const (
    40  	// ALBDualstackAnnotationKey is the annotation used for determining if an ALB ingress is dualstack
    41  	ALBDualstackAnnotationKey = "alb.ingress.kubernetes.io/ip-address-type"
    42  	// ALBDualstackAnnotationValue is the value of the ALB dualstack annotation that indicates it is dualstack
    43  	ALBDualstackAnnotationValue = "dualstack"
    44  
    45  	// Possible values for the ingress-hostname-source annotation
    46  	IngressHostnameSourceAnnotationOnlyValue   = "annotation-only"
    47  	IngressHostnameSourceDefinedHostsOnlyValue = "defined-hosts-only"
    48  
    49  	IngressClassAnnotationKey = "kubernetes.io/ingress.class"
    50  )
    51  
    52  // ingressSource is an implementation of Source for Kubernetes ingress objects.
    53  // Ingress implementation will use the spec.rules.host value for the hostname
    54  // Use targetAnnotationKey to explicitly set Endpoint. (useful if the ingress
    55  // controller does not update, or to override with alternative endpoint)
    56  type ingressSource struct {
    57  	client                   kubernetes.Interface
    58  	namespace                string
    59  	annotationFilter         string
    60  	ingressClassNames        []string
    61  	fqdnTemplate             *template.Template
    62  	combineFQDNAnnotation    bool
    63  	ignoreHostnameAnnotation bool
    64  	ingressInformer          netinformers.IngressInformer
    65  	ignoreIngressTLSSpec     bool
    66  	ignoreIngressRulesSpec   bool
    67  	labelSelector            labels.Selector
    68  }
    69  
    70  // NewIngressSource creates a new ingressSource with the given config.
    71  func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) {
    72  	tmpl, err := parseTemplate(fqdnTemplate)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	// ensure that ingress class is only set in either the ingressClassNames or
    78  	// annotationFilter but not both
    79  	if ingressClassNames != nil && annotationFilter != "" {
    80  		selector, err := getLabelSelector(annotationFilter)
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  
    85  		requirements, _ := selector.Requirements()
    86  		for _, requirement := range requirements {
    87  			if requirement.Key() == "kubernetes.io/ingress.class" {
    88  				return nil, errors.New("--ingress-class is mutually exclusive with the kubernetes.io/ingress.class annotation filter")
    89  			}
    90  		}
    91  	}
    92  	// Use shared informer to listen for add/update/delete of ingresses in the specified namespace.
    93  	// Set resync period to 0, to prevent processing when nothing has changed.
    94  	informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
    95  	ingressInformer := informerFactory.Networking().V1().Ingresses()
    96  
    97  	// Add default resource event handlers to properly initialize informer.
    98  	ingressInformer.Informer().AddEventHandler(
    99  		cache.ResourceEventHandlerFuncs{
   100  			AddFunc: func(obj interface{}) {
   101  			},
   102  		},
   103  	)
   104  
   105  	informerFactory.Start(ctx.Done())
   106  
   107  	// wait for the local cache to be populated.
   108  	if err := waitForCacheSync(context.Background(), informerFactory); err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	sc := &ingressSource{
   113  		client:                   kubeClient,
   114  		namespace:                namespace,
   115  		annotationFilter:         annotationFilter,
   116  		ingressClassNames:        ingressClassNames,
   117  		fqdnTemplate:             tmpl,
   118  		combineFQDNAnnotation:    combineFqdnAnnotation,
   119  		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
   120  		ingressInformer:          ingressInformer,
   121  		ignoreIngressTLSSpec:     ignoreIngressTLSSpec,
   122  		ignoreIngressRulesSpec:   ignoreIngressRulesSpec,
   123  		labelSelector:            labelSelector,
   124  	}
   125  	return sc, nil
   126  }
   127  
   128  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   129  // Retrieves all ingress resources on all namespaces
   130  func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   131  	ingresses, err := sc.ingressInformer.Lister().Ingresses(sc.namespace).List(sc.labelSelector)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	ingresses, err = sc.filterByAnnotations(ingresses)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	ingresses, err = sc.filterByIngressClass(ingresses)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	endpoints := []*endpoint.Endpoint{}
   146  
   147  	for _, ing := range ingresses {
   148  		// Check controller annotation to see if we are responsible.
   149  		controller, ok := ing.Annotations[controllerAnnotationKey]
   150  		if ok && controller != controllerAnnotationValue {
   151  			log.Debugf("Skipping ingress %s/%s because controller value does not match, found: %s, required: %s",
   152  				ing.Namespace, ing.Name, controller, controllerAnnotationValue)
   153  			continue
   154  		}
   155  
   156  		ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec, sc.ignoreIngressRulesSpec)
   157  
   158  		// apply template if host is missing on ingress
   159  		if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
   160  			iEndpoints, err := sc.endpointsFromTemplate(ing)
   161  			if err != nil {
   162  				return nil, err
   163  			}
   164  
   165  			ingEndpoints = append(ingEndpoints, iEndpoints...)
   166  		}
   167  
   168  		if len(ingEndpoints) == 0 {
   169  			log.Debugf("No endpoints could be generated from ingress %s/%s", ing.Namespace, ing.Name)
   170  			continue
   171  		}
   172  
   173  		log.Debugf("Endpoints generated from ingress: %s/%s: %v", ing.Namespace, ing.Name, ingEndpoints)
   174  		sc.setDualstackLabel(ing, ingEndpoints)
   175  		endpoints = append(endpoints, ingEndpoints...)
   176  	}
   177  
   178  	for _, ep := range endpoints {
   179  		sort.Sort(ep.Targets)
   180  	}
   181  
   182  	return endpoints, nil
   183  }
   184  
   185  func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpoint.Endpoint, error) {
   186  	hostnames, err := execTemplate(sc.fqdnTemplate, ing)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
   192  
   193  	ttl := getTTLFromAnnotations(ing.Annotations, resource)
   194  
   195  	targets := getTargetsFromTargetAnnotation(ing.Annotations)
   196  	if len(targets) == 0 {
   197  		targets = targetsFromIngressStatus(ing.Status)
   198  	}
   199  
   200  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
   201  
   202  	var endpoints []*endpoint.Endpoint
   203  	for _, hostname := range hostnames {
   204  		endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   205  	}
   206  	return endpoints, nil
   207  }
   208  
   209  // filterByAnnotations filters a list of ingresses by a given annotation selector.
   210  func (sc *ingressSource) filterByAnnotations(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) {
   211  	selector, err := getLabelSelector(sc.annotationFilter)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	// empty filter returns original list
   217  	if selector.Empty() {
   218  		return ingresses, nil
   219  	}
   220  
   221  	filteredList := []*networkv1.Ingress{}
   222  
   223  	for _, ingress := range ingresses {
   224  		// include ingress if its annotations match the selector
   225  		if matchLabelSelector(selector, ingress.Annotations) {
   226  			filteredList = append(filteredList, ingress)
   227  		}
   228  	}
   229  
   230  	return filteredList, nil
   231  }
   232  
   233  // filterByIngressClass filters a list of ingresses based on a required ingress
   234  // class
   235  func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) {
   236  	// if no class filter is specified then there's nothing to do
   237  	if len(sc.ingressClassNames) == 0 {
   238  		return ingresses, nil
   239  	}
   240  
   241  	classNameReq, err := labels.NewRequirement(IngressClassAnnotationKey, selection.In, sc.ingressClassNames)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	selector := labels.NewSelector()
   247  	selector = selector.Add(*classNameReq)
   248  
   249  	filteredList := []*networkv1.Ingress{}
   250  
   251  	for _, ingress := range ingresses {
   252  		var matched = false
   253  
   254  		for _, nameFilter := range sc.ingressClassNames {
   255  			if ingress.Spec.IngressClassName != nil && len(*ingress.Spec.IngressClassName) > 0 {
   256  				if nameFilter == *ingress.Spec.IngressClassName {
   257  					matched = true
   258  				}
   259  			} else if matchLabelSelector(selector, ingress.Annotations) {
   260  				matched = true
   261  			}
   262  
   263  			if matched {
   264  				filteredList = append(filteredList, ingress)
   265  				break
   266  			}
   267  		}
   268  
   269  		if !matched {
   270  			log.Debugf("Discarding ingress %s/%s because it does not match required ingress classes %v", ingress.Namespace, ingress.Name, sc.ingressClassNames)
   271  		}
   272  	}
   273  
   274  	return filteredList, nil
   275  }
   276  
   277  func (sc *ingressSource) setDualstackLabel(ingress *networkv1.Ingress, endpoints []*endpoint.Endpoint) {
   278  	val, ok := ingress.Annotations[ALBDualstackAnnotationKey]
   279  	if ok && val == ALBDualstackAnnotationValue {
   280  		log.Debugf("Adding dualstack label to ingress %s/%s.", ingress.Namespace, ingress.Name)
   281  		for _, ep := range endpoints {
   282  			ep.Labels[endpoint.DualstackLabelKey] = "true"
   283  		}
   284  	}
   285  }
   286  
   287  // endpointsFromIngress extracts the endpoints from ingress object
   288  func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) []*endpoint.Endpoint {
   289  	resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
   290  
   291  	ttl := getTTLFromAnnotations(ing.Annotations, resource)
   292  
   293  	targets := getTargetsFromTargetAnnotation(ing.Annotations)
   294  
   295  	if len(targets) == 0 {
   296  		targets = targetsFromIngressStatus(ing.Status)
   297  	}
   298  
   299  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
   300  
   301  	// Gather endpoints defined on hosts sections of the ingress
   302  	var definedHostsEndpoints []*endpoint.Endpoint
   303  	// Skip endpoints if we do not want entries from Rules section
   304  	if !ignoreIngressRulesSpec {
   305  		for _, rule := range ing.Spec.Rules {
   306  			if rule.Host == "" {
   307  				continue
   308  			}
   309  			definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   310  		}
   311  	}
   312  
   313  	// Skip endpoints if we do not want entries from tls spec section
   314  	if !ignoreIngressTLSSpec {
   315  		for _, tls := range ing.Spec.TLS {
   316  			for _, host := range tls.Hosts {
   317  				if host == "" {
   318  					continue
   319  				}
   320  				definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   321  			}
   322  		}
   323  	}
   324  
   325  	// Gather endpoints defined on annotations in the ingress
   326  	var annotationEndpoints []*endpoint.Endpoint
   327  	if !ignoreHostnameAnnotation {
   328  		for _, hostname := range getHostnamesFromAnnotations(ing.Annotations) {
   329  			annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   330  		}
   331  	}
   332  
   333  	// Determine which hostnames to consider in our final list
   334  	hostnameSourceAnnotation, hostnameSourceAnnotationExists := ing.Annotations[ingressHostnameSourceKey]
   335  	if !hostnameSourceAnnotationExists {
   336  		return append(definedHostsEndpoints, annotationEndpoints...)
   337  	}
   338  
   339  	// Include endpoints according to the hostname source annotation in our final list
   340  	var endpoints []*endpoint.Endpoint
   341  	if strings.ToLower(hostnameSourceAnnotation) == IngressHostnameSourceDefinedHostsOnlyValue {
   342  		endpoints = append(endpoints, definedHostsEndpoints...)
   343  	}
   344  	if strings.ToLower(hostnameSourceAnnotation) == IngressHostnameSourceAnnotationOnlyValue {
   345  		endpoints = append(endpoints, annotationEndpoints...)
   346  	}
   347  	return endpoints
   348  }
   349  
   350  func targetsFromIngressStatus(status networkv1.IngressStatus) endpoint.Targets {
   351  	var targets endpoint.Targets
   352  
   353  	for _, lb := range status.LoadBalancer.Ingress {
   354  		if lb.IP != "" {
   355  			targets = append(targets, lb.IP)
   356  		}
   357  		if lb.Hostname != "" {
   358  			targets = append(targets, lb.Hostname)
   359  		}
   360  	}
   361  
   362  	return targets
   363  }
   364  
   365  func (sc *ingressSource) AddEventHandler(ctx context.Context, handler func()) {
   366  	log.Debug("Adding event handler for ingress")
   367  
   368  	// Right now there is no way to remove event handler from informer, see:
   369  	// https://github.com/kubernetes/kubernetes/issues/79610
   370  	sc.ingressInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
   371  }