Merge branch 'master' into issue-239-multiple-targets

This commit is contained in:
Gary Kramlich 2018-04-12 12:11:15 -05:00
commit 0e8354795f
25 changed files with 1726 additions and 224 deletions

View File

@ -6,7 +6,7 @@ os:
language: go language: go
go: go:
- 1.9 - 1.x
- tip - tip
matrix: matrix:
@ -16,6 +16,7 @@ matrix:
before_install: before_install:
- make dep - make dep
- go get github.com/mattn/goveralls - go get github.com/mattn/goveralls
- go get github.com/lawrencewoodman/roveralls
- go get github.com/alecthomas/gometalinter - go get github.com/alecthomas/gometalinter
install: install:
@ -24,4 +25,5 @@ install:
script: script:
- vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=$(pwd) - vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=$(pwd)
- vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir $(pwd) - vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir $(pwd)
- travis_wait 20 goveralls -service=travis-ci - travis_wait 20 roveralls
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci

View File

@ -1,3 +1,6 @@
- Add a flag that allows FQDN template and annotations to combine (#513) @helgi
- Fix: Use PodIP instead of HostIP for headless Services (#498) @nrobert13
- Support a comma separated list for the FQDN template (#512) @helgi
- Google Provider: Add auto-detection of Google Project when running on GCP (#492) @drzero42 - Google Provider: Add auto-detection of Google Project when running on GCP (#492) @drzero42
- Add custom TTL support for DNSimple (#477) @jbowes - Add custom TTL support for DNSimple (#477) @jbowes
- Fix docker build and delete vendor files which were not deleted (#473) @njuettner - Fix docker build and delete vendor files which were not deleted (#473) @njuettner

View File

@ -13,7 +13,7 @@
# limitations under the License. # limitations under the License.
# builder image # builder image
FROM golang:1.9 as builder FROM golang as builder
WORKDIR /go/src/github.com/kubernetes-incubator/external-dns WORKDIR /go/src/github.com/kubernetes-incubator/external-dns
COPY . . COPY . .

8
Gopkg.lock generated
View File

@ -226,6 +226,12 @@
packages = ["."] packages = ["."]
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
[[projects]]
branch = "master"
name = "github.com/gophercloud/gophercloud"
packages = ["."]
revision = "bfc4756e1a693a850d7d459f4b28b21f35a24b5a"
[[projects]] [[projects]]
name = "github.com/howeyc/gopass" name = "github.com/howeyc/gopass"
packages = ["."] packages = ["."]
@ -622,6 +628,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "7af57b8d195abce34060c82b92243ddba947f5d882be8f9a08b1aa827a9061fa" inputs-digest = "3cc043ee8be5b7cdba42793f12259ad23e13608d4b6a18523e540e14f029c0b0"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -159,6 +159,15 @@ Here's a rough outline on what is to come (subject to change):
- [x] Support for DigitalOcean - [x] Support for DigitalOcean
- [x] Multiple DNS names per Service - [x] Multiple DNS names per Service
### v0.5
- [ ] Support for creating DNS records to multiple targets
### v0.6
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/master/dns-controller) (This could also directly become `v1.0`)
### v1.0 ### v1.0
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/master/dns-controller) - [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/master/dns-controller)
@ -167,11 +176,11 @@ Here's a rough outline on what is to come (subject to change):
### Yet to be defined ### Yet to be defined
* Support for CoreDNS and Azure DNS * Support for CoreDNS
* Support for record weights * Support for record weights
* Support for different behavioral policies * Support for different behavioral policies
* Support for Services with `type=NodePort` * Support for Services with `type=NodePort`
* Support for TPRs * Support for CRDs
* Support for more advanced DNS record configurations * Support for more advanced DNS record configurations
Have a look at [the milestones](https://github.com/kubernetes-incubator/external-dns/milestones) to get an idea of where we currently stand. Have a look at [the milestones](https://github.com/kubernetes-incubator/external-dns/milestones) to get an idea of where we currently stand.

View File

@ -40,7 +40,7 @@ Services exposed via `type=LoadBalancer` and for the hostnames defined in Ingres
### How do I specify DNS name for my Kubernetes objects? ### How do I specify DNS name for my Kubernetes objects?
There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below: There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below:
1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value. 1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
@ -48,6 +48,10 @@ There are three sources of information for ExternalDNS to decide on DNS name. Ex
3. If `--fqdn-template` flag is specified, e.g. `--fqdn-template={{.Name}}.my-org.com`, ExternalDNS will use service/ingress specifications for the provided template to generate DNS name. 3. If `--fqdn-template` flag is specified, e.g. `--fqdn-template={{.Name}}.my-org.com`, ExternalDNS will use service/ingress specifications for the provided template to generate DNS name.
### Can I specify multiple global FQDN templates?
Yes, yes you can. Pass in a comma separated list to `--fqdn-template`. Beaware this will double (triple, etc) the amount of DNS entries based on how many services, ingresses and so on you have and will get you faster towards the API request limit of your DNS provider.
### Which Service and Ingress controllers are supported? ### Which Service and Ingress controllers are supported?
Regarding Services, we'll support the OSI Layer 4 load balancers that Kubernetes creates on AWS and Google Container Engine, and possibly other clusters running on Google Compute Engine. Regarding Services, we'll support the OSI Layer 4 load balancers that Kubernetes creates on AWS and Google Container Engine, and possibly other clusters running on Google Compute Engine.
@ -91,13 +95,13 @@ Yes — Zalando replaced [Mate](https://github.com/linki/mate) with ExternalDNS
### How can we start using ExternalDNS? ### How can we start using ExternalDNS?
Check out the following descriptive tutorials on how to run ExternalDNS in [GKE](tutorials/gke.md) and [AWS](tutorials/aws.md). Check out the following descriptive tutorials on how to run ExternalDNS in [GKE](tutorials/gke.md) and [AWS](tutorials/aws.md).
### Why is ExternalDNS only adding a single IP address in Route 53 on AWS when using the `nginx-ingress-controller`? How do I get it to use the FQDN of the ELB assigned to my `nginx-ingress-controller` Service instead? ### Why is ExternalDNS only adding a single IP address in Route 53 on AWS when using the `nginx-ingress-controller`? How do I get it to use the FQDN of the ELB assigned to my `nginx-ingress-controller` Service instead?
By default the `nginx-ingress-controller` assigns a single IP address to an Ingress resource when it's created. ExternalDNS uses what's assigned to the Ingress resource, so it too will use this single IP address when adding the record in Route 53. By default the `nginx-ingress-controller` assigns a single IP address to an Ingress resource when it's created. ExternalDNS uses what's assigned to the Ingress resource, so it too will use this single IP address when adding the record in Route 53.
In most AWS deployments, you'll instead want the Route 53 entry to be the FQDN of the ELB that is assigned to the `nginx-ingress-controller` Service. To accomplish this, when you create the `nginx-ingress-controller` Deployment, you need to provide the `--publish-service` option to the `/nginx-ingress-controller` executable under `args`. Once this is deployed new Ingress resources will get the ELB's FQDN and ExternalDNS will use the same when creating records in Route 53. In most AWS deployments, you'll instead want the Route 53 entry to be the FQDN of the ELB that is assigned to the `nginx-ingress-controller` Service. To accomplish this, when you create the `nginx-ingress-controller` Deployment, you need to provide the `--publish-service` option to the `/nginx-ingress-controller` executable under `args`. Once this is deployed new Ingress resources will get the ELB's FQDN and ExternalDNS will use the same when creating records in Route 53.
According to the `nginx-ingress-controller` [docs](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) the value you need to provide `--publish-service` is: According to the `nginx-ingress-controller` [docs](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) the value you need to provide `--publish-service` is:
@ -208,7 +212,7 @@ Sometimes you need to run an internal and an external dns service.
The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external
one to expose DNS to the internet. one to expose DNS to the internet.
To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to
an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external` an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal` then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal`
and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`. and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`.

View File

@ -1,3 +1,4 @@
# Setting up ExternalDNS for Services on Azure # Setting up ExternalDNS for Services on Azure
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Azure. This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Azure.
@ -48,56 +49,80 @@ To create the secret:
``` ```
$ kubectl create secret generic azure-config-file --from-file=/etc/kubernetes/azure.json $ kubectl create secret generic azure-config-file --from-file=/etc/kubernetes/azure.json
```
### Azure Kubernetes Services (aka AKS)
When your cluster is created, unlike ACS there are no Azure credentials stored and you must create an azure.json object manually like with other hosting providers. In order to create the azure.json you must first create an Azure AD service principal in the Azure AD tenant linked to your Azure subscription that is hosting your DNS zone.
#### Create service principal
A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
```
>az login
...
# find the relevant subscription and set the az context. id = subscriptionId value in the azure.json.
>az account list
{
"cloudName": "AzureCloud",
"id": "<subscriptionId GUID>",
"isDefault": false,
"name": "My Subscription",
"state": "Enabled",
"tenantId": "AzureAD tenant ID",
"user": {
"name": "name",
"type": "user"
}
>az account set -s id
...
>az group show --name externaldns
{
"id": "/subscriptions/id/resourceGroups/externaldns",
...
}
# use the id from the previous step in the scopes argument
>az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/id/resourceGroups/externaldns" -n ExternalDnsServicePrincipal
{
"appId": "appId GUID", <-- aadClientId value
...
"password": "password", <-- aadClientSecret value
"tenant": "AzureAD Tenant Id" <-- tenantId value
}
...
``` ```
### Other hosting providers ### Other hosting providers
If the Kubernetes cluster is not hosted by Azure Container Services and you still want to use Azure DNS, you need to create the secret manually. The secret should contain an object named azure.json with content similar to this: If the Kubernetes cluster is not hosted by Azure Container Services and you still want to use Azure DNS, you need to create the secret manually. The secret should contain an object named azure.json with content similar to this:
``` ```
{ {
"tenantId": "837b898d-7dd5-4967-b718-7dfd25878104", "tenantId": "AzureAD tenant Id",
"subscriptionId": "670d2139-c4ef-4a98-8f38-b7052d5a06b2", "subscriptionId": "Id",
"aadClientId": "a0b083bd-c0fc-473d-be48-e2a4df3ec908", "aadClientId": "Service Principal AppId",
"aadClientSecret": "11c78103-8109-40af-a6d4-3db265fed095", "aadClientSecret": "Service Principal Password",
"resourceGroup": "MyDnsResourceGroup", "resourceGroup": "MyDnsResourceGroup",
} }
``` ```
If you have all the information necessary: create a file called azure.json containing the json structure above and substitute the values. Otherwise create a service principal as shown below before creating the Kubernetes secret. If you have all the information necessary: create a file called azure.json containing the json structure above and substitute the values. Otherwise create a service principal as previously shown before creating the Kubernetes secret.
Then add the secret to the Kubernetes cluster before continuing: Then add the secret to the Kubernetes cluster before continuing:
``` ```
kubectl create secret generic azure-config-file --from-file=azure.json kubectl create secret generic azure-config-file --from-file=azure.json
``` ```
#### (Optional) Create service principal
A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example of how you can create a resource group, service principal and dns resource pointing out key information you need to put in the azure.json file.
```
>az login
...
# find the relevant subscription and set the az context. This is the "subscriptionId" value.
>az account set --subscription "670d2139-c4ef-4a98-8f38-b7052d5a06b2"
...
>az group create --name MyDnsResourceGroup --location "West Europe"
{
"id": "/subscriptions/670d2139-c4ef-4a98-8f38-b7052d5a06b2/resourceGroups/MyDnsResourceGroup",
...
}
# use the id from the previous step in the scopes argument
>az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/670d2139-c4ef-4a98-8f38-b7052d5a06b2/resourceGroups/MyDnsResourceGroup" -n ExternalDnsServicePrincipal
{
"appId": "a0b083bd-c0fc-473d-be48-e2a4df3ec908", <-- aadClientId value
...
"password": "11c78103-8109-40af-a6d4-3db265fed095", <-- aadClientSecret value
"tenant": "837b898d-7dd5-4967-b718-7dfd25878104" <-- tenantId value
}
>az network dns zone create -g MyDnsResourceGroup -n example.com
...
```
## Deploy ExternalDNS ## Deploy ExternalDNS
This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.
Ensure that your nginx-ingress deployment has the following arg: added to it:
```
- --publish-service=namespace/nginx-ingress-controller-svcname
```
For more details see here: [nginx-ingress external-dns](https://github.com/kubernetes-incubator/external-dns/blob/master/docs/faq.md#why-is-externaldns-only-adding-a-single-ip-address-in-route-53-on-aws-when-using-the-nginx-ingress-controller-how-do-i-get-it-to-use-the-fqdn-of-the-elb-assigned-to-my-nginx-ingress-controller-service-instead)
Connect your `kubectl` client to the cluster you want to test ExternalDNS with. Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS. Then apply one of the following manifests file to deploy ExternalDNS.
@ -121,6 +146,7 @@ spec:
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8 image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
args: args:
- --source=service - --source=service
- --source=ingress
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=azure - --provider=azure
- --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group - --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group
@ -184,6 +210,7 @@ spec:
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8 image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
args: args:
- --source=service - --source=service
- --source=ingress
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=azure - --provider=azure
- --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group - --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group
@ -227,36 +254,43 @@ spec:
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: nginx name: nginx-svc
annotations:
external-dns.alpha.kubernetes.io/hostname: example.com
spec: spec:
ports:
- port: 80
protocol: tcp
targetPort: 80
selector: selector:
app: nginx app: nginx
type: LoadBalancer type: ClusterIP
ports:
- protocol: TCP ---
port: 80 apiVersion: extensions/v1beta1
targetPort: 80 kind: Ingress
metadata:
name: nginx
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: server.example.com
http:
paths:
- backend:
serviceName: nginx-svc
servicePort: 80
path: /
``` ```
Note the annotation on the service; use the same hostname as the Azure DNS zone created above. The annotation may also be a subdomain When using external-dns with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the external-dns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered.
of the DNS zone (e.g. 'www.example.com').
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation Create the deployment, service and ingress object:
will cause ExternalDNS to remove the corresponding DNS records.
Create the deployment and service:
``` ```
$ kubectl create -f nginx.yaml $ kubectl create -f nginx.yaml
``` ```
It takes a little while for the Azure cloud provider to create an external IP for the service. Check the status by running Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute.
`kubectl get services nginx`. If the `EXTERNAL-IP` field shows an address, the service is ready to be accessed externally.
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
the Azure DNS records.
## Verifying Azure DNS records ## Verifying Azure DNS records

155
docs/tutorials/designate.md Normal file
View File

@ -0,0 +1,155 @@
# Setting up ExternalDNS for Services on OpenStack Designate
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OpenStack Designate DNS.
## Authenticating with OpenStack
We are going to use OpenStack CLI - `openstack` utility, which is an umbrella application for most of OpenStack clients including `designate`.
All OpenStack CLIs require authentication parameters to be provided. These parameters include:
* URL of the OpenStack identity service (`keystone`) which is responsible for user authentication and also served as a registry for other
OpenStack services. Designate endpoints must be registered in `keystone` in order to ExternalDNS and OpenStack CLI be able to find them.
* OpenStack region name
* User login name.
* User project (tenant) name.
* User domain (only when using keystone API v3)
Although these parameters can be passed explicitly through the CLI flags, traditionally it is done by sourcing `openrc` file (`source ~/openrc`) that is a
shell snippet that sets environment variables that all OpenStack CLI understand by convention.
Recent versions of OpenStack Dashboard have a nice UI to download `openrc` file for both v2 and v3 auth protocols. Both protocols can be used with ExternalDNS.
v3 is generally preferred over v2, but might not be available in some OpenStack installations.
## Installing OpenStack Designate
Please refer to the Designate deployment [tutorial](https://docs.openstack.org/project-install-guide/dns/ocata/install.html) for instructions on how
to install and test Designate with BIND backend. You will be required to have admin rights in existing OpenStack installation to do this. One convenient
way to get yourself an OpenStack installation to play with is to use [DevStack](https://docs.openstack.org/devstack/latest/).
## Creating DNS zones
All domain names that are ExternalDNS is going to create must belong to one of DNS zones created in advance. Here is an example of how to create `example.com` DNS zone:
```console
$ openstack zone create --email dnsmaster@example.com example.com.
```
It is important to manually create all the zones that are going to be used for kubernetes entities (ExternalDNS sources) before starting ExternalDNS.
## Deploy ExternalDNS
Create a deployment file called `externaldns.yaml` with the following contents:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=designate
env: # values from openrc file
- name: OS_AUTH_URL
value: http://controller/identity/v3
- name: OS_REGION_NAME
value: RegionOne
- name: OS_USERNAME
value: admin
- name: OS_PASSWORD
value: p@ssw0rd
- name: OS_PROJECT_NAME
value: demo
- name: OS_USER_DOMAIN_NAME
value: Default
```
Create the deployment for ExternalDNS:
```console
$ kubectl create -f externaldns.yaml
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: my-app.example.com
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Note the annotation on the service; use the same hostname as the DNS zone created above.
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.
Create the deployment and service:
```console
$ kubectl create -f nginx.yaml
```
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and notify Designate,
which in turn synchronize DNS records with underlying DNS server backend.
## Verifying DNS records
To verify that DNS record was indeed created, you can use the following command:
```console
$ openstack recordset list example.com.
```
There should be a record for my-app.example.com having `ACTIVE` status. And of course, the ultimate method to verify is to issue a DNS query:
```console
$ dig my-app.example.com @controller
```
## Cleanup
Now that we have verified that ExternalDNS created all DNS records, we can delete the tutorial's example:
```console
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
```

View File

@ -76,7 +76,6 @@ spec:
labels: labels:
app: external-dns app: external-dns
spec: spec:
serviceAccountName: external-dns
containers: containers:
- name: external-dns - name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8 image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
@ -85,7 +84,7 @@ spec:
- --source=ingress - --source=ingress
- --domain-filter=external-dns-test.gcp.zalan.do # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.gcp.zalan.do # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=google - --provider=google
- --google-project=zalando-external-dns-test # - --google-project=zalando-external-dns-test # Use this to specify a project different from the one external-dns is running inside
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --registry=txt - --registry=txt
- --txt-owner-id=my-identifier - --txt-owner-id=my-identifier
@ -135,6 +134,7 @@ spec:
labels: labels:
app: external-dns app: external-dns
spec: spec:
serviceAccountName: external-dns
containers: containers:
- name: external-dns - name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8 image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
@ -143,7 +143,7 @@ spec:
- --source=ingress - --source=ingress
- --domain-filter=external-dns-test.gcp.zalan.do # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.gcp.zalan.do # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=google - --provider=google
- --google-project=zalando-external-dns-test # - --google-project=zalando-external-dns-test # Use this to specify a project different from the one external-dns is running inside
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --registry=txt - --registry=txt
- --txt-owner-id=my-identifier - --txt-owner-id=my-identifier

View File

@ -255,6 +255,7 @@ spec:
labels: labels:
app: external-dns app: external-dns
spec: spec:
serviceAccountName: external-dns
containers: containers:
- name: external-dns - name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8 image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8

13
main.go
View File

@ -67,11 +67,12 @@ func main() {
// Create a source.Config from the flags passed by the user. // Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{ sourceCfg := &source.Config{
Namespace: cfg.Namespace, Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter, AnnotationFilter: cfg.AnnotationFilter,
FQDNTemplate: cfg.FQDNTemplate, FQDNTemplate: cfg.FQDNTemplate,
Compatibility: cfg.Compatibility, CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
PublishInternal: cfg.PublishInternal, Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
} }
// 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.
@ -133,6 +134,8 @@ func main() {
) )
case "inmemory": case "inmemory":
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
case "designate":
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
default: default:
log.Fatalf("unknown dns provider: %s", cfg.Provider) log.Fatalf("unknown dns provider: %s", cfg.Provider)
} }

View File

@ -36,78 +36,80 @@ var (
// Config is a project-wide configuration // Config is a project-wide configuration
type Config struct { type Config struct {
Master string Master string
KubeConfig string KubeConfig string
Sources []string Sources []string
Namespace string Namespace string
AnnotationFilter string AnnotationFilter string
FQDNTemplate string FQDNTemplate string
Compatibility string CombineFQDNAndAnnotation bool
PublishInternal bool Compatibility string
Provider string PublishInternal bool
GoogleProject string Provider string
DomainFilter []string GoogleProject string
ZoneIDFilter []string DomainFilter []string
AWSZoneType string ZoneIDFilter []string
AzureConfigFile string AWSZoneType string
AzureResourceGroup string AzureConfigFile string
CloudflareProxied bool AzureResourceGroup string
InfobloxGridHost string CloudflareProxied bool
InfobloxWapiPort int InfobloxGridHost string
InfobloxWapiUsername string InfobloxWapiPort int
InfobloxWapiPassword string InfobloxWapiUsername string
InfobloxWapiVersion string InfobloxWapiPassword string
InfobloxSSLVerify bool InfobloxWapiVersion string
DynCustomerName string InfobloxSSLVerify bool
DynUsername string DynCustomerName string
DynPassword string DynUsername string
DynMinTTLSeconds int DynPassword string
InMemoryZones []string DynMinTTLSeconds int
Policy string InMemoryZones []string
Registry string Policy string
TXTOwnerID string Registry string
TXTPrefix string TXTOwnerID string
Interval time.Duration TXTPrefix string
Once bool Interval time.Duration
DryRun bool Once bool
LogFormat string DryRun bool
MetricsAddress string LogFormat string
LogLevel string MetricsAddress string
LogLevel string
} }
var defaultConfig = &Config{ var defaultConfig = &Config{
Master: "", Master: "",
KubeConfig: "", KubeConfig: "",
Sources: nil, Sources: nil,
Namespace: "", Namespace: "",
AnnotationFilter: "", AnnotationFilter: "",
FQDNTemplate: "", FQDNTemplate: "",
Compatibility: "", CombineFQDNAndAnnotation: false,
PublishInternal: false, Compatibility: "",
Provider: "", PublishInternal: false,
GoogleProject: "", Provider: "",
DomainFilter: []string{}, GoogleProject: "",
AWSZoneType: "", DomainFilter: []string{},
AzureConfigFile: "/etc/kubernetes/azure.json", AWSZoneType: "",
AzureResourceGroup: "", AzureConfigFile: "/etc/kubernetes/azure.json",
CloudflareProxied: false, AzureResourceGroup: "",
InfobloxGridHost: "", CloudflareProxied: false,
InfobloxWapiPort: 443, InfobloxGridHost: "",
InfobloxWapiUsername: "admin", InfobloxWapiPort: 443,
InfobloxWapiPassword: "", InfobloxWapiUsername: "admin",
InfobloxWapiVersion: "2.3.1", InfobloxWapiPassword: "",
InfobloxSSLVerify: true, InfobloxWapiVersion: "2.3.1",
InMemoryZones: []string{}, InfobloxSSLVerify: true,
Policy: "sync", InMemoryZones: []string{},
Registry: "txt", Policy: "sync",
TXTOwnerID: "default", Registry: "txt",
TXTPrefix: "", TXTOwnerID: "default",
Interval: time.Minute, TXTPrefix: "",
Once: false, Interval: time.Minute,
DryRun: false, Once: false,
LogFormat: "text", DryRun: false,
MetricsAddress: ":7979", LogFormat: "text",
LogLevel: logrus.InfoLevel.String(), MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
} }
// NewConfig returns new Config object // NewConfig returns new Config object
@ -151,15 +153,16 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake") app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake")
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)").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)
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule") app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule")
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
// Flags related to providers // Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "inmemory") app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "desginate", "inmemory")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile) app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup) app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)

85
pkg/tlsutils/tlsconfig.go Normal file
View File

@ -0,0 +1,85 @@
/*
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 tlsutils
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
)
// CreateTLSConfig creates tls.Config instance from TLS parameters passed in environment variables with the given prefix
func CreateTLSConfig(prefix string) (*tls.Config, error) {
caFile := os.Getenv(fmt.Sprintf("%s_CA_FILE", prefix))
certFile := os.Getenv(fmt.Sprintf("%s_CERT_FILE", prefix))
keyFile := os.Getenv(fmt.Sprintf("%s_KEY_FILE", prefix))
serverName := os.Getenv(fmt.Sprintf("%s_TLS_SERVER_NAME", prefix))
isInsecureStr := strings.ToLower(os.Getenv(fmt.Sprintf("%s_TLS_INSECURE", prefix)))
isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
if err != nil {
return nil, err
}
return tlsConfig, nil
}
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
return nil, errors.New("either both cert and key or none must be provided")
}
var certificates []tls.Certificate
if certPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert: %s", err)
}
certificates = append(certificates, cert)
}
roots, err := loadRoots(caPath)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: certificates,
RootCAs: roots,
InsecureSkipVerify: insecure,
ServerName: serverName,
}, nil
}
// loads CA cert
func loadRoots(caPath string) (*x509.CertPool, error) {
if caPath == "" {
return nil, nil
}
roots := x509.NewCertPool()
pem, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, fmt.Errorf("error reading %s: %s", caPath, err)
}
ok := roots.AppendCertsFromPEM(pem)
if !ok {
return nil, fmt.Errorf("could not read root certs: %s", err)
}
return roots, nil
}

442
provider/designate.go Normal file
View File

@ -0,0 +1,442 @@
/*
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 provider
import (
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
"github.com/gophercloud/gophercloud/pagination"
log "github.com/sirupsen/logrus"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/pkg/tlsutils"
"github.com/kubernetes-incubator/external-dns/plan"
)
const (
// ID of the RecordSet from which endpoint was created
designateRecordSetID = "designate-recordset-id"
// Zone ID of the RecordSet
designateZoneID = "designate-record-id"
// Initial records values of the RecordSet. This label is required in order not to loose records that haven't
// changed where there are several targets per domain and only some of them changed.
// Values are joined by zero-byte to in order to get a single string
designateOriginalRecords = "designate-original-records"
)
// interface between provider and OpenStack DNS API
type designateClientInterface interface {
// ForEachZone calls handler for each zone managed by the Designate
ForEachZone(handler func(zone *zones.Zone) error) error
// ForEachRecordSet calls handler for each recordset in the given DNS zone
ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error
// CreateRecordSet creates recordset in the given DNS zone
CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error)
// UpdateRecordSet updates recordset in the given DNS zone
UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error
// DeleteRecordSet deletes recordset in the given DNS zone
DeleteRecordSet(zoneID, recordSetID string) error
}
// implementation of the designateClientInterface
type designateClient struct {
serviceClient *gophercloud.ServiceClient
}
// factory function for the designateClientInterface
func newDesignateClient() (designateClientInterface, error) {
serviceClient, err := createDesignateServiceClient()
if err != nil {
return nil, err
}
return &designateClient{serviceClient}, nil
}
// copies environment variables to new names without overwriting existing values
func remapEnv(mapping map[string]string) {
for k, v := range mapping {
currentVal := os.Getenv(k)
newVal := os.Getenv(v)
if currentVal == "" && newVal != "" {
os.Setenv(k, newVal)
}
}
}
// returns OpenStack Keystone authentication settings by obtaining values from standard environment variables.
// also fixes incompatibilities between gophercloud implementation and *-stackrc files that can be downloaded
// from OpenStack dashboard in latest versions
func getAuthSettings() (gophercloud.AuthOptions, error) {
remapEnv(map[string]string{
"OS_TENANT_NAME": "OS_PROJECT_NAME",
"OS_TENANT_ID": "OS_PROJECT_ID",
"OS_DOMAIN_NAME": "OS_USER_DOMAIN_NAME",
"OS_DOMAIN_ID": "OS_USER_DOMAIN_ID",
})
opts, err := openstack.AuthOptionsFromEnv()
if err != nil {
return gophercloud.AuthOptions{}, err
}
opts.AllowReauth = true
if !strings.HasSuffix(opts.IdentityEndpoint, "/") {
opts.IdentityEndpoint += "/"
}
if !strings.HasSuffix(opts.IdentityEndpoint, "/v2.0/") && !strings.HasSuffix(opts.IdentityEndpoint, "/v3/") {
opts.IdentityEndpoint += "v2.0/"
}
return opts, nil
}
// authenticate in OpenStack and obtain Designate service endpoint
func createDesignateServiceClient() (*gophercloud.ServiceClient, error) {
opts, err := getAuthSettings()
if err != nil {
return nil, err
}
log.Infof("Using OpenStack Keystone at %s", opts.IdentityEndpoint)
authProvider, err := openstack.AuthenticatedClient(opts)
if err != nil {
return nil, err
}
eo := gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
}
client, err := openstack.NewDNSV2(authProvider, eo)
if err != nil {
return nil, err
}
tlsConfig, err := tlsutils.CreateTLSConfig("OPENSTACK")
if err != nil {
return nil, err
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: tlsConfig,
}
client.ProviderClient.HTTPClient.Transport = transport
log.Infof("Found OpenStack Designate service at %s", client.Endpoint)
return client, nil
}
// ForEachZone calls handler for each zone managed by the Designate
func (c designateClient) ForEachZone(handler func(zone *zones.Zone) error) error {
pager := zones.List(c.serviceClient, zones.ListOpts{})
return pager.EachPage(
func(page pagination.Page) (bool, error) {
list, err := zones.ExtractZones(page)
if err != nil {
return false, err
}
for _, zone := range list {
err := handler(&zone)
if err != nil {
return false, err
}
}
return true, nil
},
)
}
// ForEachRecordSet calls handler for each recordset in the given DNS zone
func (c designateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error {
pager := recordsets.ListByZone(c.serviceClient, zoneID, recordsets.ListOpts{})
return pager.EachPage(
func(page pagination.Page) (bool, error) {
list, err := recordsets.ExtractRecordSets(page)
if err != nil {
return false, err
}
for _, recordSet := range list {
err := handler(&recordSet)
if err != nil {
return false, err
}
}
return true, nil
},
)
}
// CreateRecordSet creates recordset in the given DNS zone
func (c designateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) {
r, err := recordsets.Create(c.serviceClient, zoneID, opts).Extract()
if err != nil {
return "", err
}
return r.ID, nil
}
// UpdateRecordSet updates recordset in the given DNS zone
func (c designateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error {
_, err := recordsets.Update(c.serviceClient, zoneID, recordSetID, opts).Extract()
return err
}
// DeleteRecordSet deletes recordset in the given DNS zone
func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error {
return recordsets.Delete(c.serviceClient, zoneID, recordSetID).ExtractErr()
}
// designate provider type
type designateProvider struct {
client designateClientInterface
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
dryRun bool
}
// NewDesignateProvider is a factory function for OpenStack designate providers
func NewDesignateProvider(domainFilter DomainFilter, dryRun bool) (Provider, error) {
client, err := newDesignateClient()
if err != nil {
return nil, err
}
return &designateProvider{
client: client,
domainFilter: domainFilter,
dryRun: dryRun,
}, nil
}
// converts domain name to FQDN
func canonicalizeDomainName(domain string) string {
if !strings.HasSuffix(domain, ".") {
domain += "."
}
return strings.ToLower(domain)
}
// returns ZoneID -> ZoneName mapping for zones that are managed by the Designate and match domain filter
func (p designateProvider) getZones() (map[string]string, error) {
result := map[string]string{}
err := p.client.ForEachZone(
func(zone *zones.Zone) error {
if zone.Type != "" && strings.ToUpper(zone.Type) != "PRIMARY" || zone.Status != "ACTIVE" {
return nil
}
zoneName := canonicalizeDomainName(zone.Name)
if !p.domainFilter.Match(zoneName) {
return nil
}
result[zone.ID] = zoneName
return nil
},
)
return result, err
}
// finds best suitable DNS zone for the hostname
func (p designateProvider) getHostZoneID(hostname string, managedZones map[string]string) (string, error) {
longestZoneLength := 0
resultID := ""
for zoneID, zoneName := range managedZones {
if !strings.HasSuffix(hostname, zoneName) {
continue
}
ln := len(zoneName)
if ln > longestZoneLength {
resultID = zoneID
longestZoneLength = ln
}
}
return resultID, nil
}
// Records returns the list of records.
func (p designateProvider) Records() ([]*endpoint.Endpoint, error) {
var result []*endpoint.Endpoint
managedZones, err := p.getZones()
if err != nil {
return nil, err
}
for zoneID := range managedZones {
err = p.client.ForEachRecordSet(zoneID,
func(recordSet *recordsets.RecordSet) error {
if recordSet.Type != endpoint.RecordTypeA && recordSet.Type != endpoint.RecordTypeTXT && recordSet.Type != endpoint.RecordTypeCNAME {
return nil
}
for _, record := range recordSet.Records {
ep := endpoint.NewEndpoint(recordSet.Name, recordSet.Type, record)
ep.Labels[designateRecordSetID] = recordSet.ID
ep.Labels[designateZoneID] = recordSet.ZoneID
ep.Labels[designateOriginalRecords] = strings.Join(recordSet.Records, "\000")
result = append(result, ep)
}
return nil
},
)
if err != nil {
return nil, err
}
}
return result, nil
}
// temporary structure to hold recordset parameters so that we could aggregate endpoints into recordsets
type recordSet struct {
dnsName string
recordType string
zoneID string
recordSetID string
names map[string]bool
}
// adds endpoint into recordset aggregation, loading original values from endpoint labels first
func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete bool) {
key := fmt.Sprintf("%s/%s", ep.DNSName, ep.RecordType)
rs := recordSets[key]
if rs == nil {
rs = &recordSet{
dnsName: canonicalizeDomainName(ep.DNSName),
recordType: ep.RecordType,
names: make(map[string]bool),
}
}
if rs.zoneID == "" {
rs.zoneID = ep.Labels[designateZoneID]
}
if rs.recordSetID == "" {
rs.recordSetID = ep.Labels[designateRecordSetID]
}
for _, rec := range strings.Split(ep.Labels[designateOriginalRecords], "\000") {
if _, ok := rs.names[rec]; !ok && rec != "" {
rs.names[rec] = true
}
}
for _, target := range ep.Targets {
if ep.RecordType == endpoint.RecordTypeCNAME {
target = canonicalizeDomainName(target)
}
rs.names[target] = !delete
recordSets[key] = rs
}
}
// ApplyChanges applies a given set of changes in a given zone.
func (p designateProvider) ApplyChanges(changes *plan.Changes) error {
managedZones, err := p.getZones()
if err != nil {
return err
}
recordSets := map[string]*recordSet{}
for _, ep := range changes.Create {
addEndpoint(ep, recordSets, false)
}
for _, ep := range changes.UpdateNew {
addEndpoint(ep, recordSets, false)
}
for _, ep := range changes.UpdateOld {
addEndpoint(ep, recordSets, true)
}
for _, ep := range changes.Delete {
addEndpoint(ep, recordSets, true)
}
for _, rs := range recordSets {
if err2 := p.upsertRecordSet(rs, managedZones); err == nil {
err = err2
}
}
return err
}
// apply recordset changes by inserting/updating/deleting recordsets
func (p designateProvider) upsertRecordSet(rs *recordSet, managedZones map[string]string) error {
if rs.zoneID == "" {
var err error
rs.zoneID, err = p.getHostZoneID(rs.dnsName, managedZones)
if err != nil {
return err
}
if rs.zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", rs.dnsName)
return nil
}
}
var records []string
for rec, v := range rs.names {
if v {
records = append(records, rec)
}
}
if rs.recordSetID == "" && records == nil {
return nil
}
if rs.recordSetID == "" {
opts := recordsets.CreateOpts{
Name: rs.dnsName,
Type: rs.recordType,
Records: records,
}
log.Infof("Creating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ","))
if p.dryRun {
return nil
}
_, err := p.client.CreateRecordSet(rs.zoneID, opts)
return err
} else if len(records) == 0 {
log.Infof("Deleting records for %s/%s", rs.dnsName, rs.recordType)
if p.dryRun {
return nil
}
return p.client.DeleteRecordSet(rs.zoneID, rs.recordSetID)
} else {
opts := recordsets.UpdateOpts{
Records: records,
}
log.Infof("Updating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ","))
if p.dryRun {
return nil
}
return p.client.UpdateRecordSet(rs.zoneID, rs.recordSetID, opts)
}
}

519
provider/designate_test.go Normal file
View File

@ -0,0 +1,519 @@
/*
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 provider
import (
"fmt"
"reflect"
"sort"
"sync/atomic"
"testing"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
var lastGeneratedDesignateID int32
func generateDesignateID() string {
return fmt.Sprintf("id-%d", atomic.AddInt32(&lastGeneratedDesignateID, 1))
}
type fakeDesignateClient struct {
managedZones map[string]*struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}
}
func (c fakeDesignateClient) AddZone(zone zones.Zone) string {
if zone.ID == "" {
zone.ID = zone.Name
}
c.managedZones[zone.ID] = &struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}{
zone: &zone,
recordSets: make(map[string]*recordsets.RecordSet),
}
return zone.ID
}
func (c fakeDesignateClient) ForEachZone(handler func(zone *zones.Zone) error) error {
for _, zone := range c.managedZones {
if err := handler(zone.zone); err != nil {
return err
}
}
return nil
}
func (c fakeDesignateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
for _, recordSet := range zone.recordSets {
if err := handler(recordSet); err != nil {
return err
}
}
return nil
}
func (c fakeDesignateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) {
zone := c.managedZones[zoneID]
if zone == nil {
return "", fmt.Errorf("unknown zone %s", zoneID)
}
rs := &recordsets.RecordSet{
ID: generateDesignateID(),
ZoneID: zoneID,
Name: opts.Name,
Description: opts.Description,
Records: opts.Records,
TTL: opts.TTL,
Type: opts.Type,
}
zone.recordSets[rs.ID] = rs
return rs.ID, nil
}
func (c fakeDesignateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
rs := zone.recordSets[recordSetID]
if rs == nil {
return fmt.Errorf("unknown record-set %s", recordSetID)
}
rs.Description = opts.Description
rs.TTL = opts.TTL
rs.Records = opts.Records
return nil
}
func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
delete(zone.recordSets, recordSetID)
return nil
}
func (c fakeDesignateClient) ToProvider() Provider {
return &designateProvider{client: c}
}
func newFakeDesignateClient() *fakeDesignateClient {
return &fakeDesignateClient{
make(map[string]*struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}),
}
}
func TestDesignateRecords(t *testing.T) {
client := newFakeDesignateClient()
zone1ID := client.AddZone(zones.Zone{
Name: "example.com.",
Type: "PRIMARY",
Status: "ACTIVE",
})
rs11ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "www.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.1"},
})
rs12ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "www.example.com.",
Type: endpoint.RecordTypeTXT,
Records: []string{"text1"},
})
client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "xxx.example.com.",
Type: "SRV",
Records: []string{"http://test.com:1234"},
})
rs14ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "ftp.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.2"},
})
zone2ID := client.AddZone(zones.Zone{
Name: "test.net.",
Type: "PRIMARY",
Status: "ACTIVE",
})
rs21ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{
Name: "srv.test.net.",
Type: endpoint.RecordTypeA,
Records: []string{"10.2.1.1", "10.2.1.2"},
})
rs22ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{
Name: "db.test.net.",
Type: endpoint.RecordTypeCNAME,
Records: []string{"sql.test.net."},
})
expected := []*endpoint.Endpoint{
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{
designateRecordSetID: rs11ID,
designateZoneID: zone1ID,
designateOriginalRecords: "10.1.1.1",
},
},
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: []string{"text1"},
Labels: map[string]string{
designateRecordSetID: rs12ID,
designateZoneID: zone1ID,
designateOriginalRecords: "text1",
},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{
designateRecordSetID: rs14ID,
designateZoneID: zone1ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{
designateRecordSetID: rs21ID,
designateZoneID: zone2ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{
designateRecordSetID: rs21ID,
designateZoneID: zone2ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
{
DNSName: "db.test.net",
RecordType: endpoint.RecordTypeCNAME,
Targets: []string{"sql.test.net"},
Labels: map[string]string{
designateRecordSetID: rs22ID,
designateZoneID: zone2ID,
designateOriginalRecords: "sql.test.net.",
},
},
}
endpoints, err := client.ToProvider().Records()
if err != nil {
t.Fatal(err)
}
out:
for _, ep := range endpoints {
for i, ex := range expected {
if reflect.DeepEqual(ep, ex) {
expected = append(expected[:i], expected[i+1:]...)
continue out
}
}
t.Errorf("unexpected endpoint %s/%s -> %s", ep.DNSName, ep.RecordType, ep.Targets)
}
if len(expected) != 0 {
t.Errorf("not all expected endpoints were returned. Remained: %v", expected)
}
}
func TestDesignateCreateRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateCreateRecords(t, client)
}
func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet {
for i, zoneName := range []string{"example.com.", "test.net."} {
client.AddZone(zones.Zone{
ID: fmt.Sprintf("zone-%d", i+1),
Name: zoneName,
Type: "PRIMARY",
Status: "ACTIVE",
})
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{},
},
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: []string{"text1"},
Labels: map[string]string{},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{},
},
{
DNSName: "db.test.net",
RecordType: endpoint.RecordTypeCNAME,
Targets: []string{"sql.test.net"},
Labels: map[string]string{},
},
}
expected := []*recordsets.RecordSet{
{
Name: "www.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.1"},
ZoneID: "zone-1",
},
{
Name: "www.example.com.",
Type: endpoint.RecordTypeTXT,
Records: []string{"text1"},
ZoneID: "zone-1",
},
{
Name: "ftp.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.2"},
ZoneID: "zone-1",
},
{
Name: "srv.test.net.",
Type: endpoint.RecordTypeA,
Records: []string{"10.2.1.1", "10.2.1.2"},
ZoneID: "zone-2",
},
{
Name: "db.test.net.",
Type: endpoint.RecordTypeCNAME,
Records: []string{"sql.test.net."},
ZoneID: "zone-2",
},
}
expectedCopy := make([]*recordsets.RecordSet, len(expected))
copy(expectedCopy, expected)
err := client.ToProvider().ApplyChanges(&plan.Changes{Create: endpoints})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
id := recordSet.ID
recordSet.ID = ""
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
ex.ID = id
recordSet.ID = id
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were created. Remained: %v", expected)
}
return expectedCopy
}
func TestDesignateUpdateRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateUpdateRecords(t, client)
}
func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet {
expected := testDesignateCreateRecords(t, client)
updatesOld := []*endpoint.Endpoint{
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[2].ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
}
updatesNew := []*endpoint.Endpoint{
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.3.3.1"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[2].ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.3.3.2"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
}
expectedCopy := make([]*recordsets.RecordSet, len(expected))
copy(expectedCopy, expected)
expected[2].Records = []string{"10.3.3.1"}
expected[3].Records = []string{"10.2.1.1", "10.3.3.2"}
err := client.ToProvider().ApplyChanges(&plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were updated. Remained: %v", expected)
}
return expectedCopy
}
func TestDesignateDeleteRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateDeleteRecords(t, client)
}
func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) {
expected := testDesignateUpdateRecords(t, client)
deletes := []*endpoint.Endpoint{
{
DNSName: "www.example.com.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[0].ID,
designateOriginalRecords: "10.1.1.1",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.3.3.2",
},
},
}
expected[3].Records = []string{"10.3.3.2"}
expected = expected[1:]
err := client.ToProvider().ApplyChanges(&plan.Changes{Delete: deletes})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were deleted. Remained: %v", expected)
}
}

View File

@ -106,7 +106,15 @@ func (p *DigitalOceanProvider) Records() ([]*endpoint.Endpoint, error) {
for _, r := range records { for _, r := range records {
if supportedRecordType(r.Type) { if supportedRecordType(r.Type) {
endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, r.Type, r.Data)) name := r.Name + "." + zone.Name
// root name is identified by @ and should be
// translated to zone name for the endpoint entry.
if r.Name == "@" {
name = zone.Name
}
endpoints = append(endpoints, endpoint.NewEndpoint(name, r.Type, r.Data))
} }
} }
} }
@ -197,13 +205,21 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
if p.DryRun { if p.DryRun {
continue continue
} }
changeName := strings.TrimSuffix(change.ResourceRecordSet.Name, "."+zoneName)
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+zoneName)
// record at the root should be defined as @ instead of
// the full domain name
if change.ResourceRecordSet.Name == zoneName {
change.ResourceRecordSet.Name = "@"
}
switch change.Action { switch change.Action {
case DigitalOceanCreate: case DigitalOceanCreate:
_, _, err = p.Client.CreateRecord(context.TODO(), zoneName, _, _, err = p.Client.CreateRecord(context.TODO(), zoneName,
&godo.DomainRecordEditRequest{ &godo.DomainRecordEditRequest{
Data: change.ResourceRecordSet.Data, Data: change.ResourceRecordSet.Data,
Name: changeName, Name: change.ResourceRecordSet.Name,
Type: change.ResourceRecordSet.Type, Type: change.ResourceRecordSet.Type,
}) })
if err != nil { if err != nil {
@ -220,7 +236,7 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
_, _, err = p.Client.EditRecord(context.TODO(), zoneName, recordID, _, _, err = p.Client.EditRecord(context.TODO(), zoneName, recordID,
&godo.DomainRecordEditRequest{ &godo.DomainRecordEditRequest{
Data: change.ResourceRecordSet.Data, Data: change.ResourceRecordSet.Data,
Name: changeName, Name: change.ResourceRecordSet.Name,
Type: change.ResourceRecordSet.Type, Type: change.ResourceRecordSet.Type,
}) })
if err != nil { if err != nil {

View File

@ -83,14 +83,18 @@ func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt
switch domain { switch domain {
case "foo.com": case "foo.com":
if opt == nil || opt.Page == 0 { if opt == nil || opt.Page == 0 {
return []godo.DomainRecord{{ID: 1, Name: "foo.ext-dns-test", Type: "CNAME"}, {ID: 2, Name: "bar.ext-dns-test", Type: "CNAME"}}, &godo.Response{ return []godo.DomainRecord{
Links: &godo.Links{ {ID: 1, Name: "foo.ext-dns-test", Type: "CNAME"},
Pages: &godo.Pages{ {ID: 2, Name: "bar.ext-dns-test", Type: "CNAME"},
Next: "http://example.com/v2/domains/?page=2", {ID: 3, Name: "@", Type: endpoint.RecordTypeCNAME},
Last: "1234", }, &godo.Response{
Links: &godo.Links{
Pages: &godo.Pages{
Next: "http://example.com/v2/domains/?page=2",
Last: "1234",
},
}, },
}, }, nil
}, nil
} }
return []godo.DomainRecord{{ID: 3, Name: "baz.ext-dns-test", Type: "A"}}, nil, nil return []godo.DomainRecord{{ID: 3, Name: "baz.ext-dns-test", Type: "A"}}, nil, nil
case "example.com": case "example.com":
@ -425,7 +429,11 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
provider := &DigitalOceanProvider{ provider := &DigitalOceanProvider{
Client: &mockDigitalOceanClient{}, Client: &mockDigitalOceanClient{},
} }
changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}, {DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}}} changes.Create = []*endpoint.Endpoint{
{DNSName: "new.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}},
{DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}},
{DNSName: "bar.com", Targets: endpoint.Targets{"target"}},
}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}} changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Targets: endpoint.Targets{"target-old"}}} changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Targets: endpoint.Targets{"target-old"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}}} changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}}}
@ -506,7 +514,7 @@ func TestDigitalOceanAllRecords(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("should not fail, %s", err) t.Errorf("should not fail, %s", err)
} }
require.Equal(t, 4, len(records)) require.Equal(t, 5, len(records))
provider.Client = &mockDigitalOceanRecordsFail{} provider.Client = &mockDigitalOceanRecordsFail{}
_, err = provider.Records() _, err = provider.Records()

View File

@ -174,8 +174,8 @@ func (p *InfobloxProvider) ApplyChanges(changes *plan.Changes) error {
} }
created, deleted := p.mapChanges(zones, changes) created, deleted := p.mapChanges(zones, changes)
p.createRecords(created)
p.deleteRecords(deleted) p.deleteRecords(deleted)
p.createRecords(created)
return nil return nil
} }

View File

@ -26,7 +26,7 @@ func (z zoneIDName) Add(zoneID, zoneName string) {
func (z zoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) { func (z zoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) {
for zoneID, zoneName := range z { for zoneID, zoneName := range z {
if strings.HasSuffix(hostname, zoneName) { if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) {
if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) { if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) {
suitableZoneID = zoneID suitableZoneID = zoneID
suitableZoneName = zoneName suitableZoneName = zoneName

View File

@ -33,15 +33,28 @@ func TestZoneIDName(t *testing.T) {
"654321": "foo.qux.baz", "654321": "foo.qux.baz",
}, z) }, z)
// simple entry in a domain
zoneID, zoneName := z.FindZone("name.qux.baz") zoneID, zoneName := z.FindZone("name.qux.baz")
assert.Equal(t, "qux.baz", zoneName) assert.Equal(t, "qux.baz", zoneName)
assert.Equal(t, "123456", zoneID) assert.Equal(t, "123456", zoneID)
// simple entry in a domain's subdomain.
zoneID, zoneName = z.FindZone("name.foo.qux.baz") zoneID, zoneName = z.FindZone("name.foo.qux.baz")
assert.Equal(t, "foo.qux.baz", zoneName) assert.Equal(t, "foo.qux.baz", zoneName)
assert.Equal(t, "654321", zoneID) assert.Equal(t, "654321", zoneID)
// no possible zone for entry
zoneID, zoneName = z.FindZone("name.qux.foo") zoneID, zoneName = z.FindZone("name.qux.foo")
assert.Equal(t, "", zoneName) assert.Equal(t, "", zoneName)
assert.Equal(t, "", zoneID) assert.Equal(t, "", zoneID)
// entry's suffix matches a subdomain but doesn't belong there
zoneID, zoneName = z.FindZone("name-foo.qux.baz")
assert.Equal(t, "qux.baz", zoneName)
assert.Equal(t, "123456", zoneID)
// entry is an exact match of the domain (e.g. azure provider)
zoneID, zoneName = z.FindZone("foo.qux.baz")
assert.Equal(t, "foo.qux.baz", zoneName)
assert.Equal(t, "654321", zoneID)
} }

View File

@ -38,14 +38,15 @@ import (
// Use targetAnnotationKey to explicitly set Endpoint. (useful if the ingress // Use targetAnnotationKey to explicitly set Endpoint. (useful if the ingress
// controller does not update, or to override with alternative endpoint) // controller does not update, or to override with alternative endpoint)
type ingressSource struct { type ingressSource struct {
client kubernetes.Interface client kubernetes.Interface
namespace string namespace string
annotationFilter string annotationFilter string
fqdnTemplate *template.Template fqdnTemplate *template.Template
combineFQDNAnnotation bool
} }
// NewIngressSource creates a new ingressSource with the given config. // NewIngressSource creates a new ingressSource with the given config.
func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string) (Source, error) { func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool) (Source, error) {
var ( var (
tmpl *template.Template tmpl *template.Template
err error err error
@ -60,10 +61,11 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
} }
return &ingressSource{ return &ingressSource{
client: kubeClient, client: kubeClient,
namespace: namespace, namespace: namespace,
annotationFilter: annotationFilter, annotationFilter: annotationFilter,
fqdnTemplate: tmpl, fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFqdnAnnotation,
}, nil }, nil
} }
@ -93,11 +95,17 @@ func (sc *ingressSource) Endpoints() ([]*endpoint.Endpoint, error) {
ingEndpoints := endpointsFromIngress(&ing) ingEndpoints := endpointsFromIngress(&ing)
// apply template if host is missing on ingress // apply template if host is missing on ingress
if len(ingEndpoints) == 0 && sc.fqdnTemplate != nil { if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
ingEndpoints, err = sc.endpointsFromTemplate(&ing) iEndpoints, err := sc.endpointsFromTemplate(&ing)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sc.combineFQDNAnnotation {
ingEndpoints = append(ingEndpoints, iEndpoints...)
} else {
ingEndpoints = iEndpoints
}
} }
if len(ingEndpoints) == 0 { if len(ingEndpoints) == 0 {
@ -136,14 +144,14 @@ func getTargetsFromTargetAnnotation(ing *v1beta1.Ingress) endpoint.Targets {
} }
func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoint.Endpoint, error) { func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoint.Endpoint, error) {
// Process the whole template string
var buf bytes.Buffer var buf bytes.Buffer
err := sc.fqdnTemplate.Execute(&buf, ing) err := sc.fqdnTemplate.Execute(&buf, ing)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to apply template on ingress %s: %v", ing.String(), err) return nil, fmt.Errorf("failed to apply template on ingress %s: %v", ing.String(), err)
} }
hostname := buf.String() hostnames := buf.String()
ttl, err := getTTLFromAnnotations(ing.Annotations) ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil { if err != nil {
@ -156,7 +164,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
return endpointsForHostname(hostname, targets, ttl), nil 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)...)
}
return endpoints, nil
} }
// filterByAnnotations filters a list of ingresses by a given annotation selector. // filterByAnnotations filters a list of ingresses by a given annotation selector.

View File

@ -49,6 +49,7 @@ func (suite *IngressSuite) SetupTest() {
"", "",
"", "",
"{{.Name}}", "{{.Name}}",
false,
) )
suite.NoError(err, "should initialize ingress source") suite.NoError(err, "should initialize ingress source")
@ -78,10 +79,11 @@ func TestIngress(t *testing.T) {
func TestNewIngressSource(t *testing.T) { func TestNewIngressSource(t *testing.T) {
for _, ti := range []struct { for _, ti := range []struct {
title string title string
annotationFilter string annotationFilter string
fqdnTemplate string fqdnTemplate string
expectError bool combineFQDNAndAnnotation bool
expectError bool
}{ }{
{ {
title: "invalid template", title: "invalid template",
@ -97,6 +99,17 @@ func TestNewIngressSource(t *testing.T) {
expectError: false, expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
}, },
{
title: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
},
{
title: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
combineFQDNAndAnnotation: true,
},
{ {
title: "non-empty annotation filter label", title: "non-empty annotation filter label",
expectError: false, expectError: false,
@ -109,6 +122,7 @@ func TestNewIngressSource(t *testing.T) {
"", "",
ti.annotationFilter, ti.annotationFilter,
ti.fqdnTemplate, ti.fqdnTemplate,
ti.combineFQDNAndAnnotation,
) )
if ti.expectError { if ti.expectError {
assert.Error(t, err) assert.Error(t, err)
@ -204,13 +218,14 @@ func testEndpointsFromIngress(t *testing.T) {
func testIngressEndpoints(t *testing.T) { func testIngressEndpoints(t *testing.T) {
namespace := "testing" namespace := "testing"
for _, ti := range []struct { for _, ti := range []struct {
title string title string
targetNamespace string targetNamespace string
annotationFilter string annotationFilter string
ingressItems []fakeIngress ingressItems []fakeIngress
expected []*endpoint.Endpoint expected []*endpoint.Endpoint
expectError bool expectError bool
fqdnTemplate string fqdnTemplate string
combineFQDNAndAnnotation bool
}{ }{
{ {
title: "no ingress", title: "no ingress",
@ -473,6 +488,83 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{}, expected: []*endpoint.Endpoint{},
fqdnTemplate: "{{.Name}}.ext-dns.test.com", fqdnTemplate: "{{.Name}}.ext-dns.test.com",
}, },
{
title: "multiple FQDN template hostnames",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{},
dnsnames: []string{},
ips: []string{"8.8.8.8"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "fake1.ext-dns.test.com",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "fake1.ext-dna.test.com",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com",
},
{
title: "multiple FQDN template hostnames",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{},
dnsnames: []string{},
ips: []string{"8.8.8.8"},
},
{
name: "fake2",
namespace: namespace,
annotations: map[string]string{
targetAnnotationKey: "ingress-target.com",
},
dnsnames: []string{"example.org"},
ips: []string{},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "fake1.ext-dns.test.com",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "fake1.ext-dna.test.com",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "example.org",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "fake2.ext-dns.test.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "fake2.ext-dna.test.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
},
fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com",
combineFQDNAndAnnotation: true,
},
{ {
title: "ingress rules with annotation", title: "ingress rules with annotation",
targetNamespace: "", targetNamespace: "",
@ -627,6 +719,7 @@ func testIngressEndpoints(t *testing.T) {
ti.targetNamespace, ti.targetNamespace,
ti.annotationFilter, ti.annotationFilter,
ti.fqdnTemplate, ti.fqdnTemplate,
ti.combineFQDNAndAnnotation,
) )
for _, ingress := range ingresses { for _, ingress := range ingresses {
_, err := fakeClient.Extensions().Ingresses(ingress.Namespace).Create(ingress) _, err := fakeClient.Extensions().Ingresses(ingress.Namespace).Create(ingress)

View File

@ -47,13 +47,14 @@ type serviceSource struct {
namespace string namespace string
annotationFilter string annotationFilter string
// process Services with legacy annotations // process Services with legacy annotations
compatibility string compatibility string
fqdnTemplate *template.Template fqdnTemplate *template.Template
publishInternal bool combineFQDNAnnotation bool
publishInternal bool
} }
// NewServiceSource creates a new serviceSource with the given config. // NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate, compatibility string, publishInternal bool) (Source, error) { func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool) (Source, error) {
var ( var (
tmpl *template.Template tmpl *template.Template
err error err error
@ -68,12 +69,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
} }
return &serviceSource{ return &serviceSource{
client: kubeClient, client: kubeClient,
namespace: namespace, namespace: namespace,
annotationFilter: annotationFilter, annotationFilter: annotationFilter,
compatibility: compatibility, compatibility: compatibility,
fqdnTemplate: tmpl, fqdnTemplate: tmpl,
publishInternal: publishInternal, combineFQDNAnnotation: combineFqdnAnnotation,
publishInternal: publishInternal,
}, nil }, nil
} }
@ -107,11 +109,17 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
} }
// apply template if none of the above is found // apply template if none of the above is found
if len(svcEndpoints) == 0 && sc.fqdnTemplate != nil { if (sc.combineFQDNAnnotation || len(svcEndpoints) == 0) && sc.fqdnTemplate != nil {
svcEndpoints, err = sc.endpointsFromTemplate(&svc) sEndpoints, err := sc.endpointsFromTemplate(&svc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sc.combineFQDNAnnotation {
svcEndpoints = append(svcEndpoints, sEndpoints...)
} else {
svcEndpoints = sEndpoints
}
} }
if len(svcEndpoints) == 0 { if len(svcEndpoints) == 0 {
@ -148,10 +156,10 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
headlessDomain = v.Spec.Hostname + "." + headlessDomain headlessDomain = v.Spec.Hostname + "." + headlessDomain
} }
log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP)
// To reduce traffice on the DNS API only add record for running Pods. Good Idea? // To reduce traffice on the DNS API only add record for running Pods. Good Idea?
if v.Status.Phase == v1.PodRunning { if v.Status.Phase == v1.PodRunning {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.HostIP)) endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.PodIP))
} else { } else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
} }
@ -162,15 +170,17 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.Endpoint, error) { func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// Process the whole template string
var buf bytes.Buffer var buf bytes.Buffer
err := sc.fqdnTemplate.Execute(&buf, svc) err := sc.fqdnTemplate.Execute(&buf, svc)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err) return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err)
} }
hostname := buf.String() hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
for _, hostname := range hostnameList {
endpoints = sc.generateEndpoints(svc, hostname) endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
}
return endpoints, nil return endpoints, nil
} }

View File

@ -46,6 +46,7 @@ func (suite *ServiceSuite) SetupTest() {
"", "",
"", "",
"{{.Name}}", "{{.Name}}",
false,
"", "",
false, false,
) )
@ -128,6 +129,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
"", "",
ti.annotationFilter, ti.annotationFilter,
ti.fqdnTemplate, ti.fqdnTemplate,
false,
"", "",
false, false,
) )
@ -144,20 +146,21 @@ func testServiceSourceNewServiceSource(t *testing.T) {
// testServiceSourceEndpoints tests that various services generate the correct endpoints. // testServiceSourceEndpoints tests that various services generate the correct endpoints.
func testServiceSourceEndpoints(t *testing.T) { func testServiceSourceEndpoints(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
title string title string
targetNamespace string targetNamespace string
annotationFilter string annotationFilter string
svcNamespace string svcNamespace string
svcName string svcName string
svcType v1.ServiceType svcType v1.ServiceType
compatibility string compatibility string
fqdnTemplate string fqdnTemplate string
labels map[string]string combineFQDNAndAnnotation bool
annotations map[string]string labels map[string]string
clusterIP string annotations map[string]string
lbs []string clusterIP string
expected []*endpoint.Endpoint lbs []string
expectError bool expected []*endpoint.Endpoint
expectError bool
}{ }{
{ {
"no annotated services return no endpoints", "no annotated services return no endpoints",
@ -168,6 +171,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
"", "",
@ -184,6 +188,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -204,6 +209,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeClusterIP, v1.ServiceTypeClusterIP,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -213,6 +219,50 @@ func testServiceSourceEndpoints(t *testing.T) {
[]*endpoint.Endpoint{}, []*endpoint.Endpoint{},
false, false,
}, },
{
"FQDN template with multiple hostnames return an endpoint with target IP",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
false,
map[string]string{},
map[string]string{},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{
"FQDN template and annotation both with multiple hostnames return an endpoint with target IP",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
true,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{ {
"annotated services with multiple hostnames return an endpoint with target IP", "annotated services with multiple hostnames return an endpoint with target IP",
"", "",
@ -222,6 +272,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org., bar.example.org.", hostnameAnnotationKey: "foo.example.org., bar.example.org.",
@ -243,6 +294,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org, bar.example.org", hostnameAnnotationKey: "foo.example.org, bar.example.org",
@ -264,6 +316,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -284,6 +337,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
@ -305,6 +359,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
controllerAnnotationKey: controllerAnnotationValue, controllerAnnotationKey: controllerAnnotationValue,
@ -326,6 +381,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"{{.Name}}.ext-dns.test.com", "{{.Name}}.ext-dns.test.com",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
controllerAnnotationKey: "some-other-tool", controllerAnnotationKey: "some-other-tool",
@ -345,6 +401,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -365,6 +422,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -383,6 +441,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -403,6 +462,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -424,6 +484,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -443,6 +504,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -462,6 +524,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -483,6 +546,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -502,6 +566,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -520,6 +585,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -540,6 +606,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
"zalando.org/dnsname": "foo.example.org.", "zalando.org/dnsname": "foo.example.org.",
@ -558,6 +625,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"mate", "mate",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
"zalando.org/dnsname": "foo.example.org.", "zalando.org/dnsname": "foo.example.org.",
@ -578,6 +646,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"molecule", "molecule",
"", "",
false,
map[string]string{ map[string]string{
"dns": "route53", "dns": "route53",
}, },
@ -601,6 +670,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"{{.Name}}.bar.example.com", "{{.Name}}.bar.example.com",
false,
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
"", "",
@ -620,6 +690,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"{{.Name}}.bar.example.com", "{{.Name}}.bar.example.com",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -641,6 +712,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"mate", "mate",
"{{.Name}}.bar.example.com", "{{.Name}}.bar.example.com",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
"zalando.org/dnsname": "mate.example.org.", "zalando.org/dnsname": "mate.example.org.",
@ -661,6 +733,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"{{.Calibre}}.bar.example.com", "{{.Calibre}}.bar.example.com",
false,
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
"", "",
@ -677,6 +750,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -697,6 +771,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -718,6 +793,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -739,6 +815,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer, v1.ServiceTypeLoadBalancer,
"", "",
"", "",
false,
map[string]string{}, map[string]string{},
map[string]string{ map[string]string{
hostnameAnnotationKey: "foo.example.org.", hostnameAnnotationKey: "foo.example.org.",
@ -793,6 +870,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.targetNamespace, tc.targetNamespace,
tc.annotationFilter, tc.annotationFilter,
tc.fqdnTemplate, tc.fqdnTemplate,
tc.combineFQDNAndAnnotation,
tc.compatibility, tc.compatibility,
false, false,
) )
@ -925,6 +1003,7 @@ func TestClusterIpServices(t *testing.T) {
tc.targetNamespace, tc.targetNamespace,
tc.annotationFilter, tc.annotationFilter,
tc.fqdnTemplate, tc.fqdnTemplate,
false,
tc.compatibility, tc.compatibility,
true, true,
) )
@ -956,7 +1035,7 @@ func TestHeadlessServices(t *testing.T) {
labels map[string]string labels map[string]string
annotations map[string]string annotations map[string]string
clusterIP string clusterIP string
hostIP string podIP string
selector map[string]string selector map[string]string
lbs []string lbs []string
podnames []string podnames []string
@ -1080,8 +1159,8 @@ func TestHeadlessServices(t *testing.T) {
Annotations: tc.annotations, Annotations: tc.annotations,
}, },
Status: v1.PodStatus{ Status: v1.PodStatus{
HostIP: tc.hostIP, PodIP: tc.podIP,
Phase: tc.phases[i], Phase: tc.phases[i],
}, },
} }
@ -1095,6 +1174,7 @@ func TestHeadlessServices(t *testing.T) {
tc.targetNamespace, tc.targetNamespace,
"", "",
tc.fqdnTemplate, tc.fqdnTemplate,
false,
tc.compatibility, tc.compatibility,
true, true,
) )
@ -1137,7 +1217,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
require.NoError(b, err) require.NoError(b, err)
client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", "", false) client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false)
require.NoError(b, err) require.NoError(b, err)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View File

@ -35,11 +35,12 @@ var ErrSourceNotFound = errors.New("source not found")
// Config holds shared configuration options for all Sources. // Config holds shared configuration options for all Sources.
type Config struct { type Config struct {
Namespace string Namespace string
AnnotationFilter string AnnotationFilter string
FQDNTemplate string FQDNTemplate string
Compatibility string CombineFQDNAndAnnotation bool
PublishInternal bool Compatibility string
PublishInternal bool
} }
// ClientGenerator provides clients // ClientGenerator provides clients
@ -87,13 +88,13 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.Compatibility, cfg.PublishInternal) return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal)
case "ingress": case "ingress":
client, err := p.KubeClient() client, err := p.KubeClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate) return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation)
case "fake": case "fake":
return NewFakeSource(cfg.FQDNTemplate) return NewFakeSource(cfg.FQDNTemplate)
} }