Unverified Commit b0c12fba authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub
Browse files

Merge pull request #1444 from szuecs/feature/route-group

Feature routegroup
parents b3609c4c bbed273d
......@@ -111,6 +111,7 @@ The following tutorials are provided:
* [Route53](docs/tutorials/aws.md)
* [Same domain for public and private Route53 zones](docs/tutorials/public-private-route53.md)
* [Cloud Map](docs/tutorials/aws-sd.md)
* [Kube Ingress AWS Controller](docs/tutorials/kube-ingress-aws.md)
* [Azure DNS](docs/tutorials/azure.md)
* [Azure Private DNS](docs/tutorials/azure-private-dns.md)
* [Cloudflare](docs/tutorials/cloudflare.md)
......
# Using ExternalDNS with kube-ingress-aws-controller
This tutorial describes how to use ExternalDNS with the [kube-ingress-aws-controller][1].
[1]: https://github.com/zalando-incubator/kube-ingress-aws-controller
## Setting up ExternalDNS and kube-ingress-aws-controller
Follow the [AWS tutorial](aws.md) to setup ExternalDNS for use in Kubernetes clusters
running in AWS. Specify the `source=ingress` argument so that ExternalDNS will look
for hostnames in Ingress objects. In addition, you may wish to limit which Ingress
objects are used as an ExternalDNS source via the `ingress-class` argument, but
this is not required.
For help setting up the Kubernetes Ingress AWS Controller, that can
create ALBs and NLBs, follow the [Setup Guide][2].
[2]: https://github.com/zalando-incubator/kube-ingress-aws-controller/tree/master/deploy
### Optional RouteGroup
[RouteGroup][3] is a CRD, that enables you to do complex routing with
[Skipper][4].
First, you have to apply the RouteGroup CRD to your cluster:
```
kubectl apply -f https://github.com/zalando/skipper/blob/master/dataclients/kubernetes/deploy/apply/routegroups_crd.yaml
```
You have to grant all controllers: [Skipper][4],
[kube-ingress-aws-controller][1] and external-dns to read the routegroup resource and
kube-ingress-aws-controller to update the status field of a routegroup.
This depends on your RBAC policies, in case you use RBAC, you can use
this for all 3 controllers:
```yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: kube-ingress-aws-controller
rules:
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses/status
verbs:
- patch
- update
- apiGroups:
- zalando.org
resources:
- routegroups
verbs:
- get
- list
- watch
- apiGroups:
- zalando.org
resources:
- routegroups/status
verbs:
- patch
- update
```
See also current RBAC yaml files:
- [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/rbac.yaml)
- [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/rbac.yaml)
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
[4]: https://opensource.zalando.com/skipper
## Deploy an example application
Create the following sample "echoserver" application to demonstrate how
ExternalDNS works with ingress objects, that were created by [kube-ingress-aws-controller][1].
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echoserver
spec:
replicas: 1
selector:
matchLabels:
app: echoserver
template:
metadata:
labels:
app: echoserver
spec:
containers:
- image: gcr.io/google_containers/echoserver:1.4
imagePullPolicy: Always
name: echoserver
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: echoserver
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
selector:
app: echoserver
```
Note that the Service object is of type `ClusterIP`, because we will
target [Skipper][4] and do the HTTP routing in Skipper. We don't need
a Service of type `LoadBalancer` here, since we will be using a shared
skipper-ingress for all Ingress. Skipper use `hostNetwork` to be able
to get traffic from AWS LoadBalancers EC2 network. ALBs or NLBs, will
be created based on need and will be shared across all ingress as
default.
## Ingress examples
Create the following Ingress to expose the echoserver application to the Internet.
```yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
rules:
- host: echoserver.mycluster.example.org
http: &echoserver_root
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- host: echoserver.example.org
http: *echoserver_root
```
The above should result in the creation of an (ipv4) ALB in AWS which will forward
traffic to skipper which will forward to the echoserver application.
If the `--source=ingress` argument is specified, then ExternalDNS will create DNS
records based on the hosts specified in ingress objects. The above example would
result in two alias records being created, `echoserver.mycluster.example.org` and
`echoserver.example.org`, which both alias the ALB that is associated with the
Ingress object.
Note that the above example makes use of the YAML anchor feature to avoid having
to repeat the http section for multiple hosts that use the exact same paths. If
this Ingress object will only be fronting one backend Service, we might instead
create the following:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
rules:
- http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
```
In the above example we create a default path that works for any hostname, and
make use of the `external-dns.alpha.kubernetes.io/hostname` annotation to create
multiple aliases for the resulting ALB.
## Dualstack ALBs
AWS [supports](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#ip-address-type) both IPv4 and "dualstack" (both IPv4 and IPv6) interfaces for ALBs.
The Kubernetes Ingress AWS controller supports the `alb.ingress.kubernetes.io/ip-address-type`
annotation (which defaults to `ipv4`) to determine this. If this annotation is
set to `dualstack` then ExternalDNS will create two alias records (one A record
and one AAAA record) for each hostname associated with the Ingress object.
Example:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/ip-address-type: dualstack
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
rules:
- host: echoserver.example.org
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
```
The above Ingress object will result in the creation of an ALB with a dualstack
interface. ExternalDNS will create both an A `echoserver.example.org` record and
an AAAA record of the same name, that each are aliases for the same ALB.
## NLBs
AWS has
[NLBs](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html)
and [kube-ingress-aws-controller][1] is able to create NLBs instead of ALBs.
The Kubernetes Ingress AWS controller supports the `zalando.org/aws-load-balancer-type`
annotation (which defaults to `alb`) to determine this. If this annotation is
set to `nlb` then ExternalDNS will create an NLB instead of an ALB.
Example:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
zalando.org/aws-load-balancer-type: nlb
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
rules:
- host: echoserver.example.org
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
```
The above Ingress object will result in the creation of an NLB. A
successful create, you can observe in the ingress `status` field, that is
written by [kube-ingress-aws-controller][1]:
```yaml
status:
loadBalancer:
ingress:
- hostname: kube-ing-lb-atedkrlml7iu-1681027139.$region.elb.amazonaws.com
```
ExternalDNS will create a A-records `echoserver.example.org`, that
use AWS ALIAS record to automatically maintain IP adresses of the NLB.
## RouteGroup (optional)
[Kube-ingress-aws-controller][1], [Skipper][4] and external-dns
support [RouteGroups][3]. External-dns needs to be started with
`--source=skipper-routegroup` parameter in order to work on RouteGroup objects.
Here we can not show [all RouteGroup
capabilities](https://opensource.zalando.com/skipper/kubernetes/routegroups/),
but we show one simple example with an application and a custom https
redirect.
```yaml
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-route-group
spec:
backends:
- name: my-backend
type: service
serviceName: my-service
servicePort: 80
- name: redirectShunt
type: shunt
defaultBackends:
- backendName: my-service
routes:
- pathSubtree: /
- pathSubtree: /
predicates:
- Header("X-Forwarded-Proto", "http")
filters:
- redirectTo(302, "https:")
backends:
- redirectShunt
```
......@@ -91,6 +91,8 @@ func main() {
CFUsername: cfg.CFUsername,
CFPassword: cfg.CFPassword,
ContourLoadBalancerService: cfg.ContourLoadBalancerService,
SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion,
RequestTimeout: cfg.RequestTimeout,
}
// Lookup all the selected sources by names and pass them the desired configuration.
......
......@@ -24,6 +24,7 @@ import (
"github.com/alecthomas/kingpin"
"github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/source"
)
const (
......@@ -42,6 +43,7 @@ type Config struct {
RequestTimeout time.Duration
IstioIngressGatewayServices []string
ContourLoadBalancerService string
SkipperRouteGroupVersion string
Sources []string
Namespace string
AnnotationFilter string
......@@ -144,6 +146,7 @@ var defaultConfig = &Config{
RequestTimeout: time.Second * 30,
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
ContourLoadBalancerService: "heptio-contour/contour",
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: nil,
Namespace: "",
AnnotationFilter: "",
......@@ -286,8 +289,12 @@ func (cfg *Config) ParseFlags(args []string) error {
// Flags related to Contour
app.Flag("contour-load-balancer", "The fully-qualified name of the Contour load balancer service. (default: heptio-contour/contour)").Default("heptio-contour/contour").StringVar(&cfg.ContourLoadBalancerService)
// Flags related to Skipper RouteGroup
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing sources
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, cloudfoundry, contour-ingressroute, crd, empty)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, cloudfoundry, contour-ingressroute, crd, empty)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty", "skipper-routegroup")
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
......
......@@ -33,6 +33,7 @@ var (
KubeConfig: "",
RequestTimeout: time.Second * 30,
ContourLoadBalancerService: "heptio-contour/contour",
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: []string{"service"},
Namespace: "",
FQDNTemplate: "",
......@@ -103,6 +104,7 @@ var (
KubeConfig: "/some/path",
RequestTimeout: time.Second * 77,
ContourLoadBalancerService: "heptio-contour-other/contour-other",
SkipperRouteGroupVersion: "zalando.org/v2",
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
IgnoreHostnameAnnotation: true,
......@@ -199,6 +201,7 @@ func TestParseFlags(t *testing.T) {
"--kubeconfig=/some/path",
"--request-timeout=77s",
"--contour-load-balancer=heptio-contour-other/contour-other",
"--skipper-routegroup-groupversion=zalando.org/v2",
"--source=service",
"--source=ingress",
"--source=connector",
......@@ -282,80 +285,81 @@ func TestParseFlags(t *testing.T) {
title: "override everything via environment variables",
args: []string{},
envVars: map[string]string{
"EXTERNAL_DNS_MASTER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_VIEW": "internal",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",
"EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud",
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
"EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_ONCE": "1",
"EXTERNAL_DNS_DRY_RUN": "1",
"EXTERNAL_DNS_EVENTS": "1",
"EXTERNAL_DNS_LOG_FORMAT": "json",
"EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099",
"EXTERNAL_DNS_LOG_LEVEL": "debug",
"EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081",
"EXTERNAL_DNS_EXOSCALE_ENDPOINT": "https://api.foo.ch/dns",
"EXTERNAL_DNS_EXOSCALE_APIKEY": "1",
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
"EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1",
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
"EXTERNAL_DNS_RCODEZERO_TXT_ENCRYPT": "1",
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_MASTER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
"EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_GOOGLE_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_VIEW": "internal",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",
"EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud",
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
"EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_ONCE": "1",
"EXTERNAL_DNS_DRY_RUN": "1",
"EXTERNAL_DNS_EVENTS": "1",
"EXTERNAL_DNS_LOG_FORMAT": "json",
"EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099",
"EXTERNAL_DNS_LOG_LEVEL": "debug",
"EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081",
"EXTERNAL_DNS_EXOSCALE_ENDPOINT": "https://api.foo.ch/dns",
"EXTERNAL_DNS_EXOSCALE_APIKEY": "1",
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
"EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1",
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
"EXTERNAL_DNS_RCODEZERO_TXT_ENCRYPT": "1",
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
},
expected: overriddenConfig,
},
......
......@@ -26,7 +26,6 @@ import (
log "github.com/sirupsen/logrus"
"k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers"
......@@ -203,11 +202,7 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
// filterByAnnotations filters a list of ingresses by a given annotation selector.
func (sc *ingressSource) filterByAnnotations(ingresses []*v1beta1.Ingress) ([]*v1beta1.Ingress, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := getLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
......@@ -220,11 +215,8 @@ func (sc *ingressSource) filterByAnnotations(ingresses []*v1beta1.Ingress) ([]*v
filteredList := []*v1beta1.Ingress{}
for _, ingress := range ingresses {
// convert the ingress' annotations to an equivalent label selector
annotations := labels.Set(ingress.Annotations)
// include ingress if its annotations match the selector
if selector.Matches(annotations) {
if matchLabelSelector(selector, ingress.Annotations) {
filteredList = append(filteredList, ingress)
}
}
......
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"text/template"
"time"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
)
const (
defaultIdleConnTimeout = 30 * time.Second
DefaultRoutegroupVersion = "zalando.org/v1"
routeGroupListResource = "/apis/%s/routegroups"
routeGroupNamespacedResource = "/apis/%s/namespaces/%s/routegroups"
)
type routeGroupSource struct {
cli routeGroupListClient
master string
namespace string
apiEndpoint string
annotationFilter string
fqdnTemplate *template.Template
combineFQDNAnnotation bool
ignoreHostnameAnnotation bool
}
// for testing
type routeGroupListClient interface {
getRouteGroupList(string) (*routeGroupList, error)
}
type routeGroupClient struct {
mu sync.Mutex
quit chan struct{}
client *http.Client
token string
tokenFile string
}
func newRouteGroupClient(token, tokenPath string, timeout time.Duration) *routeGroupClient {
const (
tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
if tokenPath != "" {
tokenPath = tokenFile
}
tr := &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: timeout,
IdleConnTimeout: defaultIdleConnTimeout,
MaxIdleConns: 5,
MaxIdleConnsPerHost: 5,
}
cli := &routeGroupClient{
client: &http.Client{
Transport: tr,
},
quit: make(chan struct{}),
tokenFile: tokenPath,
token: token,
}
go func() {
for {
select {
case <-time.After(tr.IdleConnTimeout):
tr.CloseIdleConnections()
cli.updateToken()
case <-cli.quit:
return
}
}
}()
// in cluster config, errors are treated as not running in cluster
cli.updateToken()
// cluster internal use custom CA to reach TLS endpoint
rootCA, err := ioutil.ReadFile(rootCAFile)
if err != nil {
return cli
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(rootCA) {
return cli
}
tr.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: certPool,
}
return cli
}
func (cli *routeGroupClient) updateToken() {
if cli.tokenFile == "" {
return
}
token, err := ioutil.ReadFile(cli.tokenFile)
if err != nil {
log.Errorf("Failed to read token from file (%s): %v", cli.tokenFile, err)
}
cli.mu.Lock()
cli.token = string(token)
cli.mu.Unlock()
}
func (cli *routeGroupClient) getToken() string {
cli.mu.Lock()
defer cli.mu.Unlock()
return cli.token
}
func (cli *routeGroupClient) getRouteGroupList(url string) (*routeGroupList, error) {
resp, err := cli.get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to get routegroup list from %s, got: %s", url, resp.Status)
}
var rgs routeGroupList
err = json.NewDecoder(resp.Body).Decode(&rgs)
if err != nil {
return nil, err
}
return &rgs, nil
}
func (cli *routeGroupClient) get(url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return cli.do(req)
}
func (cli *routeGroupClient) do(req *http.Request) (*http.Response, error) {
if tok := cli.getToken(); tok != "" && req.Header.Get("Authorization") == "" {
req.Header.Set("Authorization", "Bearer "+tok)
}
return cli.client.Do(req)
}
func parseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) {
if fqdnTemplate != "" {
tmpl, err = template.New("endpoint").Funcs(template.FuncMap{
"trimPrefix": strings.TrimPrefix,
}).Parse(fqdnTemplate)
}
return tmpl, err
}
// NewRouteGroupSource creates a new routeGroupSource with the given config.
func NewRouteGroupSource(timeout time.Duration, token, tokenPath, master, namespace, annotationFilter, fqdnTemplate, routegroupVersion string, combineFqdnAnnotation, ignoreHostnameAnnotation bool) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
return nil, err
}
if routegroupVersion == "" {
routegroupVersion = DefaultRoutegroupVersion
}
cli := newRouteGroupClient(token, tokenPath, timeout)
u, err := url.Parse(master)
if err != nil {
return nil, err
}
apiServer := u.String()
// strip port if well known port, because of TLS certifcate match
if u.Scheme == "https" && u.Port() == "443" {
apiServer = "https://" + u.Hostname()
}
sc := &routeGroupSource{
cli: cli,
master: apiServer,
namespace: namespace,
apiEndpoint: apiServer + fmt.Sprintf(routeGroupListResource, routegroupVersion),
annotationFilter: annotationFilter,
fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFqdnAnnotation,
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
}
if namespace != "" {
sc.apiEndpoint = apiServer + fmt.Sprintf(routeGroupNamespacedResource, routegroupVersion, namespace)
}
log.Infoln("Created route group source")
return sc, nil
}
// AddEventHandler for routegroup is currently a no op, because we do not implement caching, yet.
func (sc *routeGroupSource) AddEventHandler(func() error, <-chan struct{}, time.Duration) {}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all routeGroup resources on all namespaces.
// Logic is ported from ingress without fqdnTemplate
func (sc *routeGroupSource) Endpoints() ([]*endpoint.Endpoint, error) {
rgList, err := sc.cli.getRouteGroupList(sc.apiEndpoint)
if err != nil {
log.Errorf("Failed to get RouteGroup list: %v", err)
return nil, err
}
rgList, err = sc.filterByAnnotations(rgList)
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, rg := range rgList.Items {
// Check controller annotation to see if we are responsible.
controller, ok := rg.Metadata.Annotations[controllerAnnotationKey]
if ok && controller != controllerAnnotationValue {
log.Debugf("Skipping routegroup %s/%s because controller value does not match, found: %s, required: %s",
rg.Metadata.Namespace, rg.Metadata.Name, controller, controllerAnnotationValue)
continue
}
eps := sc.endpointsFromRouteGroup(rg)
if (sc.combineFQDNAnnotation || len(eps) == 0) && sc.fqdnTemplate != nil {
tmplEndpoints, err := sc.endpointsFromTemplate(rg)
if err != nil {
return nil, err
}
if sc.combineFQDNAnnotation {
eps = append(eps, tmplEndpoints...)
} else {
eps = tmplEndpoints
}
}
if len(eps) == 0 {
log.Debugf("No endpoints could be generated from routegroup %s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
continue
}
log.Debugf("Endpoints generated from ingress: %s/%s: %v", rg.Metadata.Namespace, rg.Metadata.Name, eps)
sc.setRouteGroupResourceLabel(rg, eps)
sc.setRouteGroupDualstackLabel(rg, eps)
endpoints = append(endpoints, eps...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.Endpoint, error) {
// Process the whole template string
var buf bytes.Buffer
err := sc.fqdnTemplate.Execute(&buf, rg)
if err != nil {
return nil, fmt.Errorf("failed to apply template on routegroup %s/%s: %v", rg.Metadata.Namespace, rg.Metadata.Name, err)
}
hostnames := buf.String()
// error handled in endpointsFromRouteGroup(), otherwise duplicate log
ttl, _ := getTTLFromAnnotations(rg.Metadata.Annotations)
targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
if len(targets) == 0 {
targets = targetsFromRouteGroupStatus(rg.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
}
return endpoints, nil
}
func (sc *routeGroupSource) setRouteGroupResourceLabel(rg *routeGroup, eps []*endpoint.Endpoint) {
for _, ep := range eps {
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
}
}
func (sc *routeGroupSource) setRouteGroupDualstackLabel(rg *routeGroup, eps []*endpoint.Endpoint) {
val, ok := rg.Metadata.Annotations[ALBDualstackAnnotationKey]
if ok && val == ALBDualstackAnnotationValue {
log.Debugf("Adding dualstack label to routegroup %s/%s.", rg.Metadata.Namespace, rg.Metadata.Name)
for _, ep := range eps {
ep.Labels[endpoint.DualstackLabelKey] = "true"
}
}
}
// annotation logic ported from source/ingress.go without Spec.TLS part, because it'S not supported in RouteGroup
func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.Endpoint {
endpoints := []*endpoint.Endpoint{}
ttl, err := getTTLFromAnnotations(rg.Metadata.Annotations)
if err != nil {
log.Warnf("Failed to get TTL from annotation: %v", err)
}
targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations)
if len(targets) == 0 {
for _, lb := range rg.Status.LoadBalancer.RouteGroup {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
for _, src := range rg.Spec.Hosts {
if src == "" {
continue
}
endpoints = append(endpoints, endpointsForHostname(src, targets, ttl, providerSpecific, setIdentifier)...)
}
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(rg.Metadata.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
}
}
return endpoints
}
// filterByAnnotations filters a list of routeGroupList by a given annotation selector.
func (sc *routeGroupSource) filterByAnnotations(rgs *routeGroupList) (*routeGroupList, error) {
selector, err := getLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
// empty filter returns original list
if selector.Empty() {
return rgs, nil
}
var filteredList []*routeGroup
for _, rg := range rgs.Items {
// include ingress if its annotations match the selector
if matchLabelSelector(selector, rg.Metadata.Annotations) {
filteredList = append(filteredList, rg)
}
}
rgs.Items = filteredList
return rgs, nil
}
func targetsFromRouteGroupStatus(status routeGroupStatus) endpoint.Targets {
var targets endpoint.Targets
for _, lb := range status.LoadBalancer.RouteGroup {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
return targets
}
type routeGroupList struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion"`
Metadata routeGroupListMetadata `json:"metadata"`
Items []*routeGroup `json:"items"`
}
type routeGroupListMetadata struct {
SelfLink string `json:"selfLink"`
ResourceVersion string `json:"resourceVersion"`
}
type routeGroup struct {
Metadata itemMetadata `json:"metadata"`
Spec routeGroupSpec `json:"spec"`
Status routeGroupStatus `json:"status"`
}
type itemMetadata struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
}
type routeGroupSpec struct {
Hosts []string `json:"hosts"`
}
type routeGroupStatus struct {
LoadBalancer routeGroupLoadBalancerStatus `json:"loadBalancer"`
}
type routeGroupLoadBalancerStatus struct {
RouteGroup []routeGroupLoadBalancer `json:"routeGroup"`
}
type routeGroupLoadBalancer struct {
IP string `json:"ip,omitempty"`
Hostname string `json:"hostname,omitempty"`
}
This diff is collapsed.
......@@ -24,6 +24,8 @@ import (
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/external-dns/endpoint"
)
......@@ -209,3 +211,16 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
return endpoints
}
func getLabelSelector(annotationFilter string) (labels.Selector, error) {
labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
if err != nil {
return nil, err
}
return metav1.LabelSelectorAsSelector(labelSelector)
}
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
annotations := labels.Set(srcAnnotations)
return selector.Matches(annotations)
}
......@@ -60,6 +60,8 @@ type Config struct {
CFUsername string
CFPassword string
ContourLoadBalancerService string
SkipperRouteGroupVersion string
RequestTimeout time.Duration
}
// ClientGenerator provides clients
......@@ -212,16 +214,24 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
return nil, err
}
return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, scheme)
case "skipper-routegroup":
master := cfg.KubeMaster
tokenPath := ""
token := ""
restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.KubeMaster)
if err == nil {
master = restConfig.Host
tokenPath = restConfig.BearerTokenFile
token = restConfig.BearerToken
}
return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, master, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
}
return nil, ErrSourceNotFound
}
// NewKubeClient returns a new Kubernetes client object. It takes a Config and
// uses KubeMaster and KubeConfig attributes to connect to the cluster. If
// KubeConfig isn't provided it defaults to using the recommended default.
func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*kubernetes.Clientset, error) {
log.Infof("Instantiating new Kubernetes client")
// GetRestConfig returns the rest clients config to get automatically
// data if you run inside a cluster or by passing flags.
func GetRestConfig(kubeConfig, kubeMaster string) (*rest.Config, error) {
if kubeConfig == "" {
if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
kubeConfig = clientcmd.RecommendedHomeFile
......@@ -246,6 +256,20 @@ func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration)
return nil, err
}
return config, nil
}
// NewKubeClient returns a new Kubernetes client object. It takes a Config and
// uses KubeMaster and KubeConfig attributes to connect to the cluster. If
// KubeConfig isn't provided it defaults to using the recommended default.
func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*kubernetes.Clientset, error) {
log.Infof("Instantiating new Kubernetes client")
config, err := GetRestConfig(kubeConfig, kubeMaster)
if err != nil {
return nil, err
}
config.Timeout = requestTimeout
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
......@@ -255,8 +279,6 @@ func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration)
})
}
config.Timeout = requestTimeout
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment