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 }