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

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

View File

@ -6,7 +6,7 @@ os:
language: go
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

View File

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

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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
View File

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

View File

@ -76,7 +76,6 @@ spec:
labels:
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

View File

@ -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

View File

@ -70,6 +70,7 @@ func main() {
Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter,
FQDNTemplate: cfg.FQDNTemplate,
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
}
@ -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)
}

View File

@ -42,6 +42,7 @@ type Config struct {
Namespace string
AnnotationFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
Compatibility string
PublishInternal bool
Provider string
@ -82,6 +83,7 @@ var defaultConfig = &Config{
Namespace: "",
AnnotationFilter: "",
FQDNTemplate: "",
CombineFQDNAndAnnotation: false,
Compatibility: "",
PublishInternal: false,
Provider: "",
@ -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
View File

@ -0,0 +1,85 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tlsutils
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
)
// CreateTLSConfig creates tls.Config instance from TLS parameters passed in environment variables with the given prefix
func CreateTLSConfig(prefix string) (*tls.Config, error) {
caFile := os.Getenv(fmt.Sprintf("%s_CA_FILE", prefix))
certFile := os.Getenv(fmt.Sprintf("%s_CERT_FILE", prefix))
keyFile := os.Getenv(fmt.Sprintf("%s_KEY_FILE", prefix))
serverName := os.Getenv(fmt.Sprintf("%s_TLS_SERVER_NAME", prefix))
isInsecureStr := strings.ToLower(os.Getenv(fmt.Sprintf("%s_TLS_INSECURE", prefix)))
isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
if err != nil {
return nil, err
}
return tlsConfig, nil
}
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
return nil, errors.New("either both cert and key or none must be provided")
}
var certificates []tls.Certificate
if certPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert: %s", err)
}
certificates = append(certificates, cert)
}
roots, err := loadRoots(caPath)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: certificates,
RootCAs: roots,
InsecureSkipVerify: insecure,
ServerName: serverName,
}, nil
}
// loads CA cert
func loadRoots(caPath string) (*x509.CertPool, error) {
if caPath == "" {
return nil, nil
}
roots := x509.NewCertPool()
pem, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, fmt.Errorf("error reading %s: %s", caPath, err)
}
ok := roots.AppendCertsFromPEM(pem)
if !ok {
return nil, fmt.Errorf("could not read root certs: %s", err)
}
return roots, nil
}

442
provider/designate.go Normal file
View File

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

519
provider/designate_test.go Normal file
View File

@ -0,0 +1,519 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"fmt"
"reflect"
"sort"
"sync/atomic"
"testing"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
var lastGeneratedDesignateID int32
func generateDesignateID() string {
return fmt.Sprintf("id-%d", atomic.AddInt32(&lastGeneratedDesignateID, 1))
}
type fakeDesignateClient struct {
managedZones map[string]*struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}
}
func (c fakeDesignateClient) AddZone(zone zones.Zone) string {
if zone.ID == "" {
zone.ID = zone.Name
}
c.managedZones[zone.ID] = &struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}{
zone: &zone,
recordSets: make(map[string]*recordsets.RecordSet),
}
return zone.ID
}
func (c fakeDesignateClient) ForEachZone(handler func(zone *zones.Zone) error) error {
for _, zone := range c.managedZones {
if err := handler(zone.zone); err != nil {
return err
}
}
return nil
}
func (c fakeDesignateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
for _, recordSet := range zone.recordSets {
if err := handler(recordSet); err != nil {
return err
}
}
return nil
}
func (c fakeDesignateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) {
zone := c.managedZones[zoneID]
if zone == nil {
return "", fmt.Errorf("unknown zone %s", zoneID)
}
rs := &recordsets.RecordSet{
ID: generateDesignateID(),
ZoneID: zoneID,
Name: opts.Name,
Description: opts.Description,
Records: opts.Records,
TTL: opts.TTL,
Type: opts.Type,
}
zone.recordSets[rs.ID] = rs
return rs.ID, nil
}
func (c fakeDesignateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
rs := zone.recordSets[recordSetID]
if rs == nil {
return fmt.Errorf("unknown record-set %s", recordSetID)
}
rs.Description = opts.Description
rs.TTL = opts.TTL
rs.Records = opts.Records
return nil
}
func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error {
zone := c.managedZones[zoneID]
if zone == nil {
return fmt.Errorf("unknown zone %s", zoneID)
}
delete(zone.recordSets, recordSetID)
return nil
}
func (c fakeDesignateClient) ToProvider() Provider {
return &designateProvider{client: c}
}
func newFakeDesignateClient() *fakeDesignateClient {
return &fakeDesignateClient{
make(map[string]*struct {
zone *zones.Zone
recordSets map[string]*recordsets.RecordSet
}),
}
}
func TestDesignateRecords(t *testing.T) {
client := newFakeDesignateClient()
zone1ID := client.AddZone(zones.Zone{
Name: "example.com.",
Type: "PRIMARY",
Status: "ACTIVE",
})
rs11ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "www.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.1"},
})
rs12ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "www.example.com.",
Type: endpoint.RecordTypeTXT,
Records: []string{"text1"},
})
client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "xxx.example.com.",
Type: "SRV",
Records: []string{"http://test.com:1234"},
})
rs14ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{
Name: "ftp.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.2"},
})
zone2ID := client.AddZone(zones.Zone{
Name: "test.net.",
Type: "PRIMARY",
Status: "ACTIVE",
})
rs21ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{
Name: "srv.test.net.",
Type: endpoint.RecordTypeA,
Records: []string{"10.2.1.1", "10.2.1.2"},
})
rs22ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{
Name: "db.test.net.",
Type: endpoint.RecordTypeCNAME,
Records: []string{"sql.test.net."},
})
expected := []*endpoint.Endpoint{
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{
designateRecordSetID: rs11ID,
designateZoneID: zone1ID,
designateOriginalRecords: "10.1.1.1",
},
},
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: []string{"text1"},
Labels: map[string]string{
designateRecordSetID: rs12ID,
designateZoneID: zone1ID,
designateOriginalRecords: "text1",
},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{
designateRecordSetID: rs14ID,
designateZoneID: zone1ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{
designateRecordSetID: rs21ID,
designateZoneID: zone2ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{
designateRecordSetID: rs21ID,
designateZoneID: zone2ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
{
DNSName: "db.test.net",
RecordType: endpoint.RecordTypeCNAME,
Targets: []string{"sql.test.net"},
Labels: map[string]string{
designateRecordSetID: rs22ID,
designateZoneID: zone2ID,
designateOriginalRecords: "sql.test.net.",
},
},
}
endpoints, err := client.ToProvider().Records()
if err != nil {
t.Fatal(err)
}
out:
for _, ep := range endpoints {
for i, ex := range expected {
if reflect.DeepEqual(ep, ex) {
expected = append(expected[:i], expected[i+1:]...)
continue out
}
}
t.Errorf("unexpected endpoint %s/%s -> %s", ep.DNSName, ep.RecordType, ep.Targets)
}
if len(expected) != 0 {
t.Errorf("not all expected endpoints were returned. Remained: %v", expected)
}
}
func TestDesignateCreateRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateCreateRecords(t, client)
}
func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet {
for i, zoneName := range []string{"example.com.", "test.net."} {
client.AddZone(zones.Zone{
ID: fmt.Sprintf("zone-%d", i+1),
Name: zoneName,
Type: "PRIMARY",
Status: "ACTIVE",
})
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{},
},
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: []string{"text1"},
Labels: map[string]string{},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{},
},
{
DNSName: "db.test.net",
RecordType: endpoint.RecordTypeCNAME,
Targets: []string{"sql.test.net"},
Labels: map[string]string{},
},
}
expected := []*recordsets.RecordSet{
{
Name: "www.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.1"},
ZoneID: "zone-1",
},
{
Name: "www.example.com.",
Type: endpoint.RecordTypeTXT,
Records: []string{"text1"},
ZoneID: "zone-1",
},
{
Name: "ftp.example.com.",
Type: endpoint.RecordTypeA,
Records: []string{"10.1.1.2"},
ZoneID: "zone-1",
},
{
Name: "srv.test.net.",
Type: endpoint.RecordTypeA,
Records: []string{"10.2.1.1", "10.2.1.2"},
ZoneID: "zone-2",
},
{
Name: "db.test.net.",
Type: endpoint.RecordTypeCNAME,
Records: []string{"sql.test.net."},
ZoneID: "zone-2",
},
}
expectedCopy := make([]*recordsets.RecordSet, len(expected))
copy(expectedCopy, expected)
err := client.ToProvider().ApplyChanges(&plan.Changes{Create: endpoints})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
id := recordSet.ID
recordSet.ID = ""
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
ex.ID = id
recordSet.ID = id
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were created. Remained: %v", expected)
}
return expectedCopy
}
func TestDesignateUpdateRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateUpdateRecords(t, client)
}
func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet {
expected := testDesignateCreateRecords(t, client)
updatesOld := []*endpoint.Endpoint{
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.2"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[2].ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.2"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
}
updatesNew := []*endpoint.Endpoint{
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.3.3.1"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[2].ID,
designateOriginalRecords: "10.1.1.2",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.3.3.2"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
},
},
}
expectedCopy := make([]*recordsets.RecordSet, len(expected))
copy(expectedCopy, expected)
expected[2].Records = []string{"10.3.3.1"}
expected[3].Records = []string{"10.2.1.1", "10.3.3.2"}
err := client.ToProvider().ApplyChanges(&plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were updated. Remained: %v", expected)
}
return expectedCopy
}
func TestDesignateDeleteRecords(t *testing.T) {
client := newFakeDesignateClient()
testDesignateDeleteRecords(t, client)
}
func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) {
expected := testDesignateUpdateRecords(t, client)
deletes := []*endpoint.Endpoint{
{
DNSName: "www.example.com.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.1.1.1"},
Labels: map[string]string{
designateZoneID: "zone-1",
designateRecordSetID: expected[0].ID,
designateOriginalRecords: "10.1.1.1",
},
},
{
DNSName: "srv.test.net.",
RecordType: endpoint.RecordTypeA,
Targets: []string{"10.2.1.1"},
Labels: map[string]string{
designateZoneID: "zone-2",
designateRecordSetID: expected[3].ID,
designateOriginalRecords: "10.2.1.1\00010.3.3.2",
},
},
}
expected[3].Records = []string{"10.3.3.2"}
expected = expected[1:]
err := client.ToProvider().ApplyChanges(&plan.Changes{Delete: deletes})
if err != nil {
t.Fatal(err)
}
client.ForEachZone(func(zone *zones.Zone) error {
client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error {
for i, ex := range expected {
sort.Strings(recordSet.Records)
if reflect.DeepEqual(ex, recordSet) {
expected = append(expected[:i], expected[i+1:]...)
return nil
}
}
t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records)
return nil
})
return nil
})
if len(expected) != 0 {
t.Errorf("not all expected record-sets were deleted. Remained: %v", expected)
}
}

View File

@ -106,7 +106,15 @@ func (p *DigitalOceanProvider) Records() ([]*endpoint.Endpoint, error) {
for _, r := range records {
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 {

View File

@ -83,7 +83,11 @@ 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{
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",
@ -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()

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -42,10 +42,11 @@ type ingressSource struct {
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
@ -64,6 +65,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
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.

View File

@ -49,6 +49,7 @@ func (suite *IngressSuite) SetupTest() {
"",
"",
"{{.Name}}",
false,
)
suite.NoError(err, "should initialize ingress source")
@ -81,6 +82,7 @@ func TestNewIngressSource(t *testing.T) {
title string
annotationFilter string
fqdnTemplate string
combineFQDNAndAnnotation bool
expectError bool
}{
{
@ -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)
@ -211,6 +225,7 @@ func testIngressEndpoints(t *testing.T) {
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)

View File

@ -49,11 +49,12 @@ type serviceSource struct {
// process Services with legacy annotations
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
@ -73,6 +74,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
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
}

View File

@ -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,
)
@ -152,6 +154,7 @@ func testServiceSourceEndpoints(t *testing.T) {
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
@ -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,7 +1159,7 @@ func TestHeadlessServices(t *testing.T) {
Annotations: tc.annotations,
},
Status: v1.PodStatus{
HostIP: tc.hostIP,
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++ {

View File

@ -38,6 +38,7 @@ type Config struct {
Namespace string
AnnotationFilter string
FQDNTemplate string
CombineFQDNAndAnnotation bool
Compatibility string
PublishInternal bool
}
@ -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)
}