sigs.k8s.io/external-dns@v0.14.1/source/skipper_routegroup.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  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/json"
    25  	"fmt"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"sort"
    31  	"strings"
    32  	"sync"
    33  	"text/template"
    34  	"time"
    35  
    36  	log "github.com/sirupsen/logrus"
    37  
    38  	"sigs.k8s.io/external-dns/endpoint"
    39  )
    40  
    41  const (
    42  	defaultIdleConnTimeout = 30 * time.Second
    43  	// DefaultRoutegroupVersion is the default version for route groups.
    44  	DefaultRoutegroupVersion     = "zalando.org/v1"
    45  	routeGroupListResource       = "/apis/%s/routegroups"
    46  	routeGroupNamespacedResource = "/apis/%s/namespaces/%s/routegroups"
    47  )
    48  
    49  type routeGroupSource struct {
    50  	cli                      routeGroupListClient
    51  	apiServer                string
    52  	namespace                string
    53  	apiEndpoint              string
    54  	annotationFilter         string
    55  	fqdnTemplate             *template.Template
    56  	combineFQDNAnnotation    bool
    57  	ignoreHostnameAnnotation bool
    58  }
    59  
    60  // for testing
    61  type routeGroupListClient interface {
    62  	getRouteGroupList(string) (*routeGroupList, error)
    63  }
    64  
    65  type routeGroupClient struct {
    66  	mu        sync.Mutex
    67  	quit      chan struct{}
    68  	client    *http.Client
    69  	token     string
    70  	tokenFile string
    71  }
    72  
    73  func newRouteGroupClient(token, tokenPath string, timeout time.Duration) *routeGroupClient {
    74  	const (
    75  		tokenFile  = "/var/run/secrets/kubernetes.io/serviceaccount/token"
    76  		rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    77  	)
    78  	if tokenPath != "" {
    79  		tokenPath = tokenFile
    80  	}
    81  
    82  	tr := &http.Transport{
    83  		DialContext: (&net.Dialer{
    84  			Timeout:   timeout,
    85  			KeepAlive: 30 * time.Second,
    86  			DualStack: true,
    87  		}).DialContext,
    88  		TLSHandshakeTimeout:   3 * time.Second,
    89  		ResponseHeaderTimeout: timeout,
    90  		IdleConnTimeout:       defaultIdleConnTimeout,
    91  		MaxIdleConns:          5,
    92  		MaxIdleConnsPerHost:   5,
    93  	}
    94  	cli := &routeGroupClient{
    95  		client: &http.Client{
    96  			Transport: tr,
    97  		},
    98  		quit:      make(chan struct{}),
    99  		tokenFile: tokenPath,
   100  		token:     token,
   101  	}
   102  
   103  	go func() {
   104  		for {
   105  			select {
   106  			case <-time.After(tr.IdleConnTimeout):
   107  				tr.CloseIdleConnections()
   108  				cli.updateToken()
   109  			case <-cli.quit:
   110  				return
   111  			}
   112  		}
   113  	}()
   114  
   115  	// in cluster config, errors are treated as not running in cluster
   116  	cli.updateToken()
   117  
   118  	// cluster internal use custom CA to reach TLS endpoint
   119  	rootCA, err := os.ReadFile(rootCAFile)
   120  	if err != nil {
   121  		return cli
   122  	}
   123  	certPool := x509.NewCertPool()
   124  	if !certPool.AppendCertsFromPEM(rootCA) {
   125  		return cli
   126  	}
   127  
   128  	tr.TLSClientConfig = &tls.Config{
   129  		MinVersion: tls.VersionTLS12,
   130  		RootCAs:    certPool,
   131  	}
   132  
   133  	return cli
   134  }
   135  
   136  func (cli *routeGroupClient) updateToken() {
   137  	if cli.tokenFile == "" {
   138  		return
   139  	}
   140  
   141  	token, err := os.ReadFile(cli.tokenFile)
   142  	if err != nil {
   143  		log.Errorf("Failed to read token from file (%s): %v", cli.tokenFile, err)
   144  		return
   145  	}
   146  
   147  	cli.mu.Lock()
   148  	cli.token = string(token)
   149  	cli.mu.Unlock()
   150  }
   151  
   152  func (cli *routeGroupClient) getToken() string {
   153  	cli.mu.Lock()
   154  	defer cli.mu.Unlock()
   155  	return cli.token
   156  }
   157  
   158  func (cli *routeGroupClient) getRouteGroupList(url string) (*routeGroupList, error) {
   159  	resp, err := cli.get(url)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	defer resp.Body.Close()
   164  
   165  	if resp.StatusCode != 200 {
   166  		return nil, fmt.Errorf("failed to get routegroup list from %s, got: %s", url, resp.Status)
   167  	}
   168  
   169  	var rgs routeGroupList
   170  	err = json.NewDecoder(resp.Body).Decode(&rgs)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	return &rgs, nil
   176  }
   177  
   178  func (cli *routeGroupClient) get(url string) (*http.Response, error) {
   179  	req, err := http.NewRequest("GET", url, nil)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	return cli.do(req)
   184  }
   185  
   186  func (cli *routeGroupClient) do(req *http.Request) (*http.Response, error) {
   187  	if tok := cli.getToken(); tok != "" && req.Header.Get("Authorization") == "" {
   188  		req.Header.Set("Authorization", "Bearer "+tok)
   189  	}
   190  	return cli.client.Do(req)
   191  }
   192  
   193  // NewRouteGroupSource creates a new routeGroupSource with the given config.
   194  func NewRouteGroupSource(timeout time.Duration, token, tokenPath, apiServerURL, namespace, annotationFilter, fqdnTemplate, routegroupVersion string, combineFqdnAnnotation, ignoreHostnameAnnotation bool) (Source, error) {
   195  	tmpl, err := parseTemplate(fqdnTemplate)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	if routegroupVersion == "" {
   201  		routegroupVersion = DefaultRoutegroupVersion
   202  	}
   203  	cli := newRouteGroupClient(token, tokenPath, timeout)
   204  
   205  	u, err := url.Parse(apiServerURL)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	apiServer := u.String()
   211  	// strip port if well known port, because of TLS certificate match
   212  	if u.Scheme == "https" && u.Port() == "443" {
   213  		// correctly handle IPv6 addresses by keeping surrounding `[]`.
   214  		apiServer = "https://" + strings.TrimSuffix(u.Host, ":443")
   215  	}
   216  
   217  	sc := &routeGroupSource{
   218  		cli:                      cli,
   219  		apiServer:                apiServer,
   220  		namespace:                namespace,
   221  		apiEndpoint:              apiServer + fmt.Sprintf(routeGroupListResource, routegroupVersion),
   222  		annotationFilter:         annotationFilter,
   223  		fqdnTemplate:             tmpl,
   224  		combineFQDNAnnotation:    combineFqdnAnnotation,
   225  		ignoreHostnameAnnotation: ignoreHostnameAnnotation,
   226  	}
   227  	if namespace != "" {
   228  		sc.apiEndpoint = apiServer + fmt.Sprintf(routeGroupNamespacedResource, routegroupVersion, namespace)
   229  	}
   230  
   231  	log.Infoln("Created route group source")
   232  	return sc, nil
   233  }
   234  
   235  // AddEventHandler for routegroup is currently a no op, because we do not implement caching, yet.
   236  func (sc *routeGroupSource) AddEventHandler(ctx context.Context, handler func()) {}
   237  
   238  // Endpoints returns endpoint objects for each host-target combination that should be processed.
   239  // Retrieves all routeGroup resources on all namespaces.
   240  // Logic is ported from ingress without fqdnTemplate
   241  func (sc *routeGroupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   242  	rgList, err := sc.cli.getRouteGroupList(sc.apiEndpoint)
   243  	if err != nil {
   244  		log.Errorf("Failed to get RouteGroup list: %v", err)
   245  		return nil, err
   246  	}
   247  	rgList, err = sc.filterByAnnotations(rgList)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	endpoints := []*endpoint.Endpoint{}
   253  	for _, rg := range rgList.Items {
   254  		// Check controller annotation to see if we are responsible.
   255  		controller, ok := rg.Metadata.Annotations[controllerAnnotationKey]
   256  		if ok && controller != controllerAnnotationValue {
   257  			log.Debugf("Skipping routegroup %s/%s because controller value does not match, found: %s, required: %s",
   258  				rg.Metadata.Namespace, rg.Metadata.Name, controller, controllerAnnotationValue)
   259  			continue
   260  		}
   261  
   262  		eps := sc.endpointsFromRouteGroup(rg)
   263  
   264  		if (sc.combineFQDNAnnotation || len(eps) == 0) && sc.fqdnTemplate != nil {
   265  			tmplEndpoints, err := sc.endpointsFromTemplate(rg)
   266  			if err != nil {
   267  				return nil, err
   268  			}
   269  
   270  			if sc.combineFQDNAnnotation {
   271  				eps = append(eps, tmplEndpoints...)
   272  			} else {
   273  				eps = tmplEndpoints
   274  			}
   275  		}
   276  
   277  		if len(eps) == 0 {
   278  			log.Debugf("No endpoints could be generated from routegroup %s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
   279  			continue
   280  		}
   281  
   282  		log.Debugf("Endpoints generated from ingress: %s/%s: %v", rg.Metadata.Namespace, rg.Metadata.Name, eps)
   283  		sc.setRouteGroupDualstackLabel(rg, eps)
   284  		endpoints = append(endpoints, eps...)
   285  	}
   286  
   287  	for _, ep := range endpoints {
   288  		sort.Sort(ep.Targets)
   289  	}
   290  
   291  	return endpoints, nil
   292  }
   293  
   294  func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.Endpoint, error) {
   295  	// Process the whole template string
   296  	var buf bytes.Buffer
   297  	err := sc.fqdnTemplate.Execute(&buf, rg)
   298  	if err != nil {
   299  		return nil, fmt.Errorf("failed to apply template on routegroup %s/%s: %v", rg.Metadata.Namespace, rg.Metadata.Name, err)
   300  	}
   301  
   302  	hostnames := buf.String()
   303  
   304  	resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
   305  
   306  	// error handled in endpointsFromRouteGroup(), otherwise duplicate log
   307  	ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource)
   308  
   309  	targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
   310  
   311  	if len(targets) == 0 {
   312  		targets = targetsFromRouteGroupStatus(rg.Status)
   313  	}
   314  
   315  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
   316  
   317  	var endpoints []*endpoint.Endpoint
   318  	// splits the FQDN template and removes the trailing periods
   319  	hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
   320  	for _, hostname := range hostnameList {
   321  		hostname = strings.TrimSuffix(hostname, ".")
   322  		endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   323  	}
   324  	return endpoints, nil
   325  }
   326  
   327  func (sc *routeGroupSource) setRouteGroupDualstackLabel(rg *routeGroup, eps []*endpoint.Endpoint) {
   328  	val, ok := rg.Metadata.Annotations[ALBDualstackAnnotationKey]
   329  	if ok && val == ALBDualstackAnnotationValue {
   330  		log.Debugf("Adding dualstack label to routegroup %s/%s.", rg.Metadata.Namespace, rg.Metadata.Name)
   331  		for _, ep := range eps {
   332  			ep.Labels[endpoint.DualstackLabelKey] = "true"
   333  		}
   334  	}
   335  }
   336  
   337  // annotation logic ported from source/ingress.go without Spec.TLS part, because it'S not supported in RouteGroup
   338  func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.Endpoint {
   339  	endpoints := []*endpoint.Endpoint{}
   340  
   341  	resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
   342  
   343  	ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource)
   344  
   345  	targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
   346  	if len(targets) == 0 {
   347  		for _, lb := range rg.Status.LoadBalancer.RouteGroup {
   348  			if lb.IP != "" {
   349  				targets = append(targets, lb.IP)
   350  			}
   351  			if lb.Hostname != "" {
   352  				targets = append(targets, lb.Hostname)
   353  			}
   354  		}
   355  	}
   356  
   357  	providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
   358  
   359  	for _, src := range rg.Spec.Hosts {
   360  		if src == "" {
   361  			continue
   362  		}
   363  		endpoints = append(endpoints, endpointsForHostname(src, targets, ttl, providerSpecific, setIdentifier, resource)...)
   364  	}
   365  
   366  	// Skip endpoints if we do not want entries from annotations
   367  	if !sc.ignoreHostnameAnnotation {
   368  		hostnameList := getHostnamesFromAnnotations(rg.Metadata.Annotations)
   369  		for _, hostname := range hostnameList {
   370  			endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
   371  		}
   372  	}
   373  	return endpoints
   374  }
   375  
   376  // filterByAnnotations filters a list of routeGroupList by a given annotation selector.
   377  func (sc *routeGroupSource) filterByAnnotations(rgs *routeGroupList) (*routeGroupList, error) {
   378  	selector, err := getLabelSelector(sc.annotationFilter)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  
   383  	// empty filter returns original list
   384  	if selector.Empty() {
   385  		return rgs, nil
   386  	}
   387  
   388  	var filteredList []*routeGroup
   389  	for _, rg := range rgs.Items {
   390  		// include ingress if its annotations match the selector
   391  		if matchLabelSelector(selector, rg.Metadata.Annotations) {
   392  			filteredList = append(filteredList, rg)
   393  		}
   394  	}
   395  	rgs.Items = filteredList
   396  
   397  	return rgs, nil
   398  }
   399  
   400  func targetsFromRouteGroupStatus(status routeGroupStatus) endpoint.Targets {
   401  	var targets endpoint.Targets
   402  
   403  	for _, lb := range status.LoadBalancer.RouteGroup {
   404  		if lb.IP != "" {
   405  			targets = append(targets, lb.IP)
   406  		}
   407  		if lb.Hostname != "" {
   408  			targets = append(targets, lb.Hostname)
   409  		}
   410  	}
   411  
   412  	return targets
   413  }
   414  
   415  type routeGroupList struct {
   416  	Kind       string                 `json:"kind"`
   417  	APIVersion string                 `json:"apiVersion"`
   418  	Metadata   routeGroupListMetadata `json:"metadata"`
   419  	Items      []*routeGroup          `json:"items"`
   420  }
   421  
   422  type routeGroupListMetadata struct {
   423  	SelfLink        string `json:"selfLink"`
   424  	ResourceVersion string `json:"resourceVersion"`
   425  }
   426  
   427  type routeGroup struct {
   428  	Metadata itemMetadata     `json:"metadata"`
   429  	Spec     routeGroupSpec   `json:"spec"`
   430  	Status   routeGroupStatus `json:"status"`
   431  }
   432  
   433  type itemMetadata struct {
   434  	Namespace   string            `json:"namespace"`
   435  	Name        string            `json:"name"`
   436  	Annotations map[string]string `json:"annotations"`
   437  }
   438  
   439  type routeGroupSpec struct {
   440  	Hosts []string `json:"hosts"`
   441  }
   442  
   443  type routeGroupStatus struct {
   444  	LoadBalancer routeGroupLoadBalancerStatus `json:"loadBalancer"`
   445  }
   446  
   447  type routeGroupLoadBalancerStatus struct {
   448  	RouteGroup []routeGroupLoadBalancer `json:"routeGroup"`
   449  }
   450  
   451  type routeGroupLoadBalancer struct {
   452  	IP       string `json:"ip,omitempty"`
   453  	Hostname string `json:"hostname,omitempty"`
   454  }