sigs.k8s.io/external-dns@v0.14.1/source/openshift_route.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  	"text/template"
    24  	"time"
    25  
    26  	routev1 "github.com/openshift/api/route/v1"
    27  	versioned "github.com/openshift/client-go/route/clientset/versioned"
    28  	extInformers "github.com/openshift/client-go/route/informers/externalversions"
    29  	routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1"
    30  	log "github.com/sirupsen/logrus"
    31  	corev1 "k8s.io/api/core/v1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/client-go/tools/cache"
    35  
    36  	"sigs.k8s.io/external-dns/endpoint"
    37  )
    38  
    39  // ocpRouteSource is an implementation of Source for OpenShift Route objects.
    40  // The Route implementation will use the Route spec.host field for the hostname,
    41  // and the Route status' canonicalHostname field as the target.
    42  // The targetAnnotationKey can be used to explicitly set an alternative
    43  // endpoint, if desired.
    44  type ocpRouteSource struct {
    45  	client                   versioned.Interface
    46  	namespace                string
    47  	annotationFilter         string
    48  	fqdnTemplate             *template.Template
    49  	combineFQDNAnnotation    bool
    50  	ignoreHostnameAnnotation bool
    51  	routeInformer            routeInformer.RouteInformer
    52  	labelSelector            labels.Selector
    53  	ocpRouterName            string
    54  }
    55  
    56  // NewOcpRouteSource creates a new ocpRouteSource with the given config.
    57  func NewOcpRouteSource(
    58  	ctx context.Context,
    59  	ocpClient versioned.Interface,
    60  	namespace string,
    61  	annotationFilter string,
    62  	fqdnTemplate string,
    63  	combineFQDNAnnotation bool,
    64  	ignoreHostnameAnnotation bool,
    65  	labelSelector labels.Selector,
    66  	ocpRouterName string,
    67  ) (Source, error) {
    68  	tmpl, err := parseTemplate(fqdnTemplate)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	// Use a shared informer to listen for add/update/delete of Routes in the specified namespace.
    74  	// Set resync period to 0, to prevent processing when nothing has changed.
    75  	informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0*time.Second, namespace, nil)
    76  	informer := informerFactory.Route().V1().Routes()
    77  
    78  	// Add default resource event handlers to properly initialize informer.
    79  	informer.Informer().AddEventHandler(
    80  		cache.ResourceEventHandlerFuncs{
    81  			AddFunc: func(obj interface{}) {
    82  			},
    83  		},
    84  	)
    85  
    86  	informerFactory.Start(ctx.Done())
    87  
    88  	// wait for the local cache to be populated.
    89  	if err := waitForCacheSync(context.Background(), informerFactory); err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	return &ocpRouteSource{
    94  		client:                   ocpClient,
    95  		namespace:                namespace,
    96  		annotationFilter:         annotationFilter,
    97  		fqdnTemplate:             tmpl,
    98  		combineFQDNAnnotation:    combineFQDNAnnotation,
    99  		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
   100  		routeInformer:            informer,
   101  		labelSelector:            labelSelector,
   102  		ocpRouterName:            ocpRouterName,
   103  	}, nil
   104  }
   105  
   106  func (ors *ocpRouteSource) AddEventHandler(ctx context.Context, handler func()) {
   107  	log.Debug("Adding event handler for openshift route")
   108  
   109  	// Right now there is no way to remove event handler from informer, see:
   110  	// https://github.com/kubernetes/kubernetes/issues/79610
   111  	ors.routeInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
   112  }
   113  
   114  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   115  // Retrieves all OpenShift Route resources on all namespaces, unless an explicit namespace
   116  // is specified in ocpRouteSource.
   117  func (ors *ocpRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   118  	ocpRoutes, err := ors.routeInformer.Lister().Routes(ors.namespace).List(ors.labelSelector)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	ocpRoutes, err = ors.filterByAnnotations(ocpRoutes)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	endpoints := []*endpoint.Endpoint{}
   129  
   130  	for _, ocpRoute := range ocpRoutes {
   131  		// Check controller annotation to see if we are responsible.
   132  		controller, ok := ocpRoute.Annotations[controllerAnnotationKey]
   133  		if ok && controller != controllerAnnotationValue {
   134  			log.Debugf("Skipping OpenShift Route %s/%s because controller value does not match, found: %s, required: %s",
   135  				ocpRoute.Namespace, ocpRoute.Name, controller, controllerAnnotationValue)
   136  			continue
   137  		}
   138  
   139  		orEndpoints := ors.endpointsFromOcpRoute(ocpRoute, ors.ignoreHostnameAnnotation)
   140  
   141  		// apply template if host is missing on OpenShift Route
   142  		if (ors.combineFQDNAnnotation || len(orEndpoints) == 0) && ors.fqdnTemplate != nil {
   143  			oEndpoints, err := ors.endpointsFromTemplate(ocpRoute)
   144  			if err != nil {
   145  				return nil, err
   146  			}
   147  
   148  			if ors.combineFQDNAnnotation {
   149  				orEndpoints = append(orEndpoints, oEndpoints...)
   150  			} else {
   151  				orEndpoints = oEndpoints
   152  			}
   153  		}
   154  
   155  		if len(orEndpoints) == 0 {
   156  			log.Debugf("No endpoints could be generated from OpenShift Route %s/%s", ocpRoute.Namespace, ocpRoute.Name)
   157  			continue
   158  		}
   159  
   160  		log.Debugf("Endpoints generated from OpenShift Route: %s/%s: %v", ocpRoute.Namespace, ocpRoute.Name, orEndpoints)
   161  		endpoints = append(endpoints, orEndpoints...)
   162  	}
   163  
   164  	for _, ep := range endpoints {
   165  		sort.Sort(ep.Targets)
   166  	}
   167  
   168  	return endpoints, nil
   169  }
   170  
   171  func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*endpoint.Endpoint, error) {
   172  	hostnames, err := execTemplate(ors.fqdnTemplate, ocpRoute)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
   178  
   179  	ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource)
   180  
   181  	targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
   182  	if len(targets) == 0 {
   183  		targetsFromRoute, _ := ors.getTargetsFromRouteStatus(ocpRoute.Status)
   184  		targets = targetsFromRoute
   185  	}
   186  
   187  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
   188  
   189  	var endpoints []*endpoint.Endpoint
   190  	for _, hostname := range hostnames {
   191  		endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   192  	}
   193  	return endpoints, nil
   194  }
   195  
   196  func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*routev1.Route, error) {
   197  	labelSelector, err := metav1.ParseToLabelSelector(ors.annotationFilter)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	// empty filter returns original list
   207  	if selector.Empty() {
   208  		return ocpRoutes, nil
   209  	}
   210  
   211  	filteredList := []*routev1.Route{}
   212  
   213  	for _, ocpRoute := range ocpRoutes {
   214  		// convert the Route's annotations to an equivalent label selector
   215  		annotations := labels.Set(ocpRoute.Annotations)
   216  
   217  		// include ocpRoute if its annotations match the selector
   218  		if selector.Matches(annotations) {
   219  			filteredList = append(filteredList, ocpRoute)
   220  		}
   221  	}
   222  
   223  	return filteredList, nil
   224  }
   225  
   226  // endpointsFromOcpRoute extracts the endpoints from a OpenShift Route object
   227  func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation bool) []*endpoint.Endpoint {
   228  	var endpoints []*endpoint.Endpoint
   229  
   230  	resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
   231  
   232  	ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource)
   233  
   234  	targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
   235  	targetsFromRoute, host := ors.getTargetsFromRouteStatus(ocpRoute.Status)
   236  
   237  	if len(targets) == 0 {
   238  		targets = targetsFromRoute
   239  	}
   240  
   241  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
   242  
   243  	if host != "" {
   244  		endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   245  	}
   246  
   247  	// Skip endpoints if we do not want entries from annotations
   248  	if !ignoreHostnameAnnotation {
   249  		hostnameList := getHostnamesFromAnnotations(ocpRoute.Annotations)
   250  		for _, hostname := range hostnameList {
   251  			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   252  		}
   253  	}
   254  	return endpoints
   255  }
   256  
   257  // getTargetsFromRouteStatus returns the router's canonical hostname and host
   258  // either for the given router if it admitted the route
   259  // or for the first (in the status list) router that admitted the route.
   260  func (ors *ocpRouteSource) getTargetsFromRouteStatus(status routev1.RouteStatus) (endpoint.Targets, string) {
   261  	for _, ing := range status.Ingress {
   262  		// if this Ingress didn't admit the route or it doesn't have the canonical hostname, then ignore it
   263  		if ingressConditionStatus(&ing, routev1.RouteAdmitted) != corev1.ConditionTrue || ing.RouterCanonicalHostname == "" {
   264  			continue
   265  		}
   266  
   267  		// if the router name is specified for the Route source and it matches the route's ingress name, then return it
   268  		if ors.ocpRouterName != "" && ors.ocpRouterName == ing.RouterName {
   269  			return endpoint.Targets{ing.RouterCanonicalHostname}, ing.Host
   270  		}
   271  
   272  		// if the router name is not specified in the Route source then return the first ingress
   273  		if ors.ocpRouterName == "" {
   274  			return endpoint.Targets{ing.RouterCanonicalHostname}, ing.Host
   275  		}
   276  	}
   277  	return endpoint.Targets{}, ""
   278  }
   279  
   280  func ingressConditionStatus(ingress *routev1.RouteIngress, t routev1.RouteIngressConditionType) corev1.ConditionStatus {
   281  	for _, condition := range ingress.Conditions {
   282  		if t != condition.Type {
   283  			continue
   284  		}
   285  		return condition.Status
   286  	}
   287  	return corev1.ConditionUnknown
   288  }