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 }