mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-12-16 01:01:10 +01:00
Merge branch 'master' into issue-239-multiple-targets
This commit is contained in:
commit
0e8354795f
@ -6,7 +6,7 @@ os:
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- 1.x
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
@ -16,6 +16,7 @@ matrix:
|
||||
before_install:
|
||||
- make dep
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get github.com/lawrencewoodman/roveralls
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
|
||||
install:
|
||||
@ -24,4 +25,5 @@ install:
|
||||
script:
|
||||
- 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)
|
||||
- travis_wait 20 goveralls -service=travis-ci
|
||||
- travis_wait 20 roveralls
|
||||
- goveralls -coverprofile=roveralls.coverprofile -service=travis-ci
|
||||
|
||||
@ -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
|
||||
- Add custom TTL support for DNSimple (#477) @jbowes
|
||||
- Fix docker build and delete vendor files which were not deleted (#473) @njuettner
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
# builder image
|
||||
FROM golang:1.9 as builder
|
||||
FROM golang as builder
|
||||
|
||||
WORKDIR /go/src/github.com/kubernetes-incubator/external-dns
|
||||
COPY . .
|
||||
|
||||
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -226,6 +226,12 @@
|
||||
packages = ["."]
|
||||
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gophercloud/gophercloud"
|
||||
packages = ["."]
|
||||
revision = "bfc4756e1a693a850d7d459f4b28b21f35a24b5a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
@ -622,6 +628,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "7af57b8d195abce34060c82b92243ddba947f5d882be8f9a08b1aa827a9061fa"
|
||||
inputs-digest = "3cc043ee8be5b7cdba42793f12259ad23e13608d4b6a18523e540e14f029c0b0"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
13
README.md
13
README.md
@ -159,6 +159,15 @@ Here's a rough outline on what is to come (subject to change):
|
||||
- [x] Support for DigitalOcean
|
||||
- [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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
* Support for CoreDNS and Azure DNS
|
||||
* Support for CoreDNS
|
||||
* Support for record weights
|
||||
* Support for different behavioral policies
|
||||
* Support for Services with `type=NodePort`
|
||||
* Support for TPRs
|
||||
* Support for CRDs
|
||||
* 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.
|
||||
|
||||
14
docs/faq.md
14
docs/faq.md
@ -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?
|
||||
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
||||
### 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?
|
||||
|
||||
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?
|
||||
|
||||
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?
|
||||
|
||||
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:
|
||||
|
||||
@ -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
|
||||
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`
|
||||
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`.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
# Setting up ExternalDNS for Services 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
|
||||
```
|
||||
|
||||
### 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
|
||||
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",
|
||||
"subscriptionId": "670d2139-c4ef-4a98-8f38-b7052d5a06b2",
|
||||
"aadClientId": "a0b083bd-c0fc-473d-be48-e2a4df3ec908",
|
||||
"aadClientSecret": "11c78103-8109-40af-a6d4-3db265fed095",
|
||||
"tenantId": "AzureAD tenant Id",
|
||||
"subscriptionId": "Id",
|
||||
"aadClientId": "Service Principal AppId",
|
||||
"aadClientSecret": "Service Principal Password",
|
||||
"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:
|
||||
```
|
||||
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
|
||||
|
||||
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.
|
||||
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
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=azure
|
||||
- --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
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=azure
|
||||
- --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group
|
||||
@ -227,36 +254,43 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: example.com
|
||||
name: nginx-svc
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: tcp
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
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
|
||||
of the DNS zone (e.g. 'www.example.com').
|
||||
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.
|
||||
|
||||
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:
|
||||
Create the deployment, service and ingress object:
|
||||
|
||||
```
|
||||
$ 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
|
||||
`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.
|
||||
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.
|
||||
|
||||
## Verifying Azure DNS records
|
||||
|
||||
|
||||
155
docs/tutorials/designate.md
Normal file
155
docs/tutorials/designate.md
Normal 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
|
||||
```
|
||||
@ -76,7 +76,6 @@ spec:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
|
||||
@ -85,7 +84,7 @@ spec:
|
||||
- --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
|
||||
- --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
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
@ -135,6 +134,7 @@ spec:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
|
||||
@ -143,7 +143,7 @@ spec:
|
||||
- --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
|
||||
- --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
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
|
||||
@ -255,6 +255,7 @@ spec:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
|
||||
|
||||
13
main.go
13
main.go
@ -67,11 +67,12 @@ func main() {
|
||||
|
||||
// Create a source.Config from the flags passed by the user.
|
||||
sourceCfg := &source.Config{
|
||||
Namespace: cfg.Namespace,
|
||||
AnnotationFilter: cfg.AnnotationFilter,
|
||||
FQDNTemplate: cfg.FQDNTemplate,
|
||||
Compatibility: cfg.Compatibility,
|
||||
PublishInternal: cfg.PublishInternal,
|
||||
Namespace: cfg.Namespace,
|
||||
AnnotationFilter: cfg.AnnotationFilter,
|
||||
FQDNTemplate: cfg.FQDNTemplate,
|
||||
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
|
||||
Compatibility: cfg.Compatibility,
|
||||
PublishInternal: cfg.PublishInternal,
|
||||
}
|
||||
|
||||
// Lookup all the selected sources by names and pass them the desired configuration.
|
||||
@ -133,6 +134,8 @@ func main() {
|
||||
)
|
||||
case "inmemory":
|
||||
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
|
||||
case "designate":
|
||||
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
|
||||
@ -36,78 +36,80 @@ var (
|
||||
|
||||
// Config is a project-wide configuration
|
||||
type Config struct {
|
||||
Master string
|
||||
KubeConfig string
|
||||
Sources []string
|
||||
Namespace string
|
||||
AnnotationFilter string
|
||||
FQDNTemplate string
|
||||
Compatibility string
|
||||
PublishInternal bool
|
||||
Provider string
|
||||
GoogleProject string
|
||||
DomainFilter []string
|
||||
ZoneIDFilter []string
|
||||
AWSZoneType string
|
||||
AzureConfigFile string
|
||||
AzureResourceGroup string
|
||||
CloudflareProxied bool
|
||||
InfobloxGridHost string
|
||||
InfobloxWapiPort int
|
||||
InfobloxWapiUsername string
|
||||
InfobloxWapiPassword string
|
||||
InfobloxWapiVersion string
|
||||
InfobloxSSLVerify bool
|
||||
DynCustomerName string
|
||||
DynUsername string
|
||||
DynPassword string
|
||||
DynMinTTLSeconds int
|
||||
InMemoryZones []string
|
||||
Policy string
|
||||
Registry string
|
||||
TXTOwnerID string
|
||||
TXTPrefix string
|
||||
Interval time.Duration
|
||||
Once bool
|
||||
DryRun bool
|
||||
LogFormat string
|
||||
MetricsAddress string
|
||||
LogLevel string
|
||||
Master string
|
||||
KubeConfig string
|
||||
Sources []string
|
||||
Namespace string
|
||||
AnnotationFilter string
|
||||
FQDNTemplate string
|
||||
CombineFQDNAndAnnotation bool
|
||||
Compatibility string
|
||||
PublishInternal bool
|
||||
Provider string
|
||||
GoogleProject string
|
||||
DomainFilter []string
|
||||
ZoneIDFilter []string
|
||||
AWSZoneType string
|
||||
AzureConfigFile string
|
||||
AzureResourceGroup string
|
||||
CloudflareProxied bool
|
||||
InfobloxGridHost string
|
||||
InfobloxWapiPort int
|
||||
InfobloxWapiUsername string
|
||||
InfobloxWapiPassword string
|
||||
InfobloxWapiVersion string
|
||||
InfobloxSSLVerify bool
|
||||
DynCustomerName string
|
||||
DynUsername string
|
||||
DynPassword string
|
||||
DynMinTTLSeconds int
|
||||
InMemoryZones []string
|
||||
Policy string
|
||||
Registry string
|
||||
TXTOwnerID string
|
||||
TXTPrefix string
|
||||
Interval time.Duration
|
||||
Once bool
|
||||
DryRun bool
|
||||
LogFormat string
|
||||
MetricsAddress string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
Master: "",
|
||||
KubeConfig: "",
|
||||
Sources: nil,
|
||||
Namespace: "",
|
||||
AnnotationFilter: "",
|
||||
FQDNTemplate: "",
|
||||
Compatibility: "",
|
||||
PublishInternal: false,
|
||||
Provider: "",
|
||||
GoogleProject: "",
|
||||
DomainFilter: []string{},
|
||||
AWSZoneType: "",
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
CloudflareProxied: false,
|
||||
InfobloxGridHost: "",
|
||||
InfobloxWapiPort: 443,
|
||||
InfobloxWapiUsername: "admin",
|
||||
InfobloxWapiPassword: "",
|
||||
InfobloxWapiVersion: "2.3.1",
|
||||
InfobloxSSLVerify: true,
|
||||
InMemoryZones: []string{},
|
||||
Policy: "sync",
|
||||
Registry: "txt",
|
||||
TXTOwnerID: "default",
|
||||
TXTPrefix: "",
|
||||
Interval: time.Minute,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
LogFormat: "text",
|
||||
MetricsAddress: ":7979",
|
||||
LogLevel: logrus.InfoLevel.String(),
|
||||
Master: "",
|
||||
KubeConfig: "",
|
||||
Sources: nil,
|
||||
Namespace: "",
|
||||
AnnotationFilter: "",
|
||||
FQDNTemplate: "",
|
||||
CombineFQDNAndAnnotation: false,
|
||||
Compatibility: "",
|
||||
PublishInternal: false,
|
||||
Provider: "",
|
||||
GoogleProject: "",
|
||||
DomainFilter: []string{},
|
||||
AWSZoneType: "",
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
CloudflareProxied: false,
|
||||
InfobloxGridHost: "",
|
||||
InfobloxWapiPort: 443,
|
||||
InfobloxWapiUsername: "admin",
|
||||
InfobloxWapiPassword: "",
|
||||
InfobloxWapiVersion: "2.3.1",
|
||||
InfobloxSSLVerify: true,
|
||||
InMemoryZones: []string{},
|
||||
Policy: "sync",
|
||||
Registry: "txt",
|
||||
TXTOwnerID: "default",
|
||||
TXTPrefix: "",
|
||||
Interval: time.Minute,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
LogFormat: "text",
|
||||
MetricsAddress: ":7979",
|
||||
LogLevel: logrus.InfoLevel.String(),
|
||||
}
|
||||
|
||||
// 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("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
|
||||
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
|
||||
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional)").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("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
|
||||
|
||||
// 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("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("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)
|
||||
|
||||
85
pkg/tlsutils/tlsconfig.go
Normal file
85
pkg/tlsutils/tlsconfig.go
Normal 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
442
provider/designate.go
Normal 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
519
provider/designate_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -106,7 +106,15 @@ func (p *DigitalOceanProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
|
||||
for _, r := range records {
|
||||
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 {
|
||||
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 {
|
||||
case DigitalOceanCreate:
|
||||
_, _, err = p.Client.CreateRecord(context.TODO(), zoneName,
|
||||
&godo.DomainRecordEditRequest{
|
||||
Data: change.ResourceRecordSet.Data,
|
||||
Name: changeName,
|
||||
Name: change.ResourceRecordSet.Name,
|
||||
Type: change.ResourceRecordSet.Type,
|
||||
})
|
||||
if err != nil {
|
||||
@ -220,7 +236,7 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
|
||||
_, _, err = p.Client.EditRecord(context.TODO(), zoneName, recordID,
|
||||
&godo.DomainRecordEditRequest{
|
||||
Data: change.ResourceRecordSet.Data,
|
||||
Name: changeName,
|
||||
Name: change.ResourceRecordSet.Name,
|
||||
Type: change.ResourceRecordSet.Type,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@ -83,14 +83,18 @@ func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt
|
||||
switch domain {
|
||||
case "foo.com":
|
||||
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{
|
||||
Links: &godo.Links{
|
||||
Pages: &godo.Pages{
|
||||
Next: "http://example.com/v2/domains/?page=2",
|
||||
Last: "1234",
|
||||
return []godo.DomainRecord{
|
||||
{ID: 1, Name: "foo.ext-dns-test", Type: "CNAME"},
|
||||
{ID: 2, Name: "bar.ext-dns-test", Type: "CNAME"},
|
||||
{ID: 3, Name: "@", Type: endpoint.RecordTypeCNAME},
|
||||
}, &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
|
||||
case "example.com":
|
||||
@ -425,7 +429,11 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
|
||||
provider := &DigitalOceanProvider{
|
||||
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.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"}}}
|
||||
@ -506,7 +514,7 @@ func TestDigitalOceanAllRecords(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
require.Equal(t, 4, len(records))
|
||||
require.Equal(t, 5, len(records))
|
||||
|
||||
provider.Client = &mockDigitalOceanRecordsFail{}
|
||||
_, err = provider.Records()
|
||||
|
||||
@ -174,8 +174,8 @@ func (p *InfobloxProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
}
|
||||
|
||||
created, deleted := p.mapChanges(zones, changes)
|
||||
p.createRecords(created)
|
||||
p.deleteRecords(deleted)
|
||||
p.createRecords(created)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ func (z zoneIDName) Add(zoneID, zoneName string) {
|
||||
|
||||
func (z zoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) {
|
||||
for zoneID, zoneName := range z {
|
||||
if strings.HasSuffix(hostname, zoneName) {
|
||||
if hostname == zoneName || strings.HasSuffix(hostname, "."+zoneName) {
|
||||
if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) {
|
||||
suitableZoneID = zoneID
|
||||
suitableZoneName = zoneName
|
||||
|
||||
@ -33,15 +33,28 @@ func TestZoneIDName(t *testing.T) {
|
||||
"654321": "foo.qux.baz",
|
||||
}, z)
|
||||
|
||||
// simple entry in a domain
|
||||
zoneID, zoneName := z.FindZone("name.qux.baz")
|
||||
assert.Equal(t, "qux.baz", zoneName)
|
||||
assert.Equal(t, "123456", zoneID)
|
||||
|
||||
// simple entry in a domain's subdomain.
|
||||
zoneID, zoneName = z.FindZone("name.foo.qux.baz")
|
||||
assert.Equal(t, "foo.qux.baz", zoneName)
|
||||
assert.Equal(t, "654321", zoneID)
|
||||
|
||||
// no possible zone for entry
|
||||
zoneID, zoneName = z.FindZone("name.qux.foo")
|
||||
assert.Equal(t, "", zoneName)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -38,14 +38,15 @@ import (
|
||||
// Use targetAnnotationKey to explicitly set Endpoint. (useful if the ingress
|
||||
// controller does not update, or to override with alternative endpoint)
|
||||
type ingressSource struct {
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
annotationFilter string
|
||||
fqdnTemplate *template.Template
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
annotationFilter string
|
||||
fqdnTemplate *template.Template
|
||||
combineFQDNAnnotation bool
|
||||
}
|
||||
|
||||
// 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 (
|
||||
tmpl *template.Template
|
||||
err error
|
||||
@ -60,10 +61,11 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
|
||||
}
|
||||
|
||||
return &ingressSource{
|
||||
client: kubeClient,
|
||||
namespace: namespace,
|
||||
annotationFilter: annotationFilter,
|
||||
fqdnTemplate: tmpl,
|
||||
client: kubeClient,
|
||||
namespace: namespace,
|
||||
annotationFilter: annotationFilter,
|
||||
fqdnTemplate: tmpl,
|
||||
combineFQDNAnnotation: combineFqdnAnnotation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -93,11 +95,17 @@ func (sc *ingressSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
ingEndpoints := endpointsFromIngress(&ing)
|
||||
|
||||
// apply template if host is missing on ingress
|
||||
if len(ingEndpoints) == 0 && sc.fqdnTemplate != nil {
|
||||
ingEndpoints, err = sc.endpointsFromTemplate(&ing)
|
||||
if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
|
||||
iEndpoints, err := sc.endpointsFromTemplate(&ing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sc.combineFQDNAnnotation {
|
||||
ingEndpoints = append(ingEndpoints, iEndpoints...)
|
||||
} else {
|
||||
ingEndpoints = iEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
// Process the whole template string
|
||||
var buf bytes.Buffer
|
||||
err := sc.fqdnTemplate.Execute(&buf, ing)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -156,7 +164,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
|
||||
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.
|
||||
|
||||
@ -49,6 +49,7 @@ func (suite *IngressSuite) SetupTest() {
|
||||
"",
|
||||
"",
|
||||
"{{.Name}}",
|
||||
false,
|
||||
)
|
||||
suite.NoError(err, "should initialize ingress source")
|
||||
|
||||
@ -78,10 +79,11 @@ func TestIngress(t *testing.T) {
|
||||
|
||||
func TestNewIngressSource(t *testing.T) {
|
||||
for _, ti := range []struct {
|
||||
title string
|
||||
annotationFilter string
|
||||
fqdnTemplate string
|
||||
expectError bool
|
||||
title string
|
||||
annotationFilter string
|
||||
fqdnTemplate string
|
||||
combineFQDNAndAnnotation bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
title: "invalid template",
|
||||
@ -97,6 +99,17 @@ func TestNewIngressSource(t *testing.T) {
|
||||
expectError: false,
|
||||
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",
|
||||
expectError: false,
|
||||
@ -109,6 +122,7 @@ func TestNewIngressSource(t *testing.T) {
|
||||
"",
|
||||
ti.annotationFilter,
|
||||
ti.fqdnTemplate,
|
||||
ti.combineFQDNAndAnnotation,
|
||||
)
|
||||
if ti.expectError {
|
||||
assert.Error(t, err)
|
||||
@ -204,13 +218,14 @@ func testEndpointsFromIngress(t *testing.T) {
|
||||
func testIngressEndpoints(t *testing.T) {
|
||||
namespace := "testing"
|
||||
for _, ti := range []struct {
|
||||
title string
|
||||
targetNamespace string
|
||||
annotationFilter string
|
||||
ingressItems []fakeIngress
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
fqdnTemplate string
|
||||
title string
|
||||
targetNamespace string
|
||||
annotationFilter string
|
||||
ingressItems []fakeIngress
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
fqdnTemplate string
|
||||
combineFQDNAndAnnotation bool
|
||||
}{
|
||||
{
|
||||
title: "no ingress",
|
||||
@ -473,6 +488,83 @@ func testIngressEndpoints(t *testing.T) {
|
||||
expected: []*endpoint.Endpoint{},
|
||||
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",
|
||||
targetNamespace: "",
|
||||
@ -627,6 +719,7 @@ func testIngressEndpoints(t *testing.T) {
|
||||
ti.targetNamespace,
|
||||
ti.annotationFilter,
|
||||
ti.fqdnTemplate,
|
||||
ti.combineFQDNAndAnnotation,
|
||||
)
|
||||
for _, ingress := range ingresses {
|
||||
_, err := fakeClient.Extensions().Ingresses(ingress.Namespace).Create(ingress)
|
||||
|
||||
@ -47,13 +47,14 @@ type serviceSource struct {
|
||||
namespace string
|
||||
annotationFilter string
|
||||
// process Services with legacy annotations
|
||||
compatibility string
|
||||
fqdnTemplate *template.Template
|
||||
publishInternal bool
|
||||
compatibility string
|
||||
fqdnTemplate *template.Template
|
||||
combineFQDNAnnotation bool
|
||||
publishInternal bool
|
||||
}
|
||||
|
||||
// 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 (
|
||||
tmpl *template.Template
|
||||
err error
|
||||
@ -68,12 +69,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
|
||||
}
|
||||
|
||||
return &serviceSource{
|
||||
client: kubeClient,
|
||||
namespace: namespace,
|
||||
annotationFilter: annotationFilter,
|
||||
compatibility: compatibility,
|
||||
fqdnTemplate: tmpl,
|
||||
publishInternal: publishInternal,
|
||||
client: kubeClient,
|
||||
namespace: namespace,
|
||||
annotationFilter: annotationFilter,
|
||||
compatibility: compatibility,
|
||||
fqdnTemplate: tmpl,
|
||||
combineFQDNAnnotation: combineFqdnAnnotation,
|
||||
publishInternal: publishInternal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -107,11 +109,17 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
}
|
||||
|
||||
// apply template if none of the above is found
|
||||
if len(svcEndpoints) == 0 && sc.fqdnTemplate != nil {
|
||||
svcEndpoints, err = sc.endpointsFromTemplate(&svc)
|
||||
if (sc.combineFQDNAnnotation || len(svcEndpoints) == 0) && sc.fqdnTemplate != nil {
|
||||
sEndpoints, err := sc.endpointsFromTemplate(&svc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sc.combineFQDNAnnotation {
|
||||
svcEndpoints = append(svcEndpoints, sEndpoints...)
|
||||
} else {
|
||||
svcEndpoints = sEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
if len(svcEndpoints) == 0 {
|
||||
@ -148,10 +156,10 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
|
||||
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?
|
||||
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 {
|
||||
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) {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
// Process the whole template string
|
||||
var buf bytes.Buffer
|
||||
err := sc.fqdnTemplate.Execute(&buf, svc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err)
|
||||
}
|
||||
|
||||
hostname := buf.String()
|
||||
|
||||
endpoints = sc.generateEndpoints(svc, hostname)
|
||||
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
|
||||
for _, hostname := range hostnameList {
|
||||
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ func (suite *ServiceSuite) SetupTest() {
|
||||
"",
|
||||
"",
|
||||
"{{.Name}}",
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
)
|
||||
@ -128,6 +129,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
|
||||
"",
|
||||
ti.annotationFilter,
|
||||
ti.fqdnTemplate,
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
)
|
||||
@ -144,20 +146,21 @@ func testServiceSourceNewServiceSource(t *testing.T) {
|
||||
// testServiceSourceEndpoints tests that various services generate the correct endpoints.
|
||||
func testServiceSourceEndpoints(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
title string
|
||||
targetNamespace string
|
||||
annotationFilter string
|
||||
svcNamespace string
|
||||
svcName string
|
||||
svcType v1.ServiceType
|
||||
compatibility string
|
||||
fqdnTemplate string
|
||||
labels map[string]string
|
||||
annotations map[string]string
|
||||
clusterIP string
|
||||
lbs []string
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
title string
|
||||
targetNamespace string
|
||||
annotationFilter string
|
||||
svcNamespace string
|
||||
svcName string
|
||||
svcType v1.ServiceType
|
||||
compatibility string
|
||||
fqdnTemplate string
|
||||
combineFQDNAndAnnotation bool
|
||||
labels map[string]string
|
||||
annotations map[string]string
|
||||
clusterIP string
|
||||
lbs []string
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"no annotated services return no endpoints",
|
||||
@ -168,6 +171,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
"",
|
||||
@ -184,6 +188,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -204,6 +209,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeClusterIP,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -213,6 +219,50 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
[]*endpoint.Endpoint{},
|
||||
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",
|
||||
"",
|
||||
@ -222,6 +272,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
|
||||
@ -243,6 +294,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org, bar.example.org",
|
||||
@ -264,6 +316,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -284,6 +337,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
|
||||
@ -305,6 +359,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
controllerAnnotationKey: controllerAnnotationValue,
|
||||
@ -326,6 +381,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"{{.Name}}.ext-dns.test.com",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
controllerAnnotationKey: "some-other-tool",
|
||||
@ -345,6 +401,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -365,6 +422,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -383,6 +441,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -403,6 +462,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -424,6 +484,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -443,6 +504,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -462,6 +524,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -483,6 +546,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -502,6 +566,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -520,6 +585,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -540,6 +606,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
"zalando.org/dnsname": "foo.example.org.",
|
||||
@ -558,6 +625,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"mate",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
"zalando.org/dnsname": "foo.example.org.",
|
||||
@ -578,6 +646,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"molecule",
|
||||
"",
|
||||
false,
|
||||
map[string]string{
|
||||
"dns": "route53",
|
||||
},
|
||||
@ -601,6 +670,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"{{.Name}}.bar.example.com",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
"",
|
||||
@ -620,6 +690,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"{{.Name}}.bar.example.com",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -641,6 +712,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"mate",
|
||||
"{{.Name}}.bar.example.com",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
"zalando.org/dnsname": "mate.example.org.",
|
||||
@ -661,6 +733,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"{{.Calibre}}.bar.example.com",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
"",
|
||||
@ -677,6 +750,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -697,6 +771,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -718,6 +793,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -739,6 +815,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
@ -793,6 +870,7 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
tc.targetNamespace,
|
||||
tc.annotationFilter,
|
||||
tc.fqdnTemplate,
|
||||
tc.combineFQDNAndAnnotation,
|
||||
tc.compatibility,
|
||||
false,
|
||||
)
|
||||
@ -925,6 +1003,7 @@ func TestClusterIpServices(t *testing.T) {
|
||||
tc.targetNamespace,
|
||||
tc.annotationFilter,
|
||||
tc.fqdnTemplate,
|
||||
false,
|
||||
tc.compatibility,
|
||||
true,
|
||||
)
|
||||
@ -956,7 +1035,7 @@ func TestHeadlessServices(t *testing.T) {
|
||||
labels map[string]string
|
||||
annotations map[string]string
|
||||
clusterIP string
|
||||
hostIP string
|
||||
podIP string
|
||||
selector map[string]string
|
||||
lbs []string
|
||||
podnames []string
|
||||
@ -1080,8 +1159,8 @@ func TestHeadlessServices(t *testing.T) {
|
||||
Annotations: tc.annotations,
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
HostIP: tc.hostIP,
|
||||
Phase: tc.phases[i],
|
||||
PodIP: tc.podIP,
|
||||
Phase: tc.phases[i],
|
||||
},
|
||||
}
|
||||
|
||||
@ -1095,6 +1174,7 @@ func TestHeadlessServices(t *testing.T) {
|
||||
tc.targetNamespace,
|
||||
"",
|
||||
tc.fqdnTemplate,
|
||||
false,
|
||||
tc.compatibility,
|
||||
true,
|
||||
)
|
||||
@ -1137,7 +1217,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
|
||||
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
|
||||
require.NoError(b, err)
|
||||
|
||||
client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", "", false)
|
||||
client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false)
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@ -35,11 +35,12 @@ var ErrSourceNotFound = errors.New("source not found")
|
||||
|
||||
// Config holds shared configuration options for all Sources.
|
||||
type Config struct {
|
||||
Namespace string
|
||||
AnnotationFilter string
|
||||
FQDNTemplate string
|
||||
Compatibility string
|
||||
PublishInternal bool
|
||||
Namespace string
|
||||
AnnotationFilter string
|
||||
FQDNTemplate string
|
||||
CombineFQDNAndAnnotation bool
|
||||
Compatibility string
|
||||
PublishInternal bool
|
||||
}
|
||||
|
||||
// ClientGenerator provides clients
|
||||
@ -87,13 +88,13 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
||||
if err != nil {
|
||||
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":
|
||||
client, err := p.KubeClient()
|
||||
if err != nil {
|
||||
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":
|
||||
return NewFakeSource(cfg.FQDNTemplate)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user