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 }