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  }