sigs.k8s.io/external-dns@v0.14.1/source/contour_httpproxy.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  	"text/template"
    24  
    25  	"github.com/pkg/errors"
    26  	projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1"
    27  	log "github.com/sirupsen/logrus"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/client-go/dynamic"
    32  	"k8s.io/client-go/dynamic/dynamicinformer"
    33  	"k8s.io/client-go/informers"
    34  	"k8s.io/client-go/tools/cache"
    35  
    36  	"sigs.k8s.io/external-dns/endpoint"
    37  )
    38  
    39  // HTTPProxySource is an implementation of Source for ProjectContour HTTPProxy objects.
    40  // The HTTPProxy implementation uses the spec.virtualHost.fqdn value for the hostname.
    41  // Use targetAnnotationKey to explicitly set Endpoint.
    42  type httpProxySource struct {
    43  	dynamicKubeClient        dynamic.Interface
    44  	namespace                string
    45  	annotationFilter         string
    46  	fqdnTemplate             *template.Template
    47  	combineFQDNAnnotation    bool
    48  	ignoreHostnameAnnotation bool
    49  	httpProxyInformer        informers.GenericInformer
    50  	unstructuredConverter    *UnstructuredConverter
    51  }
    52  
    53  // NewContourHTTPProxySource creates a new contourHTTPProxySource with the given config.
    54  func NewContourHTTPProxySource(
    55  	ctx context.Context,
    56  	dynamicKubeClient dynamic.Interface,
    57  	namespace string,
    58  	annotationFilter string,
    59  	fqdnTemplate string,
    60  	combineFqdnAnnotation bool,
    61  	ignoreHostnameAnnotation bool,
    62  ) (Source, error) {
    63  	tmpl, err := parseTemplate(fqdnTemplate)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	// Use shared informer to listen for add/update/delete of HTTPProxys in the specified namespace.
    69  	// Set resync period to 0, to prevent processing when nothing has changed.
    70  	informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
    71  	httpProxyInformer := informerFactory.ForResource(projectcontour.HTTPProxyGVR)
    72  
    73  	// Add default resource event handlers to properly initialize informer.
    74  	httpProxyInformer.Informer().AddEventHandler(
    75  		cache.ResourceEventHandlerFuncs{
    76  			AddFunc: func(obj interface{}) {
    77  			},
    78  		},
    79  	)
    80  
    81  	informerFactory.Start(ctx.Done())
    82  
    83  	// wait for the local cache to be populated.
    84  	if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	uc, err := NewUnstructuredConverter()
    89  	if err != nil {
    90  		return nil, errors.Wrap(err, "failed to setup Unstructured Converter")
    91  	}
    92  
    93  	return &httpProxySource{
    94  		dynamicKubeClient:        dynamicKubeClient,
    95  		namespace:                namespace,
    96  		annotationFilter:         annotationFilter,
    97  		fqdnTemplate:             tmpl,
    98  		combineFQDNAnnotation:    combineFqdnAnnotation,
    99  		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
   100  		httpProxyInformer:        httpProxyInformer,
   101  		unstructuredConverter:    uc,
   102  	}, nil
   103  }
   104  
   105  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   106  // Retrieves all HTTPProxy resources in the source's namespace(s).
   107  func (sc *httpProxySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   108  	hps, err := sc.httpProxyInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	// Convert to []*projectcontour.HTTPProxy
   114  	var httpProxies []*projectcontour.HTTPProxy
   115  	for _, hp := range hps {
   116  		unstructuredHP, ok := hp.(*unstructured.Unstructured)
   117  		if !ok {
   118  			return nil, errors.New("could not convert")
   119  		}
   120  
   121  		hpConverted := &projectcontour.HTTPProxy{}
   122  		err := sc.unstructuredConverter.scheme.Convert(unstructuredHP, hpConverted, nil)
   123  		if err != nil {
   124  			return nil, errors.Wrap(err, "failed to convert to HTTPProxy")
   125  		}
   126  		httpProxies = append(httpProxies, hpConverted)
   127  	}
   128  
   129  	httpProxies, err = sc.filterByAnnotations(httpProxies)
   130  	if err != nil {
   131  		return nil, errors.Wrap(err, "failed to filter HTTPProxies")
   132  	}
   133  
   134  	endpoints := []*endpoint.Endpoint{}
   135  
   136  	for _, hp := range httpProxies {
   137  		// Check controller annotation to see if we are responsible.
   138  		controller, ok := hp.Annotations[controllerAnnotationKey]
   139  		if ok && controller != controllerAnnotationValue {
   140  			log.Debugf("Skipping HTTPProxy %s/%s because controller value does not match, found: %s, required: %s",
   141  				hp.Namespace, hp.Name, controller, controllerAnnotationValue)
   142  			continue
   143  		}
   144  
   145  		hpEndpoints, err := sc.endpointsFromHTTPProxy(hp)
   146  		if err != nil {
   147  			return nil, errors.Wrap(err, "failed to get endpoints from HTTPProxy")
   148  		}
   149  
   150  		// apply template if fqdn is missing on HTTPProxy
   151  		if (sc.combineFQDNAnnotation || len(hpEndpoints) == 0) && sc.fqdnTemplate != nil {
   152  			tmplEndpoints, err := sc.endpointsFromTemplate(hp)
   153  			if err != nil {
   154  				return nil, errors.Wrap(err, "failed to get endpoints from template")
   155  			}
   156  
   157  			if sc.combineFQDNAnnotation {
   158  				hpEndpoints = append(hpEndpoints, tmplEndpoints...)
   159  			} else {
   160  				hpEndpoints = tmplEndpoints
   161  			}
   162  		}
   163  
   164  		if len(hpEndpoints) == 0 {
   165  			log.Debugf("No endpoints could be generated from HTTPProxy %s/%s", hp.Namespace, hp.Name)
   166  			continue
   167  		}
   168  
   169  		log.Debugf("Endpoints generated from HTTPProxy: %s/%s: %v", hp.Namespace, hp.Name, hpEndpoints)
   170  		endpoints = append(endpoints, hpEndpoints...)
   171  	}
   172  
   173  	for _, ep := range endpoints {
   174  		sort.Sort(ep.Targets)
   175  	}
   176  
   177  	return endpoints, nil
   178  }
   179  
   180  func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) {
   181  	hostnames, err := execTemplate(sc.fqdnTemplate, httpProxy)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
   187  
   188  	ttl := getTTLFromAnnotations(httpProxy.Annotations, resource)
   189  
   190  	targets := getTargetsFromTargetAnnotation(httpProxy.Annotations)
   191  	if len(targets) == 0 {
   192  		for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
   193  			if lb.IP != "" {
   194  				targets = append(targets, lb.IP)
   195  			}
   196  			if lb.Hostname != "" {
   197  				targets = append(targets, lb.Hostname)
   198  			}
   199  		}
   200  	}
   201  
   202  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations)
   203  
   204  	var endpoints []*endpoint.Endpoint
   205  	for _, hostname := range hostnames {
   206  		endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   207  	}
   208  	return endpoints, nil
   209  }
   210  
   211  // filterByAnnotations filters a list of configs by a given annotation selector.
   212  func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTTPProxy) ([]*projectcontour.HTTPProxy, error) {
   213  	labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	// empty filter returns original list
   223  	if selector.Empty() {
   224  		return httpProxies, nil
   225  	}
   226  
   227  	filteredList := []*projectcontour.HTTPProxy{}
   228  
   229  	for _, httpProxy := range httpProxies {
   230  		// convert the HTTPProxy's annotations to an equivalent label selector
   231  		annotations := labels.Set(httpProxy.Annotations)
   232  
   233  		// include HTTPProxy if its annotations match the selector
   234  		if selector.Matches(annotations) {
   235  			filteredList = append(filteredList, httpProxy)
   236  		}
   237  	}
   238  
   239  	return filteredList, nil
   240  }
   241  
   242  // endpointsFromHTTPProxyConfig extracts the endpoints from a Contour HTTPProxy object
   243  func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) {
   244  	resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
   245  
   246  	ttl := getTTLFromAnnotations(httpProxy.Annotations, resource)
   247  
   248  	targets := getTargetsFromTargetAnnotation(httpProxy.Annotations)
   249  
   250  	if len(targets) == 0 {
   251  		for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
   252  			if lb.IP != "" {
   253  				targets = append(targets, lb.IP)
   254  			}
   255  			if lb.Hostname != "" {
   256  				targets = append(targets, lb.Hostname)
   257  			}
   258  		}
   259  	}
   260  
   261  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations)
   262  
   263  	var endpoints []*endpoint.Endpoint
   264  
   265  	if virtualHost := httpProxy.Spec.VirtualHost; virtualHost != nil {
   266  		if fqdn := virtualHost.Fqdn; fqdn != "" {
   267  			endpoints = append(endpoints, endpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier, resource)...)
   268  		}
   269  	}
   270  
   271  	// Skip endpoints if we do not want entries from annotations
   272  	if !sc.ignoreHostnameAnnotation {
   273  		hostnameList := getHostnamesFromAnnotations(httpProxy.Annotations)
   274  		for _, hostname := range hostnameList {
   275  			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   276  		}
   277  	}
   278  
   279  	return endpoints, nil
   280  }
   281  
   282  func (sc *httpProxySource) AddEventHandler(ctx context.Context, handler func()) {
   283  	log.Debug("Adding event handler for httpproxy")
   284  
   285  	// Right now there is no way to remove event handler from informer, see:
   286  	// https://github.com/kubernetes/kubernetes/issues/79610
   287  	sc.httpProxyInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
   288  }