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 }