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 }