mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
implement RouteGroup with similar feature set to ingress
add documentation for kube-ingress-aws-controller and RouteGroup Signed-off-by: Sandor Szücs <sandor.szuecs@zalando.de>
This commit is contained in:
parent
b3609c4cb9
commit
116856f422
@ -111,6 +111,7 @@ The following tutorials are provided:
|
|||||||
* [Route53](docs/tutorials/aws.md)
|
* [Route53](docs/tutorials/aws.md)
|
||||||
* [Same domain for public and private Route53 zones](docs/tutorials/public-private-route53.md)
|
* [Same domain for public and private Route53 zones](docs/tutorials/public-private-route53.md)
|
||||||
* [Cloud Map](docs/tutorials/aws-sd.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 DNS](docs/tutorials/azure.md)
|
||||||
* [Azure Private DNS](docs/tutorials/azure-private-dns.md)
|
* [Azure Private DNS](docs/tutorials/azure-private-dns.md)
|
||||||
* [Cloudflare](docs/tutorials/cloudflare.md)
|
* [Cloudflare](docs/tutorials/cloudflare.md)
|
||||||
|
307
docs/tutorials/kube-ingress-aws.md
Normal file
307
docs/tutorials/kube-ingress-aws.md
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
# 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=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
|
||||||
|
```
|
2
main.go
2
main.go
@ -91,6 +91,8 @@ func main() {
|
|||||||
CFUsername: cfg.CFUsername,
|
CFUsername: cfg.CFUsername,
|
||||||
CFPassword: cfg.CFPassword,
|
CFPassword: cfg.CFPassword,
|
||||||
ContourLoadBalancerService: cfg.ContourLoadBalancerService,
|
ContourLoadBalancerService: cfg.ContourLoadBalancerService,
|
||||||
|
SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion,
|
||||||
|
RequestTimeout: cfg.RequestTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup all the selected sources by names and pass them the desired configuration.
|
// Lookup all the selected sources by names and pass them the desired configuration.
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alecthomas/kingpin"
|
"github.com/alecthomas/kingpin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"sigs.k8s.io/external-dns/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -42,6 +43,7 @@ type Config struct {
|
|||||||
RequestTimeout time.Duration
|
RequestTimeout time.Duration
|
||||||
IstioIngressGatewayServices []string
|
IstioIngressGatewayServices []string
|
||||||
ContourLoadBalancerService string
|
ContourLoadBalancerService string
|
||||||
|
SkipperRouteGroupVersion string
|
||||||
Sources []string
|
Sources []string
|
||||||
Namespace string
|
Namespace string
|
||||||
AnnotationFilter string
|
AnnotationFilter string
|
||||||
@ -144,6 +146,7 @@ var defaultConfig = &Config{
|
|||||||
RequestTimeout: time.Second * 30,
|
RequestTimeout: time.Second * 30,
|
||||||
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
|
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
|
||||||
ContourLoadBalancerService: "heptio-contour/contour",
|
ContourLoadBalancerService: "heptio-contour/contour",
|
||||||
|
SkipperRouteGroupVersion: "zalando.org/v1",
|
||||||
Sources: nil,
|
Sources: nil,
|
||||||
Namespace: "",
|
Namespace: "",
|
||||||
AnnotationFilter: "",
|
AnnotationFilter: "",
|
||||||
@ -286,8 +289,12 @@ func (cfg *Config) ParseFlags(args []string) error {
|
|||||||
// Flags related to Contour
|
// 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)
|
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
|
// 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("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("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)
|
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: "",
|
KubeConfig: "",
|
||||||
RequestTimeout: time.Second * 30,
|
RequestTimeout: time.Second * 30,
|
||||||
ContourLoadBalancerService: "heptio-contour/contour",
|
ContourLoadBalancerService: "heptio-contour/contour",
|
||||||
|
SkipperRouteGroupVersion: "zalando.org/v1",
|
||||||
Sources: []string{"service"},
|
Sources: []string{"service"},
|
||||||
Namespace: "",
|
Namespace: "",
|
||||||
FQDNTemplate: "",
|
FQDNTemplate: "",
|
||||||
@ -103,6 +104,7 @@ var (
|
|||||||
KubeConfig: "/some/path",
|
KubeConfig: "/some/path",
|
||||||
RequestTimeout: time.Second * 77,
|
RequestTimeout: time.Second * 77,
|
||||||
ContourLoadBalancerService: "heptio-contour-other/contour-other",
|
ContourLoadBalancerService: "heptio-contour-other/contour-other",
|
||||||
|
SkipperRouteGroupVersion: "zalando.org/v2",
|
||||||
Sources: []string{"service", "ingress", "connector"},
|
Sources: []string{"service", "ingress", "connector"},
|
||||||
Namespace: "namespace",
|
Namespace: "namespace",
|
||||||
IgnoreHostnameAnnotation: true,
|
IgnoreHostnameAnnotation: true,
|
||||||
@ -199,6 +201,7 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"--kubeconfig=/some/path",
|
"--kubeconfig=/some/path",
|
||||||
"--request-timeout=77s",
|
"--request-timeout=77s",
|
||||||
"--contour-load-balancer=heptio-contour-other/contour-other",
|
"--contour-load-balancer=heptio-contour-other/contour-other",
|
||||||
|
"--skipper-routegroup-groupversion=zalando.org/v2",
|
||||||
"--source=service",
|
"--source=service",
|
||||||
"--source=ingress",
|
"--source=ingress",
|
||||||
"--source=connector",
|
"--source=connector",
|
||||||
@ -286,6 +289,7 @@ func TestParseFlags(t *testing.T) {
|
|||||||
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
|
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
|
||||||
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
|
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
|
||||||
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
|
"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_SOURCE": "service\ningress\nconnector",
|
||||||
"EXTERNAL_DNS_NAMESPACE": "namespace",
|
"EXTERNAL_DNS_NAMESPACE": "namespace",
|
||||||
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
|
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"k8s.io/api/extensions/v1beta1"
|
"k8s.io/api/extensions/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
kubeinformers "k8s.io/client-go/informers"
|
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.
|
// filterByAnnotations filters a list of ingresses by a given annotation selector.
|
||||||
func (sc *ingressSource) filterByAnnotations(ingresses []*v1beta1.Ingress) ([]*v1beta1.Ingress, error) {
|
func (sc *ingressSource) filterByAnnotations(ingresses []*v1beta1.Ingress) ([]*v1beta1.Ingress, error) {
|
||||||
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
|
selector, err := getLabelSelector(sc.annotationFilter)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -220,11 +215,8 @@ func (sc *ingressSource) filterByAnnotations(ingresses []*v1beta1.Ingress) ([]*v
|
|||||||
filteredList := []*v1beta1.Ingress{}
|
filteredList := []*v1beta1.Ingress{}
|
||||||
|
|
||||||
for _, ingress := range ingresses {
|
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
|
// include ingress if its annotations match the selector
|
||||||
if selector.Matches(annotations) {
|
if matchLabelSelector(selector, ingress.Annotations) {
|
||||||
filteredList = append(filteredList, ingress)
|
filteredList = append(filteredList, ingress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
463
source/routegroup.go
Normal file
463
source/routegroup.go
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
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"`
|
||||||
|
}
|
841
source/routegroup_test.go
Normal file
841
source/routegroup_test.go
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTestRouteGroup(ns, name string, annotations map[string]string, hosts []string, destinations []routeGroupLoadBalancer) *routeGroup {
|
||||||
|
return &routeGroup{
|
||||||
|
Metadata: itemMetadata{
|
||||||
|
Namespace: ns,
|
||||||
|
Name: name,
|
||||||
|
Annotations: annotations,
|
||||||
|
},
|
||||||
|
Spec: routeGroupSpec{
|
||||||
|
Hosts: hosts,
|
||||||
|
},
|
||||||
|
Status: routeGroupStatus{
|
||||||
|
LoadBalancer: routeGroupLoadBalancerStatus{
|
||||||
|
RouteGroup: destinations,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointsFromRouteGroups(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
source *routeGroupSource
|
||||||
|
rg *routeGroup
|
||||||
|
want []*endpoint.Endpoint
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty routegroup should return empty endpoints",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: &routeGroup{},
|
||||||
|
want: []*endpoint.Endpoint{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup without hosts and destinations create no endpoints",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup("namespace1", "rg1", nil, nil, nil),
|
||||||
|
want: []*endpoint.Endpoint{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup without hosts create no endpoints",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup("namespace1", "rg1", nil, nil, []routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []*endpoint.Endpoint{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup without destinations create no endpoints",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup("namespace1", "rg1", nil, []string{"rg1.k8s.example"}, nil),
|
||||||
|
want: []*endpoint.Endpoint{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and destinations creates an endpoint",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup("namespace1", "rg1", nil, []string{"rg1.k8s.example"}, []routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hostname annotation, creates endpoints from the annotation ",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
hostnameAnnotationKey: "my.example",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "my.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and destinations and ignoreHostnameAnnotation creates endpoints but ignores annotation",
|
||||||
|
source: &routeGroupSource{ignoreHostnameAnnotation: true},
|
||||||
|
rg: createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
hostnameAnnotationKey: "my.example",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and destinations and ttl creates an endpoint with ttl",
|
||||||
|
source: &routeGroupSource{ignoreHostnameAnnotation: true},
|
||||||
|
rg: createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
ttlAnnotationKey: "2189",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
RecordTTL: endpoint.TTL(2189),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and destination IP creates an endpoint",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
IP: "1.5.1.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"1.5.1.4"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and mixed destinations creates endpoints",
|
||||||
|
source: &routeGroupSource{},
|
||||||
|
rg: createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
IP: "1.5.1.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"1.5.1.4"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.source.endpointsFromRouteGroup(tt.rg)
|
||||||
|
|
||||||
|
validateEndpoints(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRouteGroupClient struct {
|
||||||
|
returnErr bool
|
||||||
|
rg *routeGroupList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRouteGroupClient) getRouteGroupList(string) (*routeGroupList, error) {
|
||||||
|
if f.returnErr {
|
||||||
|
return nil, errors.New("Fake route group list error")
|
||||||
|
}
|
||||||
|
return f.rg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteGroupsEndpoints(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
source *routeGroupSource
|
||||||
|
fqdnTemplate string
|
||||||
|
want []*endpoint.Endpoint
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty routegroup should return empty endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single routegroup should return endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single routegroup with combineFQDNAnnotation with fqdn template should return endpoints from fqdnTemplate and routegroup",
|
||||||
|
fqdnTemplate: "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
combineFQDNAnnotation: true,
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg1.namespace1.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single routegroup without, with fqdn template should return endpoints from fqdnTemplate",
|
||||||
|
fqdnTemplate: "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.namespace1.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single routegroup without combineFQDNAnnotation with fqdn template should return endpoints not from fqdnTemplate",
|
||||||
|
fqdnTemplate: "{{.Metadata.Name}}.{{.Metadata.Namespace}}.example",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single routegroup with TTL should return endpoint with TTL",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
ttlAnnotationKey: "2189",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
RecordTTL: endpoint.TTL(2189),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routegroup with hosts and mixed destinations creates endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
IP: "1.5.1.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"1.5.1.4"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routegroups should return endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg2",
|
||||||
|
nil,
|
||||||
|
[]string{"rg2.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace2",
|
||||||
|
"rg3",
|
||||||
|
nil,
|
||||||
|
[]string{"rg3.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace3",
|
||||||
|
"rg",
|
||||||
|
nil,
|
||||||
|
[]string{"rg.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb2.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg2.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg3.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb2.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routegroups with filter annotations should return only filtered endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
annotationFilter: "kubernetes.io/ingress.class=skipper",
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "skipper",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg2",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "nginx",
|
||||||
|
},
|
||||||
|
[]string{"rg2.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace2",
|
||||||
|
"rg3",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "",
|
||||||
|
},
|
||||||
|
[]string{"rg3.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace3",
|
||||||
|
"rg",
|
||||||
|
nil,
|
||||||
|
[]string{"rg.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb2.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routegroups with set operation annotation filter should return only filtered endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
annotationFilter: "kubernetes.io/ingress.class in (nginx, skipper)",
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "skipper",
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg2",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "nginx",
|
||||||
|
},
|
||||||
|
[]string{"rg2.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace2",
|
||||||
|
"rg3",
|
||||||
|
map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": "",
|
||||||
|
},
|
||||||
|
[]string{"rg3.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace3",
|
||||||
|
"rg",
|
||||||
|
nil,
|
||||||
|
[]string{"rg.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb2.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg2.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple routegroups with controller annotation filter should not return filtered endpoints",
|
||||||
|
source: &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
controllerAnnotationKey: controllerAnnotationValue,
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg2",
|
||||||
|
map[string]string{
|
||||||
|
controllerAnnotationKey: "dns",
|
||||||
|
},
|
||||||
|
[]string{"rg2.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace2",
|
||||||
|
"rg3",
|
||||||
|
nil,
|
||||||
|
[]string{"rg3.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []*endpoint.Endpoint{
|
||||||
|
{
|
||||||
|
DNSName: "rg1.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DNSName: "rg3.k8s.example",
|
||||||
|
Targets: endpoint.Targets([]string{"lb.example.org"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.fqdnTemplate != "" {
|
||||||
|
println("fqdnTemplate is set")
|
||||||
|
tmpl, err := parseTemplate(tt.fqdnTemplate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse template: %v", err)
|
||||||
|
}
|
||||||
|
tt.source.fqdnTemplate = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := tt.source.Endpoints()
|
||||||
|
if err != nil && !tt.wantErr {
|
||||||
|
t.Errorf("Got error, but does not want to get an error: %v", err)
|
||||||
|
}
|
||||||
|
if tt.wantErr && err == nil {
|
||||||
|
t.Fatal("Got no error, but we want to get an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEndpoints(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceLabelIsSet(t *testing.T) {
|
||||||
|
source := &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
nil,
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, _ := source.Endpoints()
|
||||||
|
for _, ep := range got {
|
||||||
|
if _, ok := ep.Labels[endpoint.ResourceLabelKey]; !ok {
|
||||||
|
t.Errorf("Failed to set resource label on ep %v", ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDualstackLabelIsSet(t *testing.T) {
|
||||||
|
source := &routeGroupSource{
|
||||||
|
cli: &fakeRouteGroupClient{
|
||||||
|
rg: &routeGroupList{
|
||||||
|
Items: []*routeGroup{
|
||||||
|
createTestRouteGroup(
|
||||||
|
"namespace1",
|
||||||
|
"rg1",
|
||||||
|
map[string]string{
|
||||||
|
ALBDualstackAnnotationKey: ALBDualstackAnnotationValue,
|
||||||
|
},
|
||||||
|
[]string{"rg1.k8s.example"},
|
||||||
|
[]routeGroupLoadBalancer{
|
||||||
|
{
|
||||||
|
Hostname: "lb.example.org",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, _ := source.Endpoints()
|
||||||
|
for _, ep := range got {
|
||||||
|
if v, ok := ep.Labels[endpoint.DualstackLabelKey]; !ok || v != "true" {
|
||||||
|
t.Errorf("Failed to set resource label on ep %v", ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTemplate(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
annotationFilter string
|
||||||
|
fqdnTemplate string
|
||||||
|
combineFQDNAndAnnotation bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid template",
|
||||||
|
expectError: true,
|
||||||
|
fqdnTemplate: "{{.Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid empty template",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid template",
|
||||||
|
expectError: false,
|
||||||
|
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid template",
|
||||||
|
expectError: false,
|
||||||
|
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid template",
|
||||||
|
expectError: false,
|
||||||
|
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
|
||||||
|
combineFQDNAndAnnotation: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-empty annotation filter label",
|
||||||
|
expectError: false,
|
||||||
|
annotationFilter: "kubernetes.io/ingress.class=nginx",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := parseTemplate(tt.fqdnTemplate)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -209,3 +211,16 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
|
|||||||
|
|
||||||
return endpoints
|
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
|
CFUsername string
|
||||||
CFPassword string
|
CFPassword string
|
||||||
ContourLoadBalancerService string
|
ContourLoadBalancerService string
|
||||||
|
SkipperRouteGroupVersion string
|
||||||
|
RequestTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientGenerator provides clients
|
// ClientGenerator provides clients
|
||||||
@ -212,16 +214,24 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, scheme)
|
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
|
return nil, ErrSourceNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKubeClient returns a new Kubernetes client object. It takes a Config and
|
// GetRestConfig returns the rest clients config to get automatically
|
||||||
// uses KubeMaster and KubeConfig attributes to connect to the cluster. If
|
// data if you run inside a cluster or by passing flags.
|
||||||
// KubeConfig isn't provided it defaults to using the recommended default.
|
func GetRestConfig(kubeConfig, kubeMaster string) (*rest.Config, error) {
|
||||||
func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*kubernetes.Clientset, error) {
|
|
||||||
log.Infof("Instantiating new Kubernetes client")
|
|
||||||
|
|
||||||
if kubeConfig == "" {
|
if kubeConfig == "" {
|
||||||
if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
|
if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
|
||||||
kubeConfig = clientcmd.RecommendedHomeFile
|
kubeConfig = clientcmd.RecommendedHomeFile
|
||||||
@ -246,6 +256,20 @@ func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration)
|
|||||||
return nil, err
|
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 {
|
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||||
return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{
|
return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{
|
||||||
PathProcessor: func(path string) string {
|
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)
|
client, err := kubernetes.NewForConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
Loading…
Reference in New Issue
Block a user