sigs.k8s.io/external-dns@v0.14.1/source/source.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 "bytes" 21 "context" 22 "fmt" 23 "math" 24 "net" 25 "reflect" 26 "strconv" 27 "strings" 28 "text/template" 29 "time" 30 "unicode" 31 32 log "github.com/sirupsen/logrus" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/labels" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 38 "sigs.k8s.io/external-dns/endpoint" 39 ) 40 41 const ( 42 // The annotation used for figuring out which controller is responsible 43 controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller" 44 // The annotation used for defining the desired hostname 45 hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname" 46 // The annotation used for specifying whether the public or private interface address is used 47 accessAnnotationKey = "external-dns.alpha.kubernetes.io/access" 48 // The annotation used for specifying the type of endpoints to use for headless services 49 endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type" 50 // The annotation used for defining the desired ingress/service target 51 targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" 52 // The annotation used for defining the desired DNS record TTL 53 ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl" 54 // The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME 55 aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias" 56 // The annotation used to determine the source of hostnames for ingresses. This is an optional field - all 57 // available hostname sources are used if not specified. 58 ingressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source" 59 // The value of the controller annotation so that we feel responsible 60 controllerAnnotationValue = "dns-controller" 61 // The annotation used for defining the desired hostname 62 internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname" 63 ) 64 65 const ( 66 EndpointsTypeNodeExternalIP = "NodeExternalIP" 67 EndpointsTypeHostIP = "HostIP" 68 ) 69 70 // Provider-specific annotations 71 const ( 72 // The annotation used for determining if traffic will go through Cloudflare 73 CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied" 74 75 SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier" 76 ) 77 78 const ( 79 ttlMinimum = 1 80 ttlMaximum = math.MaxInt32 81 ) 82 83 // Source defines the interface Endpoint sources should implement. 84 type Source interface { 85 Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) 86 // AddEventHandler adds an event handler that should be triggered if something in source changes 87 AddEventHandler(context.Context, func()) 88 } 89 90 func getTTLFromAnnotations(annotations map[string]string, resource string) endpoint.TTL { 91 ttlNotConfigured := endpoint.TTL(0) 92 ttlAnnotation, exists := annotations[ttlAnnotationKey] 93 if !exists { 94 return ttlNotConfigured 95 } 96 ttlValue, err := parseTTL(ttlAnnotation) 97 if err != nil { 98 log.Warnf("%s: \"%v\" is not a valid TTL value: %v", resource, ttlAnnotation, err) 99 return ttlNotConfigured 100 } 101 if ttlValue < ttlMinimum || ttlValue > ttlMaximum { 102 log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum) 103 return ttlNotConfigured 104 } 105 return endpoint.TTL(ttlValue) 106 } 107 108 // parseTTL parses TTL from string, returning duration in seconds. 109 // parseTTL supports both integers like "600" and durations based 110 // on Go Duration like "10m", hence "600" and "10m" represent the same value. 111 // 112 // Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second 113 // for the example). 114 func parseTTL(s string) (ttlSeconds int64, err error) { 115 ttlDuration, errDuration := time.ParseDuration(s) 116 if errDuration != nil { 117 ttlInt, err := strconv.ParseInt(s, 10, 64) 118 if err != nil { 119 return 0, errDuration 120 } 121 return ttlInt, nil 122 } 123 124 return int64(ttlDuration.Seconds()), nil 125 } 126 127 type kubeObject interface { 128 runtime.Object 129 metav1.Object 130 } 131 132 func execTemplate(tmpl *template.Template, obj kubeObject) (hostnames []string, err error) { 133 var buf bytes.Buffer 134 if err := tmpl.Execute(&buf, obj); err != nil { 135 kind := obj.GetObjectKind().GroupVersionKind().Kind 136 return nil, fmt.Errorf("failed to apply template on %s %s/%s: %w", kind, obj.GetNamespace(), obj.GetName(), err) 137 } 138 for _, name := range strings.Split(buf.String(), ",") { 139 name = strings.TrimFunc(name, unicode.IsSpace) 140 name = strings.TrimSuffix(name, ".") 141 hostnames = append(hostnames, name) 142 } 143 return hostnames, nil 144 } 145 146 func parseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) { 147 if fqdnTemplate == "" { 148 return nil, nil 149 } 150 funcs := template.FuncMap{ 151 "trimPrefix": strings.TrimPrefix, 152 } 153 return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate) 154 } 155 156 func getHostnamesFromAnnotations(annotations map[string]string) []string { 157 hostnameAnnotation, exists := annotations[hostnameAnnotationKey] 158 if !exists { 159 return nil 160 } 161 return splitHostnameAnnotation(hostnameAnnotation) 162 } 163 164 func getAccessFromAnnotations(annotations map[string]string) string { 165 return annotations[accessAnnotationKey] 166 } 167 168 func getEndpointsTypeFromAnnotations(annotations map[string]string) string { 169 return annotations[endpointsTypeAnnotationKey] 170 } 171 172 func getInternalHostnamesFromAnnotations(annotations map[string]string) []string { 173 internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey] 174 if !exists { 175 return nil 176 } 177 return splitHostnameAnnotation(internalHostnameAnnotation) 178 } 179 180 func splitHostnameAnnotation(annotation string) []string { 181 return strings.Split(strings.Replace(annotation, " ", "", -1), ",") 182 } 183 184 func getAliasFromAnnotations(annotations map[string]string) bool { 185 aliasAnnotation, exists := annotations[aliasAnnotationKey] 186 return exists && aliasAnnotation == "true" 187 } 188 189 func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) { 190 providerSpecificAnnotations := endpoint.ProviderSpecific{} 191 192 v, exists := annotations[CloudflareProxiedKey] 193 if exists { 194 providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ 195 Name: CloudflareProxiedKey, 196 Value: v, 197 }) 198 } 199 if getAliasFromAnnotations(annotations) { 200 providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ 201 Name: "alias", 202 Value: "true", 203 }) 204 } 205 setIdentifier := "" 206 for k, v := range annotations { 207 if k == SetIdentifierKey { 208 setIdentifier = v 209 } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") { 210 attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-") 211 providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ 212 Name: fmt.Sprintf("aws/%s", attr), 213 Value: v, 214 }) 215 } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") { 216 attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-") 217 providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ 218 Name: fmt.Sprintf("scw/%s", attr), 219 Value: v, 220 }) 221 } else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") { 222 attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") 223 providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ 224 Name: fmt.Sprintf("ibmcloud-%s", attr), 225 Value: v, 226 }) 227 } 228 } 229 return providerSpecificAnnotations, setIdentifier 230 } 231 232 // getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation. 233 // Returns empty endpoints array if none are found. 234 func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets { 235 var targets endpoint.Targets 236 237 // Get the desired hostname of the ingress from the annotation. 238 targetAnnotation, exists := annotations[targetAnnotationKey] 239 if exists && targetAnnotation != "" { 240 // splits the hostname annotation and removes the trailing periods 241 targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",") 242 for _, targetHostname := range targetsList { 243 targetHostname = strings.TrimSuffix(targetHostname, ".") 244 targets = append(targets, targetHostname) 245 } 246 } 247 return targets 248 } 249 250 // suitableType returns the DNS resource record type suitable for the target. 251 // In this case type A for IPs and type CNAME for everything else. 252 func suitableType(target string) string { 253 if net.ParseIP(target) != nil && net.ParseIP(target).To4() != nil { 254 return endpoint.RecordTypeA 255 } else if net.ParseIP(target) != nil && net.ParseIP(target).To16() != nil { 256 return endpoint.RecordTypeAAAA 257 } 258 return endpoint.RecordTypeCNAME 259 } 260 261 // endpointsForHostname returns the endpoint objects for each host-target combination. 262 func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint { 263 var endpoints []*endpoint.Endpoint 264 265 var aTargets endpoint.Targets 266 var aaaaTargets endpoint.Targets 267 var cnameTargets endpoint.Targets 268 269 for _, t := range targets { 270 switch suitableType(t) { 271 case endpoint.RecordTypeA: 272 if isIPv6String(t) { 273 continue 274 } 275 aTargets = append(aTargets, t) 276 case endpoint.RecordTypeAAAA: 277 if !isIPv6String(t) { 278 continue 279 } 280 aaaaTargets = append(aaaaTargets, t) 281 default: 282 cnameTargets = append(cnameTargets, t) 283 } 284 } 285 286 if len(aTargets) > 0 { 287 epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...) 288 if epA != nil { 289 epA.ProviderSpecific = providerSpecific 290 epA.SetIdentifier = setIdentifier 291 if resource != "" { 292 epA.Labels[endpoint.ResourceLabelKey] = resource 293 } 294 endpoints = append(endpoints, epA) 295 } 296 } 297 298 if len(aaaaTargets) > 0 { 299 epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...) 300 if epAAAA != nil { 301 epAAAA.ProviderSpecific = providerSpecific 302 epAAAA.SetIdentifier = setIdentifier 303 if resource != "" { 304 epAAAA.Labels[endpoint.ResourceLabelKey] = resource 305 } 306 endpoints = append(endpoints, epAAAA) 307 } 308 } 309 310 if len(cnameTargets) > 0 { 311 epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...) 312 if epCNAME != nil { 313 epCNAME.ProviderSpecific = providerSpecific 314 epCNAME.SetIdentifier = setIdentifier 315 if resource != "" { 316 epCNAME.Labels[endpoint.ResourceLabelKey] = resource 317 } 318 endpoints = append(endpoints, epCNAME) 319 } 320 } 321 322 return endpoints 323 } 324 325 func getLabelSelector(annotationFilter string) (labels.Selector, error) { 326 labelSelector, err := metav1.ParseToLabelSelector(annotationFilter) 327 if err != nil { 328 return nil, err 329 } 330 return metav1.LabelSelectorAsSelector(labelSelector) 331 } 332 333 func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool { 334 annotations := labels.Set(srcAnnotations) 335 return selector.Matches(annotations) 336 } 337 338 type eventHandlerFunc func() 339 340 func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() } 341 func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() } 342 func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() } 343 344 type informerFactory interface { 345 WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 346 } 347 348 func waitForCacheSync(ctx context.Context, factory informerFactory) error { 349 ctx, cancel := context.WithTimeout(ctx, 60*time.Second) 350 defer cancel() 351 for typ, done := range factory.WaitForCacheSync(ctx.Done()) { 352 if !done { 353 select { 354 case <-ctx.Done(): 355 return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err()) 356 default: 357 return fmt.Errorf("failed to sync %v", typ) 358 } 359 } 360 } 361 return nil 362 } 363 364 type dynamicInformerFactory interface { 365 WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool 366 } 367 368 func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error { 369 ctx, cancel := context.WithTimeout(ctx, 60*time.Second) 370 defer cancel() 371 for typ, done := range factory.WaitForCacheSync(ctx.Done()) { 372 if !done { 373 select { 374 case <-ctx.Done(): 375 return fmt.Errorf("failed to sync %v: %v", typ, ctx.Err()) 376 default: 377 return fmt.Errorf("failed to sync %v", typ) 378 } 379 } 380 } 381 return nil 382 } 383 384 // isIPv6String returns if ip is IPv6. 385 func isIPv6String(ip string) bool { 386 netIP := net.ParseIP(ip) 387 return netIP != nil && netIP.To4() == nil 388 }