Merge remote-tracking branch 'upstream/master' into coredns

This commit is contained in:
Martin Linkhorst 2018-05-24 14:20:18 +02:00
commit e938d71bc3
No known key found for this signature in database
GPG Key ID: CBE9EF3F75BAA5FD
10167 changed files with 5661 additions and 5985324 deletions

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ cscope.*
cover.out
*.coverprofile
external-dns
# vendor dir
vendor/

View File

@ -6,7 +6,7 @@ os:
language: go
go:
- 1.9
- 1.x
- tip
matrix:
@ -14,13 +14,18 @@ matrix:
- go: tip
before_install:
- make dep
- go get github.com/mattn/goveralls
- go get github.com/lawrencewoodman/roveralls
- go get github.com/alecthomas/gometalinter
install:
- gometalinter --install
- sed -i 's/--deadline=50s/--deadline=120s/g'
./vendor/github.com/kubernetes/repo-infra/verify/go-tools/verify-gometalinter.sh
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,29 @@
## v0.5.1 - 2018-05-16
- Refactor implementation of sync loop to use `time.Ticker` (#553) @r0fls
- Document how ExternalDNS gets permission to change AWS Route53 entries (#557) @hjacobs
- Fix CNAME support for the PowerDNS provider (#547) @kciredor
- Add support for hostname annotation in Ingress resource (#545) @rajatjindal
- Fix for TTLs being ignored on headless Services (#546) @danbondd
- Fix failing tests by giving linters more time to do their work (#548) @linki
- Fix misspelled flag for the OpenStack Designate provider (#542) @zentale
- Document additional RBAC rules needed to read Pods (#538) @danbondd
## v0.5.0 - 2018-04-23
- Google: Correctly filter records that don't match all filters (#533) @prydie @linki
- AWS: add support for AWS Network Load Balancers (#531) @linki
- 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
- DigitalOcean: DigitalOcean creates entries with host in them twice (#459) @njuettner
- Bugfix: Retrive all DNSimple response pages (#468) @jbowes
- external-dns does now provide support for multiple targets for A records. This is currently only supported by the Google Cloud DNS provider (#418) @dereulenspiegel
- Graceful handling of misconfigure password for dyn provider (#470) @jvassev
- Don't log sensitive data on start (#463) @jvassev
- Google: Improve logging to help trace misconfigurations (#388) @stealthybox
- AWS: In addition to the one best public hosted zone, records will be added to all matching private hosted zones (#356) @coreypobrien
- Every record managed by External DNS is now mapped to a kubernetes resource (service/ingress) @ideahitme

View File

@ -13,10 +13,11 @@
# 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 . .
RUN make dep
RUN make test
RUN make build

24
Gopkg.lock generated
View File

@ -184,6 +184,12 @@
]
revision = "09691a3b6378b740595c1002f40c34dd5f218a22"
[[projects]]
branch = "master"
name = "github.com/ffledgling/pdns-go"
packages = ["."]
revision = "524e7daccd99651cdb56426eb15b7d61f9597a5c"
[[projects]]
name = "github.com/ghodss/yaml"
packages = ["."]
@ -244,6 +250,22 @@
packages = ["."]
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
[[projects]]
branch = "master"
name = "github.com/gophercloud/gophercloud"
packages = [
".",
"openstack",
"openstack/dns/v2/recordsets",
"openstack/dns/v2/zones",
"openstack/identity/v2/tenants",
"openstack/identity/v2/tokens",
"openstack/identity/v3/tokens",
"openstack/utils",
"pagination"
]
revision = "bfc4756e1a693a850d7d459f4b28b21f35a24b5a"
[[projects]]
name = "github.com/howeyc/gopass"
packages = ["."]
@ -640,6 +662,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "4e118ef87cbafb30cee5759beb6d5fea3acbf8906b41148b3bb116cd1e69f5e9"
inputs-digest = "371e8260c0580f391a14a867114e0b92e9b5b1b1b9fd2945410b6b8f7d1db498"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -27,6 +27,9 @@ cover:
cover-html: cover
go tool cover -html cover.out
dep:
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure -vendor-only
# The verify target runs tasks similar to the CI tasks, but without code coverage
.PHONY: verify test

1
OWNERS
View File

@ -10,3 +10,4 @@ approvers:
- chrislovecnm
- kris-nova
- iterion
- njuettner

View File

@ -21,9 +21,9 @@ The [FAQ](docs/faq.md) contains additional information and addresses several que
To see ExternalDNS in action, have a look at this [video](https://www.youtube.com/watch?v=9HQ2XgL9YVI).
## The Latest Release: v0.4
## The Latest Release: v0.5
ExternalDNS' current release is `v0.4`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
ExternalDNS' current release is `v0.5`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
* [Google CloudDNS](https://cloud.google.com/dns/docs/)
* [AWS Route 53](https://aws.amazon.com/route53/)
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
@ -32,8 +32,10 @@ ExternalDNS' current release is `v0.4`. This version allows you to keep selected
* [DNSimple](https://dnsimple.com/)
* [Infoblox](https://www.infoblox.com/products/dns/)
* [Dyn](https://dyn.com/dns/)
* [OpenStack Designate](https://docs.openstack.org/designate/latest/)
* [PowerDNS](https://www.powerdns.com/)
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.4` with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
Note that all flags can be replaced with environment variables; for instance,
`--dry-run` could be replaced with `EXTERNAL_DNS_DRY_RUN=1`, or
@ -66,10 +68,16 @@ Make sure you have the following prerequisites:
First, get ExternalDNS:
**To install all dependencies, make sure to install [dep](https://github.com/golang/dep) first.**
```console
$ go get -u github.com/kubernetes-incubator/external-dns
$ git clone https://github.com/kubernetes-incubator/external-dns.git && cd external-dns
$ dep ensure -vendor-only
$ make
```
This will create external-dns in the build directory directly from master.
Next, run an application and expose it via a Kubernetes Service:
```console
@ -146,13 +154,23 @@ Here's a rough outline on what is to come (subject to change):
- [x] Support for multiple zones
- [x] Ownership System
### v0.4 - _current version_
### v0.4
- [x] Support for AzureDNS
- [x] Support for CloudFlare
- [x] Support for DigitalOcean
- [x] Multiple DNS names per Service
### v0.5 - _current version_
- [x] Support for creating DNS records to multiple targets (for Google and AWS)
- [x] Support for OpenStack Designate
- [x] Support for PowerDNS
### 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)
@ -161,11 +179,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

@ -66,14 +66,15 @@ func (c *Controller) RunOnce() error {
// Run runs RunOnce in a loop with a delay until stopChan receives a value.
func (c *Controller) Run(stopChan <-chan struct{}) {
ticker := time.NewTicker(c.Interval)
defer ticker.Stop()
for {
err := c.RunOnce()
if err != nil {
log.Error(err)
}
select {
case <-time.After(c.Interval):
case <-ticker.C:
case <-stopChan:
log.Info("Terminating main controller loop")
return

View File

@ -48,25 +48,25 @@ func (p *mockProvider) ApplyChanges(changes *plan.Changes) error {
}
for i := range changes.Create {
if changes.Create[i].DNSName != p.ExpectChanges.Create[i].DNSName || changes.Create[i].Target != p.ExpectChanges.Create[i].Target {
if changes.Create[i].DNSName != p.ExpectChanges.Create[i].DNSName || !changes.Create[i].Targets.Same(p.ExpectChanges.Create[i].Targets) {
return errors.New("created record is wrong")
}
}
for i := range changes.UpdateNew {
if changes.UpdateNew[i].DNSName != p.ExpectChanges.UpdateNew[i].DNSName || changes.UpdateNew[i].Target != p.ExpectChanges.UpdateNew[i].Target {
if changes.UpdateNew[i].DNSName != p.ExpectChanges.UpdateNew[i].DNSName || !changes.UpdateNew[i].Targets.Same(p.ExpectChanges.UpdateNew[i].Targets) {
return errors.New("delete record is wrong")
}
}
for i := range changes.UpdateOld {
if changes.UpdateOld[i].DNSName != p.ExpectChanges.UpdateOld[i].DNSName || changes.UpdateOld[i].Target != p.ExpectChanges.UpdateOld[i].Target {
if changes.UpdateOld[i].DNSName != p.ExpectChanges.UpdateOld[i].DNSName || !changes.UpdateOld[i].Targets.Same(p.ExpectChanges.UpdateOld[i].Targets) {
return errors.New("delete record is wrong")
}
}
for i := range changes.Delete {
if changes.Delete[i].DNSName != p.ExpectChanges.Delete[i].DNSName || changes.Delete[i].Target != p.ExpectChanges.Delete[i].Target {
if changes.Delete[i].DNSName != p.ExpectChanges.Delete[i].DNSName || !changes.Delete[i].Targets.Same(p.ExpectChanges.Delete[i].Targets) {
return errors.New("delete record is wrong")
}
}
@ -91,11 +91,11 @@ func TestRunOnce(t *testing.T) {
source.On("Endpoints").Return([]*endpoint.Endpoint{
{
DNSName: "create-record",
Target: "1.2.3.4",
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "update-record",
Target: "8.8.4.4",
Targets: endpoint.Targets{"8.8.4.4"},
},
}, nil)
@ -104,25 +104,25 @@ func TestRunOnce(t *testing.T) {
[]*endpoint.Endpoint{
{
DNSName: "update-record",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "delete-record",
Target: "4.3.2.1",
Targets: endpoint.Targets{"4.3.2.1"},
},
},
&plan.Changes{
Create: []*endpoint.Endpoint{
{DNSName: "create-record", Target: "1.2.3.4"},
{DNSName: "create-record", Targets: endpoint.Targets{"1.2.3.4"}},
},
UpdateNew: []*endpoint.Endpoint{
{DNSName: "update-record", Target: "8.8.4.4"},
{DNSName: "update-record", Targets: endpoint.Targets{"8.8.4.4"}},
},
UpdateOld: []*endpoint.Endpoint{
{DNSName: "update-record", Target: "8.8.8.8"},
{DNSName: "update-record", Targets: endpoint.Targets{"8.8.8.8"}},
},
Delete: []*endpoint.Endpoint{
{DNSName: "delete-record", Target: "4.3.2.1"},
{DNSName: "delete-record", Targets: endpoint.Targets{"4.3.2.1"}},
},
},
)

View File

@ -22,6 +22,7 @@ All sources live in package `source`.
* `ServiceSource`: collects all Services that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to an annotation set on the Service or is compiled from the Service attributes via the FQDN Go template string.
* `IngressSource`: collects all Ingresses that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to the host rules defined in the Ingress object.
* `FakeSource`: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
* `ConnectorSource`: returns a list of Endpoint objects which are served by a tcp server configured through `connector-source-server` flag.
### Providers

View File

@ -1,6 +1,6 @@
# Frequently asked questions
### When would ExternalDNS become useful to me?
### How is ExternalDNS useful to me?
You've probably created many deployments. Typically, you expose your deployment to the Internet by creating a Service with `type=LoadBalancer`. Depending on your environment, this usually assigns a random publicly available endpoint to your service that you can access from anywhere in the world. On Google Container Engine, this is a public IP address:
@ -28,17 +28,26 @@ ExternalDNS can solve this for you as well.
### Which DNS providers are supported?
So far, Google CloudDNS and AWS Route 53 with ALIAS records. There's interest in supporting CoreDNS and Azure DNS. We're open to discussing/adding other providers if the community believes it would be valuable.
Currently, the following providers are supported:
Initial support for Google CloudDNS is available since the `v0.1` release. Initial support for AWS Route 53 is available in the `v0.2` release (CNAME based) and ALIAS is targeted for the `v0.3` release.
- Google CloudDNS
- AWS Route 53
- AzureDNS
- CloudFlare
- DigitalOcean
- DNSimple
- Infoblox
- Dyn
- OpenStack Designate
- PowerDNS
There are no plans regarding other providers at the moment.
As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.
### Which Kubernetes objects are supported?
Services exposed via `type=LoadBalancer` and for the hostnames defined in Ingress objects. It also seems useful to expose Services with `type=NodePort` to point to your cluster's nodes directly, but there's no commitment to doing this yet.
Services exposed via `type=LoadBalancer` and for the hostnames defined in Ingress objects as well as headless hostPort services. An initial effort to support type `NodePort` was started as of May 2018 and it is in progress at the time of writing.
### How do I specify DNS name for my Kubernetes objects?
### How do I specify a 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:
@ -48,6 +57,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, 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.
@ -61,11 +74,11 @@ Regarding Ingress, we'll support:
For Ingress objects, ExternalDNS will attempt to discover the target hostname of the relevant Ingress Controller automatically. If you are using an Ingress Controller that is not listed above you may have issues with ExternalDNS not discovering Endpoints and consequently not creating any DNS records. As a workaround, it is possible to force create an Endpoint by manually specifying a target host/IP for the records to be created by setting the annotation `external-dns.alpha.kubernetes.io/target` in the Ingress object.
Another reason you may want to override the ingress hostname or IP address is if you have an external mechanism for handling failover across ingress endpoints. Possible scenarios for this would include using [keepalived-vip](https://github.com/kubernetes/contrib/tree/master/keepalived-vip) to manage failover faster than DNS TTLs might expire.
Another reason you may want to override the ingress hostname or IP address is if you have an external mechanism for handling failover across ingress endpoints. Possible scenarios for this would include using [keepalived-vip](https://github.com/kubernetes/contrib/tree/master/keepalived-vip) to manage failover faster than DNS TTLs might expire.
Note that if you set the target to a hostname, then a CNAME record will be created. In this case, the hostname specified in the Ingress object's annotation must already exist. (i.e.: You have a Service resource for your Ingress Controller with the `external-dns.alpha.kubernetes.io/hostname` annotation set to the same value.)
Note that if you set the target to a hostname, then a CNAME record will be created. In this case, the hostname specified in the Ingress object's annotation must already exist. (i.e. you have a Service resource for your Ingress Controller with the `external-dns.alpha.kubernetes.io/hostname` annotation set to the same value.)
### What about those other projects?
### What about other projects similar to ExternalDNS?
ExternalDNS is a joint effort to unify different projects accomplishing the same goals, namely:
@ -87,11 +100,11 @@ For now ExternalDNS uses TXT records to label owned records, and there might be
### Does anyone use ExternalDNS in production?
Yes — Zalando replaced [Mate](https://github.com/linki/mate) with ExternalDNS since its v0.3 release, which now runs in production-level clusters. We are planning to document a step-by-step tutorial on how the switch from Mate to ExternalDNS has occurred.
Yes, multiple companies are using ExternalDNS in production. Zalando, as an example, has been using it in production since its v0.3 release, mostly using the AWS provider.
### 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) or any other supported provider.
### 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?
@ -131,11 +144,11 @@ spec:
### I have a Service/Ingress but it's ignored by ExternalDNS. Why?
TODO (https://github.com/kubernetes-incubator/external-dns/issues/267)
ExternalDNS can be configured to only use Services or Ingresses as source. In case Services or Ingresses seem to be ignored in your setup, consider checking how the flag `--source` was configured when deployed. For reference, see the issue https://github.com/kubernetes-incubator/external-dns/issues/267.
### I'm using an ELB with TXT registry but the CNAME record clashes with the TXT record. How to avoid this?
TODO (https://github.com/kubernetes-incubator/external-dns/issues/262)
CNAMEs cannot co-exist with other records, therefore you can use the `--txt-prefix` flag which makes sure to create a TXT record with a name following the pattern `prefix.<CNAME record>`. For reference, see the issue https://github.com/kubernetes-incubator/external-dns/issues/262.
### Which permissions do I need when running ExternalDNS on a GCE or GKE node.
@ -156,7 +169,7 @@ $ docker run \
-e EXTERNAL_DNS_SOURCE=$'service\ningress' \
-e EXTERNAL_DNS_PROVIDER=google \
-e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \
registry.opensource.zalan.do/teapot/external-dns:v0.4.8
registry.opensource.zalan.do/teapot/external-dns:v0.5.1
time="2017-08-08T14:10:26Z" level=info msg="config: &{Master: KubeConfig: Sources:[service ingress] Namespace: ...
```

View File

@ -6,7 +6,7 @@
[Initial discussion](https://docs.google.com/document/d/1ML_q3OppUtQKXan6Q42xIq2jelSoIivuXI8zExbc6ec/edit#heading=h.1pgkuagjhm4p)
This document describes the initial design proposal
This document describes the initial design proposal.
External DNS is purposed to fill the existing gap of creating DNS records for Kubernetes resources. While there exist alternative solutions, this project is meant to be a standard way of managing DNS records for Kubernetes. The current project is a fusion of the following projects and driven by its maintainers:
@ -41,8 +41,6 @@ DNS records will be automatically created in multiple situations:
### Annotations
TODO:*This should probably be placed in a separate file*.
Record configuration should occur via resource annotations. Supported annotations:
| Annotations | |
@ -63,8 +61,6 @@ Record configuration should occur via resource annotations. Supported annotation
External DNS should be compatible with annotations used by three above mentioned projects. The idea is that resources created and tagged with annotations for other projects should continue to be valid and now managed by External DNS.
TODO:*Add complete list here*
**Mate**
Mate does not require services/ingress to be tagged. Therefore, it is not safe to run both Mate and External-DNS simultaneously. The idea is that initial release (?) of External DNS will support Mate annotations, which indicates the hostname to be created. Therefore the switch should be simple.

View File

@ -31,6 +31,8 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes c
}
```
When running on AWS, you need to make sure that your nodes (on which External DNS runs) have the IAM instance profile with the above IAM role assigned (either directly or via something like [kube2iam](https://github.com/jtblin/kube2iam)).
## Set up a hosted zone
*If you prefer to try-out ExternalDNS in one of the existing hosted-zones you can skip this step*
@ -63,8 +65,9 @@ In this case it's the ones shown above but your's will differ.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply the following manifest file to deploy ExternalDNS.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
@ -80,7 +83,7 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --source=ingress
@ -92,6 +95,71 @@ spec:
- --txt-owner-id=my-identifier
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --source=ingress
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=aws
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
```
## Arguments
This list is not the full list, but a few arguments that where chosen.

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,58 +49,85 @@ 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
Create a deployment file called `externaldns.yaml` with the following contents:
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.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
@ -115,9 +143,77 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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
@ -161,36 +257,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

View File

@ -22,7 +22,10 @@ The environment vars `CF_API_KEY` and `CF_API_EMAIL` will be needed to run Exter
## Deploy ExternalDNS
Create a deployment file called `externaldns.yaml` with the following contents:
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
@ -39,7 +42,7 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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.
@ -52,10 +55,68 @@ spec:
value: "YOUR_CLOUDFLARE_EMAIL"
```
Create the deployment for ExternalDNS:
### Manifest (for clusters with RBAC enabled)
```
$ kubectl create -f externaldns.yaml
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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=cloudflare
- --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
env:
- name: CF_API_KEY
value: "YOUR_CLOUDFLARE_API_KEY"
- name: CF_API_EMAIL
value: "YOUR_CLOUDFLARE_EMAIL"
```
## Deploying an Nginx Service

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

@ -20,8 +20,10 @@ The environment variable `DO_TOKEN` will be needed to run ExternalDNS with Digit
## Deploy ExternalDNS
Create a deployment file called `externaldns.yaml` with the following contents:
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
@ -37,7 +39,7 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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.
@ -47,12 +49,67 @@ spec:
value: "YOUR_DIGITALOCEAN_API_KEY"
```
Create the deployment for ExternalDNS:
```console
$ kubectl create -f externaldns.yaml
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
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=digitalocean
env:
- name: DO_TOKEN
value: "YOUR_DIGITALOCEAN_API_KEY"
```
## Deploying an Nginx Service
Create a service file called 'nginx.yaml' with the following contents:

View File

@ -41,7 +41,7 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=ingress
- --txt-prefix=_d

View File

@ -54,14 +54,25 @@ $ gcloud dns record-sets transaction execute --zone "gcp-zalan-do"
## Deploy ExternalDNS
### Role-Based Access Control (RBAC)
[RBAC]("https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control") is enabled by default on all Container clusters which are running Kubernetes version 1.6 or higher.
Because of the way Container Engine checks permissions when you create a Role or ClusterRole, you must first create a RoleBinding that grants you all of the permissions included in the role you want to create.
```console
kubectl create clusterrolebinding your-user-cluster-admin-binding --clusterrole=cluster-admin --user=your.google.cloud.email@example.org
```
Connect your `kubectl` client to the cluster you just created.
```console
gcloud container clusters get-credentials "external-dns"
```
Apply the following manifest file to deploy ExternalDNS.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
@ -77,13 +88,75 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --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
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --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 # 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

@ -12,6 +12,7 @@ We will go through a small example of deploying a simple Kafka with use of a hea
### Exernal DNS
A simple deploy could look like this:
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
@ -24,7 +25,66 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --debug
- --source=service
- --source=ingress
- --namespace=dev
- --domain-filter=example.org.
- --provider=aws
- --registry=txt
- --txt-owner-id=dev.example.org
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: exeternal-dns
spec:
strategy:
type: Recreate
template:
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --debug
- --source=service
@ -42,7 +102,6 @@ spec:
First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called `kafka-hsvc`
```yaml
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
@ -83,7 +142,7 @@ spec:
resources:
requests:
storage: 500Gi
```
```
Very important here, is to set the `hostport`(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself.
### Headless Service
@ -96,7 +155,6 @@ Now we need to define a headless service to use to expose the Kafka pods. There
If you go with #1, you just need to define the headless service, here is an example of the case #2:
```yaml
---
apiVersion: v1
kind: Service
metadata:

View File

@ -47,10 +47,11 @@ $ kubectl create secret generic external-dns \
## Deploy ExternalDNS
Create a deployment file called `externaldns.yaml` with the following contents:
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
```
$ cat > externaldns.yaml <<EOF
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
@ -65,7 +66,7 @@ spec:
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --domain-filter=example.com # (optional) limit to only example.com domains.
@ -89,14 +90,85 @@ spec:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
EOF
```
Create the deployment for ExternalDNS:
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service
- --domain-filter=example.com # (optional) limit to only example.com domains.
- --provider=infoblox
- --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host.
- --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is "443".
- --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is "2.3.1"
- --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification.
env:
- name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS
value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10".
- name: EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT
value: "60" # (optional) Infoblox WAPI request timeout in seconds. The default is "60".
- name: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
- name: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
```
$ kubectl create -f externaldns.yaml
```
## Deploying an Nginx Service

View File

@ -203,6 +203,39 @@ spec:
Apply the following manifest file to deploy ExternalDNS.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
@ -215,9 +248,10 @@ spec:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=ingress
- --domain-filter=external-dns-test.gcp.zalan.do

154
docs/tutorials/pdns.md Normal file
View File

@ -0,0 +1,154 @@
# Setting up ExternalDNS for PowerDNS
## Prerequisites
The provider has been written for and tested against [PowerDNS](https://github.com/PowerDNS/pdns) v4.1.x and thus requires **PowerDNS Auth Server >= 4.1.x**
PowerDNS provider support was added via [this PR](https://github.com/kubernetes-incubator/external-dns/pull/373), thus you need to use external-dns version >= v0.5
The PDNS provider expects that your PowerDNS instance is already setup and
functional. It expects that zones, you wish to add records to, already exist
and are configured correctly. It does not add, remove or configure new zones in
anyway.
## Feature Support
The PDNS provider currently does not support:
1. Dry running a configuration is not supported.
2. The `--domain-filter` flag is not supported.
## Deployment
Deploying external DNS for PowerDNS is actually nearly identical to deploying
it for other providers. This is what a sample `deployment.yaml` looks like:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: deploy-external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
# Only use if you're also using RBAC
# serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
args:
- --source=service # or ingress or both
- --provider=pdns
- --pdns-server={{ pdns-api-url }}
- --pdns-api-key={{ pdns-http-api-key }}
- --txt-owner-id={{ owner-id-for-this-external-dns }}
- --log-level=debug
- --interval=30s
```
## RBAC
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
```
## Testing and Verification
**Important!**: Remember to change `example.com` with your own domain throughout the following text.
Spin up a simple "Hello World" HTTP server with the following spec (`kubectl apply -f`):
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: echo
spec:
template:
metadata:
labels:
app: echo
spec:
containers:
- image: hashicorp/http-echo
name: echo
ports:
- containerPort: 5678
args:
- -text="Hello World"
---
apiVersion: v1
kind: Service
metadata:
name: echo
annotations:
external-dns.alpha.kubernetes.io/hostname: echo.example.com
spec:
selector:
app: echo
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 5678
```
**Important!**: Don't run dig, nslookup or similar immediately (until you've
confirmed the record exists). You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush.
Run the following to make sure everything is in order:
```bash
$ kubectl get services echo
$ kubectl get endpoints echo
```
Make sure everything looks correct, i.e the service is defined and recieves a
public IP, and that the endpoint also has a pod IP.
Once that's done, wait about 30s-1m (interval for external-dns to kick in), then do:
```bash
$ curl -H "X-API-Key: ${PDNS_API_KEY}" ${PDNS_API_URL}/api/v1/servers/localhost/zones/example.com. | jq '.rrsets[] | select(.name | contains("echo"))'
```
Once the API shows the record correctly, you can double check your record using:
```bash
$ dig @${PDNS_FQDN} echo.example.com.
```

View File

@ -0,0 +1,386 @@
# Setting up ExternalDNS using the same domain for public and private Route53 zones
This tutorial describes how to setup ExternalDNS using the same domain for public and private Route53 zones and [nginx-ingress-controller](https://github.com/kubernetes/ingress-nginx). It also outlines how to use [cert-manager](https://github.com/jetstack/cert-manager) to automatically issue SSL certificates from [Let's Encrypt](https://letsencrypt.org/) for both public and private records.
## Deploy public nginx-ingress-controller
Consult [External DNS nginx ingress docs](nginx-ingress.md) for installation guidelines.
Specify `ingress-class` in nginx-ingress-controller container args:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: external-ingress
name: external-ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: external-ingress
template:
metadata:
labels:
app: external-ingress
spec:
containers:
- args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --configmap=$(POD_NAMESPACE)/external-ingress-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/external-tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/external-udp-services
- --annotations-prefix=nginx.ingress.kubernetes.io
- --ingress-class=external-ingress
- --publish-service=$(POD_NAMESPACE)/external-ingress
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
name: external-ingress-controller
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 443
name: https
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
```
Set `type: LoadBalancer` in your public nginx-ingress-controller Service definition.
```yaml
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
labels:
app: external-ingress
name: external-ingress
spec:
externalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
app: external-ingress
sessionAffinity: None
type: LoadBalancer
```
## Deploy private nginx-ingress-controller
Consult [External DNS nginx ingress docs](nginx-ingress.md) for installation guidelines.
Make sure to specify `ingress-class` in nginx-ingress-controller container args:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: internal-ingress
name: internal-ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: internal-ingress
template:
metadata:
labels:
app: internal-ingress
spec:
containers:
- args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --configmap=$(POD_NAMESPACE)/internal-ingress-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/internal-tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/internal-udp-services
- --annotations-prefix=nginx.ingress.kubernetes.io
- --ingress-class=internal-ingress
- --publish-service=$(POD_NAMESPACE)/internal-ingress
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
name: internal-ingress-controller
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 443
name: https
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
```
Set additional annotations in your private nginx-ingress-controller Service definition to create an internal load balancer.
```yaml
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
labels:
app: internal-ingress
name: internal-ingress
spec:
externalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
app: internal-ingress
sessionAffinity: None
type: LoadBalancer
```
## Deploy the public zone ExternalDNS
Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines.
In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`:
```yaml
apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
app: external-dns-public
name: external-dns-public
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: external-dns-public
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns-public
spec:
containers:
- args:
- --source=ingress
- --provider=aws
- --registry=txt
- --txt-owner-id=external-dns
- --annotation-filter=kubernetes.io/ingress.class=external-ingress
- --aws-zone-type=public
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
name: external-dns-public
```
## Deploy the private zone ExternalDNS
Consult [AWS ExternalDNS setup docs](aws.md) for installation guidelines.
In ExternalDNS containers args, make sure to specify `annotation-filter` and `aws-zone-type`:
```yaml
apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
app: external-dns-private
name: external-dns-private
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: external-dns-private
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns-private
spec:
containers:
- args:
- --source=ingress
- --provider=aws
- --registry=txt
- --txt-owner-id=dev.k8s.nexus
- --annotation-filter=kubernetes.io/ingress.class=internal-ingress
- --aws-zone-type=private
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
name: external-dns-private
```
## Create application Service definitions
For this setup to work, you've to create two Service definitions for your application.
At first, create public Service definition:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: "external-ingress"
labels:
app: app
name: app-public
spec:
rules:
- host: app.domain.com
http:
paths:
- backend:
serviceName: app
servicePort: 80
```
Then create private Service definition:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: "internal-ingress"
labels:
app: app
name: app-private
spec:
rules:
- host: app.domain.com
http:
paths:
- backend:
serviceName: app
servicePort: 80
```
Additionally, you may leverage [cert-manager](https://github.com/jetstack/cert-manager) to automatically issue SSL certificates from [Let's Encrypt](https://letsencrypt.org/). To do that, request a certificate in public service definition:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
certmanager.k8s.io/acme-challenge-type: "dns01"
certmanager.k8s.io/acme-dns01-provider: "route53"
certmanager.k8s.io/cluster-issuer: "letsencrypt-production"
kubernetes.io/ingress.class: "external-ingress"
kubernetes.io/tls-acme: "true"
labels:
app: app
name: app-public
spec:
rules:
- host: app.domain.com
http:
paths:
- backend:
serviceName: app
servicePort: 80
tls:
- hosts:
- app.domain.com
secretName: app-tls
```
And reuse the requested certificate in private Service definition:
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: "internal-ingress"
labels:
app: app
name: app-private
spec:
rules:
- host: app.domain.com
http:
paths:
- backend:
serviceName: app
servicePort: 80
tls:
- hosts:
- app.domain.com
secretName: app-tls
```

View File

@ -18,6 +18,7 @@ package endpoint
import (
"fmt"
"sort"
"strings"
)
@ -38,12 +39,78 @@ func (ttl TTL) IsConfigured() bool {
return ttl > 0
}
// Targets is a representation of a list of targets for an endpoint.
type Targets []string
// NewTargets is a convenience method to create a new Targets object from a vararg of strings
func NewTargets(target ...string) Targets {
t := make(Targets, 0, len(target))
t = append(t, target...)
return t
}
func (t Targets) String() string {
return strings.Join(t, ";")
}
func (t Targets) Len() int {
return len(t)
}
func (t Targets) Less(i, j int) bool {
return t[i] < t[j]
}
func (t Targets) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
// Same compares to Targets and returns true if they are completely identical
func (t Targets) Same(o Targets) bool {
if len(t) != len(o) {
return false
}
sort.Stable(t)
sort.Stable(o)
for i, e := range t {
if e != o[i] {
return false
}
}
return true
}
// IsLess should fulfill the requirement to compare two targets and chosse the 'lesser' one.
// In the past target was a simple string so simple string comparison could be used. Now we define 'less'
// as either being the shorter list of targets or where the first entry is less.
// FIXME We really need to define under which circumstances a list Targets is considered 'less'
// than another.
func (t Targets) IsLess(o Targets) bool {
if len(t) < len(o) {
return true
}
if len(t) > len(o) {
return false
}
sort.Sort(t)
sort.Sort(o)
for i, e := range t {
if e != o[i] {
return e < o[i]
}
}
return false
}
// Endpoint is a high-level way of a connection between a service and an IP
type Endpoint struct {
// The hostname of the DNS record
DNSName string
// The target the DNS record points to
Target string
// The targets the DNS record points to
Targets Targets
// RecordType type of record, e.g. CNAME, A, TXT etc
RecordType string
// TTL for the record
@ -53,15 +120,20 @@ type Endpoint struct {
}
// NewEndpoint initialization method to be used to create an endpoint
func NewEndpoint(dnsName, target, recordType string) *Endpoint {
return NewEndpointWithTTL(dnsName, target, recordType, TTL(0))
func NewEndpoint(dnsName, recordType string, targets ...string) *Endpoint {
return NewEndpointWithTTL(dnsName, recordType, TTL(0), targets...)
}
// NewEndpointWithTTL initialization method to be used to create an endpoint with a TTL struct
func NewEndpointWithTTL(dnsName, target, recordType string, ttl TTL) *Endpoint {
func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) *Endpoint {
cleanTargets := make([]string, len(targets))
for idx, target := range targets {
cleanTargets[idx] = strings.TrimSuffix(target, ".")
}
return &Endpoint{
DNSName: strings.TrimSuffix(dnsName, "."),
Target: strings.TrimSuffix(target, "."),
Targets: cleanTargets,
RecordType: recordType,
Labels: NewLabels(),
RecordTTL: ttl,
@ -69,5 +141,5 @@ func NewEndpointWithTTL(dnsName, target, recordType string, ttl TTL) *Endpoint {
}
func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Target)
return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets)
}

View File

@ -21,16 +21,57 @@ import (
)
func TestNewEndpoint(t *testing.T) {
e := NewEndpoint("example.org", "foo.com", "CNAME")
if e.DNSName != "example.org" || e.Target != "foo.com" || e.RecordType != "CNAME" {
e := NewEndpoint("example.org", "CNAME", "foo.com")
if e.DNSName != "example.org" || e.Targets[0] != "foo.com" || e.RecordType != "CNAME" {
t.Error("endpoint is not initialized correctly")
}
if e.Labels == nil {
t.Error("Labels is not initialized")
}
w := NewEndpoint("example.org.", "load-balancer.com.", "")
if w.DNSName != "example.org" || w.Target != "load-balancer.com" || w.RecordType != "" {
w := NewEndpoint("example.org.", "", "load-balancer.com.")
if w.DNSName != "example.org" || w.Targets[0] != "load-balancer.com" || w.RecordType != "" {
t.Error("endpoint is not initialized correctly")
}
}
func TestTargetsSame(t *testing.T) {
tests := []Targets{
{""},
{"1.2.3.4"},
{"8.8.8.8", "8.8.4.4"},
}
for _, d := range tests {
if d.Same(d) != true {
t.Errorf("%#v should equal %#v", d, d)
}
}
}
func TestSameFailures(t *testing.T) {
tests := []struct {
a Targets
b Targets
}{
{
[]string{"1.2.3.4"},
[]string{"4.3.2.1"},
}, {
[]string{"1.2.3.4"},
[]string{"1.2.3.4", "4.3.2.1"},
}, {
[]string{"1.2.3.4", "4.3.2.1"},
[]string{"1.2.3.4"},
}, {
[]string{"1.2.3.4", "4.3.2.1"},
[]string{"8.8.8.8", "8.8.4.4"},
},
}
for _, d := range tests {
if d.a.Same(d.b) == true {
t.Errorf("%#v should not equal %#v", d.a, d.b)
}
}
}

View File

@ -33,13 +33,12 @@ func (b byAllFields) Less(i, j int) bool {
return true
}
if b[i].DNSName == b[j].DNSName {
if b[i].Target < b[j].Target {
return true
}
if b[i].Target == b[j].Target {
// This rather bad, we need a more complex comparison for Targets, which considers all elements
if b[i].Targets.Same(b[j].Targets) {
return b[i].RecordType <= b[j].RecordType
}
return false
return b[i].Targets.String() <= b[j].Targets.String()
}
return false
}
@ -47,7 +46,7 @@ func (b byAllFields) Less(i, j int) bool {
// SameEndpoint returns true if two endpoints are same
// considers example.org. and example.org DNSName/Target as different endpoints
func SameEndpoint(a, b *endpoint.Endpoint) bool {
return a.DNSName == b.DNSName && a.Target == b.Target && a.RecordType == b.RecordType &&
return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType &&
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL &&
a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey]
}

View File

@ -27,31 +27,31 @@ func ExampleSameEndpoints() {
eps := []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "load-balancer.org",
Targets: endpoint.Targets{"load-balancer.org"},
},
{
DNSName: "example.org",
Target: "load-balancer.org",
Targets: endpoint.Targets{"load-balancer.org"},
RecordType: endpoint.RecordTypeTXT,
},
{
DNSName: "abc.com",
Target: "something",
Targets: endpoint.Targets{"something"},
RecordType: endpoint.RecordTypeTXT,
},
{
DNSName: "abc.com",
Target: "1.2.3.4",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "bbc.com",
Target: "foo.com",
Targets: endpoint.Targets{"foo.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "cbc.com",
Target: "foo.com",
Targets: endpoint.Targets{"foo.com"},
RecordType: "CNAME",
RecordTTL: endpoint.TTL(60),
},

35
main.go
View File

@ -41,7 +41,7 @@ func main() {
if err := cfg.ParseFlags(os.Args[1:]); err != nil {
log.Fatalf("flag parsing error: %v", err)
}
log.Infof("config: %+v", cfg)
log.Infof("config: %s", cfg)
if err := validation.ValidateConfig(cfg); err != nil {
log.Fatalf("config validation failed: %v", err)
@ -67,11 +67,13 @@ 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,
ConnectorServer: cfg.ConnectorSourceServer,
}
// Lookup all the selected sources by names and pass them the desired configuration.
@ -93,7 +95,7 @@ func main() {
var p provider.Provider
switch cfg.Provider {
case "aws":
p, err = provider.NewAWSProvider(domainFilter, zoneIDFilter, zoneTypeFilter, cfg.DryRun)
p, err = provider.NewAWSProvider(domainFilter, zoneIDFilter, zoneTypeFilter, cfg.AWSAssumeRole, cfg.DryRun)
case "azure":
p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.DryRun)
case "cloudflare":
@ -121,19 +123,24 @@ func main() {
case "dyn":
p, err = provider.NewDynProvider(
provider.DynConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
DryRun: cfg.DryRun,
CustomerName: cfg.DynCustomerName,
Username: cfg.DynUsername,
Password: cfg.DynPassword,
AppVersion: externaldns.Version,
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
DryRun: cfg.DryRun,
CustomerName: cfg.DynCustomerName,
Username: cfg.DynUsername,
Password: cfg.DynPassword,
MinTTLSeconds: cfg.DynMinTTLSeconds,
AppVersion: externaldns.Version,
},
)
case "coredns", "skydns":
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.DryRun)
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)
case "pdns":
p, err = provider.NewPDNSProvider(cfg.PDNSServer, cfg.PDNSAPIKey, domainFilter, cfg.DryRun)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package externaldns
import (
"fmt"
"strconv"
"time"
@ -24,6 +25,10 @@ import (
"github.com/sirupsen/logrus"
)
const (
passwordMask = "******"
)
var (
// Version is the current version of the app, generated at build time
Version = "unknown"
@ -31,77 +36,88 @@ 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
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
ConnectorSourceServer string
Provider string
GoogleProject string
DomainFilter []string
ZoneIDFilter []string
AWSZoneType string
AWSAssumeRole 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
PDNSServer string
PDNSAPIKey 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,
ConnectorSourceServer: "localhost:8080",
Provider: "",
GoogleProject: "",
DomainFilter: []string{},
AWSZoneType: "",
AWSAssumeRole: "",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
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
@ -109,6 +125,19 @@ func NewConfig() *Config {
return &Config{}
}
func (cfg *Config) String() string {
// prevent logging of sensitive information
temp := *cfg
if temp.DynPassword != "" {
temp.DynPassword = passwordMask
}
if temp.InfobloxWapiPassword != "" {
temp.InfobloxWapiPassword = passwordMask
}
return fmt.Sprintf("%+v", temp)
}
// allLogLevelsAsStrings returns all logrus levels as a list of strings
func allLogLevelsAsStrings() []string {
var levels []string
@ -129,19 +158,22 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig)
// Flags related to processing sources
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake", "connector")
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)
app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)
// 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, coredns, skydns, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "coredns", "skydns", "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, coredns, skydns, inmemory, pdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns")
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("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
@ -154,7 +186,11 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName)
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword)
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
// Flags related to policies
app.Flag("policy", "Modify how DNS records are sychronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only")

View File

@ -18,6 +18,7 @@ package externaldns
import (
"os"
"strings"
"testing"
"time"
@ -28,71 +29,79 @@ import (
var (
minimalConfig = &Config{
Master: "",
KubeConfig: "",
Sources: []string{"service"},
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
Provider: "google",
GoogleProject: "",
DomainFilter: []string{""},
ZoneIDFilter: []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: []string{"service"},
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
Provider: "google",
GoogleProject: "",
DomainFilter: []string{""},
ZoneIDFilter: []string{""},
AWSZoneType: "",
AWSAssumeRole: "",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
Policy: "sync",
Registry: "txt",
TXTOwnerID: "default",
TXTPrefix: "",
Interval: time.Minute,
Once: false,
DryRun: false,
LogFormat: "text",
MetricsAddress: ":7979",
LogLevel: logrus.InfoLevel.String(),
ConnectorSourceServer: "localhost:8080",
}
overriddenConfig = &Config{
Master: "http://127.0.0.1:8080",
KubeConfig: "/some/path",
Sources: []string{"service", "ingress"},
Namespace: "namespace",
FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate",
Provider: "google",
GoogleProject: "project",
DomainFilter: []string{"example.org", "company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AWSZoneType: "private",
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
CloudflareProxied: true,
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxSSLVerify: false,
InMemoryZones: []string{"example.org", "company.com"},
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record",
Interval: 10 * time.Minute,
Once: true,
DryRun: true,
LogFormat: "json",
MetricsAddress: "127.0.0.1:9099",
LogLevel: logrus.DebugLevel.String(),
Master: "http://127.0.0.1:8080",
KubeConfig: "/some/path",
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
FQDNTemplate: "{{.Name}}.service.example.com",
Compatibility: "mate",
Provider: "google",
GoogleProject: "project",
DomainFilter: []string{"example.org", "company.com"},
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AWSZoneType: "private",
AWSAssumeRole: "some-other-role",
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
CloudflareProxied: true,
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxSSLVerify: false,
InMemoryZones: []string{"example.org", "company.com"},
PDNSServer: "http://ns.example.com:8081",
PDNSAPIKey: "some-secret-key",
Policy: "upsert-only",
Registry: "noop",
TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record",
Interval: 10 * time.Minute,
Once: true,
DryRun: true,
LogFormat: "json",
MetricsAddress: "127.0.0.1:9099",
LogLevel: logrus.DebugLevel.String(),
ConnectorSourceServer: "localhost:8081",
}
)
@ -119,6 +128,7 @@ func TestParseFlags(t *testing.T) {
"--kubeconfig=/some/path",
"--source=service",
"--source=ingress",
"--source=connector",
"--namespace=namespace",
"--fqdn-template={{.Name}}.service.example.com",
"--compatibility=mate",
@ -134,12 +144,15 @@ func TestParseFlags(t *testing.T) {
"--infoblox-wapi-version=2.6.1",
"--inmemory-zone=example.org",
"--inmemory-zone=company.com",
"--pdns-server=http://ns.example.com:8081",
"--pdns-api-key=some-secret-key",
"--no-infoblox-ssl-verify",
"--domain-filter=example.org",
"--domain-filter=company.com",
"--zone-id-filter=/hostedzone/ZTST1",
"--zone-id-filter=/hostedzone/ZTST2",
"--aws-zone-type=private",
"--aws-assume-role=some-other-role",
"--policy=upsert-only",
"--registry=noop",
"--txt-owner-id=owner-1",
@ -150,6 +163,7 @@ func TestParseFlags(t *testing.T) {
"--log-format=json",
"--metrics-address=127.0.0.1:9099",
"--log-level=debug",
"--connector-source-server=localhost:8081",
},
envVars: map[string]string{},
expected: overriddenConfig,
@ -158,37 +172,41 @@ func TestParseFlags(t *testing.T) {
title: "override everything via environment variables",
args: []string{},
envVars: map[string]string{
"EXTERNAL_DNS_MASTER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_SOURCE": "service\ningress",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_ONCE": "1",
"EXTERNAL_DNS_DRY_RUN": "1",
"EXTERNAL_DNS_LOG_FORMAT": "json",
"EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099",
"EXTERNAL_DNS_LOG_LEVEL": "debug",
"EXTERNAL_DNS_MASTER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
"EXTERNAL_DNS_COMPATIBILITY": "mate",
"EXTERNAL_DNS_PROVIDER": "google",
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_ONCE": "1",
"EXTERNAL_DNS_DRY_RUN": "1",
"EXTERNAL_DNS_LOG_FORMAT": "json",
"EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099",
"EXTERNAL_DNS_LOG_LEVEL": "debug",
"EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081",
},
expected: overriddenConfig,
},
@ -222,3 +240,15 @@ func restoreEnv(t *testing.T, originalEnv map[string]string) {
require.NoError(t, os.Setenv(k, v))
}
}
func TestPasswordsNotLogged(t *testing.T) {
cfg := Config{
DynPassword: "dyn-pass",
InfobloxWapiPassword: "infoblox-pass",
}
s := cfg.String()
assert.False(t, strings.Contains(s, "dyn-pass"))
assert.False(t, strings.Contains(s, "infoblox-pass"))
}

View File

@ -52,5 +52,18 @@ func ValidateConfig(cfg *externaldns.Config) error {
return errors.New("no Infoblox WAPI password specified")
}
}
if cfg.Provider == "dyn" {
if cfg.DynUsername == "" {
return errors.New("no Dyn username specified")
}
if cfg.DynCustomerName == "" {
return errors.New("no Dyn customer name specified")
}
if cfg.DynMinTTLSeconds < 0 {
return errors.New("TTL specified for Dyn is negative")
}
}
return nil
}

View File

@ -63,3 +63,56 @@ func newValidConfig(t *testing.T) *externaldns.Config {
return cfg
}
func addRequiredFieldsForDyn(cfg *externaldns.Config) {
cfg.LogFormat = "json"
cfg.Sources = []string{"ingress"}
cfg.Provider = "dyn"
}
func TestValidateBadDynConfig(t *testing.T) {
badConfigs := []*externaldns.Config{
{},
{
// only username
DynUsername: "test",
},
{
// only customer name
DynCustomerName: "test",
},
{
// negative timeout
DynUsername: "test",
DynCustomerName: "test",
DynMinTTLSeconds: -1,
},
}
for _, cfg := range badConfigs {
addRequiredFieldsForDyn(cfg)
err := ValidateConfig(cfg)
assert.NotNil(t, err, "Configuration %+v should NOT have passed validation", cfg)
}
}
func TestValidateGoodDynConfig(t *testing.T) {
goodConfigs := []*externaldns.Config{
{
DynUsername: "test",
DynCustomerName: "test",
DynMinTTLSeconds: 600,
},
{
DynUsername: "test",
DynCustomerName: "test",
DynMinTTLSeconds: 0,
},
}
for _, cfg := range goodConfigs {
addRequiredFieldsForDyn(cfg)
err := ValidateConfig(cfg)
assert.Nil(t, err, "Configuration should be valid, got this error instead", err)
}
}

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
}

View File

@ -64,7 +64,7 @@ func (s PerResource) ResolveUpdate(current *endpoint.Endpoint, candidates []*end
// less returns true if endpoint x is less than y
func (s PerResource) less(x, y *endpoint.Endpoint) bool {
return x.Target < y.Target
return x.Targets.IsLess(y.Targets)
}
// TODO: with cross-resource/cross-cluster setup alternative variations of ConflictResolver can be used

View File

@ -45,7 +45,7 @@ func (suite *ResolverSuite) SetupTest() {
// initialize endpoints used in tests
suite.fooV1Cname = &endpoint.Endpoint{
DNSName: "foo",
Target: "v1",
Targets: endpoint.Targets{"v1"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
@ -53,7 +53,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.fooV2Cname = &endpoint.Endpoint{
DNSName: "foo",
Target: "v2",
Targets: endpoint.Targets{"v2"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
@ -61,7 +61,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.fooV2CnameDuplicate = &endpoint.Endpoint{
DNSName: "foo",
Target: "v2",
Targets: endpoint.Targets{"v2"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v2-duplicate",
@ -69,7 +69,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.fooA5 = &endpoint.Endpoint{
DNSName: "foo",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-5",
@ -77,7 +77,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.bar127A = &endpoint.Endpoint{
DNSName: "bar",
Target: "127.0.0.1",
Targets: endpoint.Targets{"127.0.0.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-127",
@ -85,7 +85,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.bar127AAnother = &endpoint.Endpoint{ //TODO: remove this once we move to multiple targets under same endpoint
DNSName: "bar",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-127",
@ -93,7 +93,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.bar192A = &endpoint.Endpoint{
DNSName: "bar",
Target: "192.168.0.1",
Targets: endpoint.Targets{"192.168.0.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-192",
@ -101,7 +101,7 @@ func (suite *ResolverSuite) SetupTest() {
}
suite.legacyBar192A = &endpoint.Endpoint{
DNSName: "bar",
Target: "192.168.0.1",
Targets: endpoint.Targets{"192.168.0.1"},
RecordType: "A",
}
}
@ -120,7 +120,7 @@ func (suite *ResolverSuite) TestStrictResolver() {
// should actually get the updated record (note ttl is different)
newFooV1Cname := &endpoint.Endpoint{
DNSName: suite.fooV1Cname.DNSName,
Target: suite.fooV1Cname.Target,
Targets: suite.fooV1Cname.Targets,
Labels: suite.fooV1Cname.Labels,
RecordType: suite.fooV1Cname.RecordType,
RecordTTL: suite.fooV1Cname.RecordTTL + 1, // ttl is different

View File

@ -166,7 +166,7 @@ func inheritOwner(from, to *endpoint.Endpoint) {
}
func targetChanged(desired, current *endpoint.Endpoint) bool {
return desired.Target != current.Target
return !desired.Targets.Same(current.Targets)
}
func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {

View File

@ -39,7 +39,7 @@ type PlanTestSuite struct {
func (suite *PlanTestSuite) SetupTest() {
suite.fooV1Cname = &endpoint.Endpoint{
DNSName: "foo",
Target: "v1",
Targets: endpoint.Targets{"v1"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
@ -49,7 +49,7 @@ func (suite *PlanTestSuite) SetupTest() {
// same resource as fooV1Cname, but target is different. It will never be picked because its target lexicographically bigger than "v1"
suite.fooV3CnameSameResource = &endpoint.Endpoint{ // TODO: remove this once endpoint can support multiple targets
DNSName: "foo",
Target: "v3",
Targets: endpoint.Targets{"v3"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
@ -58,7 +58,7 @@ func (suite *PlanTestSuite) SetupTest() {
}
suite.fooV2Cname = &endpoint.Endpoint{
DNSName: "foo",
Target: "v2",
Targets: endpoint.Targets{"v2"},
RecordType: "CNAME",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
@ -66,12 +66,12 @@ func (suite *PlanTestSuite) SetupTest() {
}
suite.fooV2CnameNoLabel = &endpoint.Endpoint{
DNSName: "foo",
Target: "v2",
Targets: endpoint.Targets{"v2"},
RecordType: "CNAME",
}
suite.fooA5 = &endpoint.Endpoint{
DNSName: "foo",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/foo-5",
@ -79,7 +79,7 @@ func (suite *PlanTestSuite) SetupTest() {
}
suite.bar127A = &endpoint.Endpoint{
DNSName: "bar",
Target: "127.0.0.1",
Targets: endpoint.Targets{"127.0.0.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-127",
@ -87,7 +87,7 @@ func (suite *PlanTestSuite) SetupTest() {
}
suite.bar127AWithTTL = &endpoint.Endpoint{
DNSName: "bar",
Target: "127.0.0.1",
Targets: endpoint.Targets{"127.0.0.1"},
RecordType: "A",
RecordTTL: 300,
Labels: map[string]string{
@ -96,7 +96,7 @@ func (suite *PlanTestSuite) SetupTest() {
}
suite.bar192A = &endpoint.Endpoint{
DNSName: "bar",
Target: "192.168.0.1",
Targets: endpoint.Targets{"192.168.0.1"},
RecordType: "A",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/bar-192",
@ -196,7 +196,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname}
expectedUpdateNew := []*endpoint.Endpoint{{
DNSName: suite.fooV2Cname.DNSName,
Target: suite.fooV2Cname.Target,
Targets: suite.fooV2Cname.Targets,
RecordType: suite.fooV2Cname.RecordType,
RecordTTL: suite.fooV2Cname.RecordTTL,
Labels: map[string]string{

View File

@ -28,12 +28,12 @@ func TestApply(t *testing.T) {
// empty list of records
empty := []*endpoint.Endpoint{}
// a simple entry
fooV1 := []*endpoint.Endpoint{{DNSName: "foo", Target: "v1"}}
fooV1 := []*endpoint.Endpoint{{DNSName: "foo", Targets: endpoint.Targets{"v1"}}}
// the same entry but with different target
fooV2 := []*endpoint.Endpoint{{DNSName: "foo", Target: "v2"}}
fooV2 := []*endpoint.Endpoint{{DNSName: "foo", Targets: endpoint.Targets{"v2"}}}
// another two simple entries
bar := []*endpoint.Endpoint{{DNSName: "bar", Target: "v1"}}
baz := []*endpoint.Endpoint{{DNSName: "baz", Target: "v1"}}
bar := []*endpoint.Endpoint{{DNSName: "bar", Targets: endpoint.Targets{"v1"}}}
baz := []*endpoint.Endpoint{{DNSName: "baz", Targets: endpoint.Targets{"v1"}}}
for _, tc := range []struct {
policy Policy

View File

@ -21,6 +21,7 @@ import (
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/kubernetes-incubator/external-dns/endpoint"
@ -30,30 +31,47 @@ import (
)
const (
elbHostnameSuffix = ".elb.amazonaws.com"
evaluateTargetHealth = true
recordTTL = 300
maxChangeCount = 4000
)
var (
// see: https://docs.aws.amazon.com/general/latest/gr/rande.html
// see: https://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region
canonicalHostedZones = map[string]string{
"us-east-1" + elbHostnameSuffix: "Z35SXDOTRQ7X7K",
"us-east-2" + elbHostnameSuffix: "Z3AADJGX6KTTL2",
"us-west-1" + elbHostnameSuffix: "Z368ELLRRE2KJ0",
"us-west-2" + elbHostnameSuffix: "Z1H1FL5HABSF5",
"ca-central-1" + elbHostnameSuffix: "ZQSVJUPU6J1EY",
"ap-south-1" + elbHostnameSuffix: "ZP97RAFLXTNZK",
"ap-northeast-2" + elbHostnameSuffix: "ZWKZPGTI48KDX",
"ap-southeast-1" + elbHostnameSuffix: "Z1LMS91P8CMLE5",
"ap-southeast-2" + elbHostnameSuffix: "Z1GM3OXH4ZPM65",
"ap-northeast-1" + elbHostnameSuffix: "Z14GRHDCWA56QT",
"eu-central-1" + elbHostnameSuffix: "Z215JYRZR1TBD5",
"eu-west-1" + elbHostnameSuffix: "Z32O12XQLNTSW2",
"eu-west-2" + elbHostnameSuffix: "ZHURV8PSTC4K8",
"eu-west-3" + elbHostnameSuffix: "Z3Q77PNBQS71R4",
"sa-east-1" + elbHostnameSuffix: "Z2P70J7HTTTPLU",
// Application Load Balancers and Classic Load Balancers
"us-east-2.elb.amazonaws.com": "Z3AADJGX6KTTL2",
"us-east-1.elb.amazonaws.com": "Z35SXDOTRQ7X7K",
"us-west-1.elb.amazonaws.com": "Z368ELLRRE2KJ0",
"us-west-2.elb.amazonaws.com": "Z1H1FL5HABSF5",
"ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY",
"ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK",
"ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX",
"ap-northeast-3.elb.amazonaws.com": "Z5LXEXXYW11ES",
"ap-southeast-1.elb.amazonaws.com": "Z1LMS91P8CMLE5",
"ap-southeast-2.elb.amazonaws.com": "Z1GM3OXH4ZPM65",
"ap-northeast-1.elb.amazonaws.com": "Z14GRHDCWA56QT",
"eu-central-1.elb.amazonaws.com": "Z215JYRZR1TBD5",
"eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2",
"eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8",
"eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4",
"sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU",
// Network Load Balancers
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
"elb.us-east-1.amazonaws.com": "Z26RNL4JYFTOTI",
"elb.us-west-1.amazonaws.com": "Z24FKFUX50B4VW",
"elb.us-west-2.amazonaws.com": "Z18D5FSROUN65G",
"elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT",
"elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA",
"elb.ap-northeast-2.amazonaws.com": "ZIBE1TIR4HY56",
"elb.ap-southeast-1.amazonaws.com": "ZKVM4W9LS7TM",
"elb.ap-southeast-2.amazonaws.com": "ZCT6FZBF4DROD",
"elb.ap-northeast-1.amazonaws.com": "Z31USIVHYNEOWT",
"elb.eu-central-1.amazonaws.com": "Z3F0SRJ5LGBH90",
"elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F",
"elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G",
"elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5",
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
}
)
@ -79,10 +97,10 @@ type AWSProvider struct {
}
// NewAWSProvider initializes a new AWS Route53 based Provider.
func NewAWSProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool) (*AWSProvider, error) {
func NewAWSProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, assumeRole string, dryRun bool) (*AWSProvider, error) {
config := aws.NewConfig()
config = config.WithHTTPClient(
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
@ -99,6 +117,11 @@ func NewAWSProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTy
return nil, err
}
if assumeRole != "" {
log.Infof("Assuming role: %s", assumeRole)
session.Config.WithCredentials(stscreds.NewCredentials(session, assumeRole))
}
provider := &AWSProvider{
client: route53.New(session),
domainFilter: domainFilter,
@ -176,12 +199,17 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
ttl = endpoint.TTL(*r.TTL)
}
for _, rr := range r.ResourceRecords {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(rr.Value), aws.StringValue(r.Type), ttl))
if len(r.ResourceRecords) > 0 {
targets := make([]string, len(r.ResourceRecords))
for idx, rr := range r.ResourceRecords {
targets[idx] = aws.StringValue(rr.Value)
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...))
}
if r.AliasTarget != nil {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.AliasTarget.DNSName), endpoint.RecordTypeCNAME, ttl))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName)))
}
}
@ -378,8 +406,8 @@ func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
if isAWSLoadBalancer(endpoint) {
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
DNSName: aws.String(endpoint.Target),
HostedZoneId: aws.String(canonicalHostedZone(endpoint.Target)),
DNSName: aws.String(endpoint.Targets[0]),
HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])),
EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
}
} else {
@ -389,10 +417,11 @@ func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
} else {
change.ResourceRecordSet.TTL = aws.Int64(int64(endpoint.RecordTTL))
}
change.ResourceRecordSet.ResourceRecords = []*route53.ResourceRecord{
{
Value: aws.String(endpoint.Target),
},
change.ResourceRecordSet.ResourceRecords = make([]*route53.ResourceRecord, len(endpoint.Targets))
for idx, val := range endpoint.Targets {
change.ResourceRecordSet.ResourceRecords[idx] = &route53.ResourceRecord{
Value: aws.String(val),
}
}
}
@ -429,7 +458,7 @@ func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*rou
// isAWSLoadBalancer determines if a given hostname belongs to an AWS load balancer.
func isAWSLoadBalancer(ep *endpoint.Endpoint) bool {
if ep.RecordType == endpoint.RecordTypeCNAME {
return canonicalHostedZone(ep.Target) != ""
return canonicalHostedZone(ep.Targets[0]) != ""
}
return false

View File

@ -205,22 +205,24 @@ func TestAWSZones(t *testing.T) {
func TestAWSRecords(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
records, err := provider.Records()
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
}
@ -229,10 +231,11 @@ func TestAWSCreateRecords(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", "172.17.0.1", endpoint.RecordTypeA, customTTL),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
require.NoError(t, provider.CreateRecords(records))
@ -241,29 +244,33 @@ func TestAWSCreateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", "172.17.0.1", endpoint.RecordTypeA, customTTL),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
}
func TestAWSUpdateRecords(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
require.NoError(t, provider.UpdateRecords(updatedRecords, currentRecords))
@ -272,19 +279,21 @@ func TestAWSUpdateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
}
func TestAWSDeleteRecords(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
}
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, originalEndpoints)
@ -300,41 +309,47 @@ func TestAWSDeleteRecords(t *testing.T) {
func TestAWSApplyChanges(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
changes := &plan.Changes{
@ -350,56 +365,64 @@ func TestAWSApplyChanges(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
}
func TestAWSApplyChangesDryRun(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
}
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), true, originalEndpoints)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
changes := &plan.Changes{
@ -524,7 +547,7 @@ func TestAWSsubmitChanges(t *testing.T) {
for j := 1; j < (hosts + 1); j++ {
hostname := fmt.Sprintf("subnet%dhost%d.zone-1.ext-dns-test-2.teapot.zalan.do", i, j)
ip := fmt.Sprintf("1.1.%d.%d", i, j)
ep := endpoint.NewEndpointWithTTL(hostname, ip, endpoint.RecordTypeA, endpoint.TTL(recordTTL))
ep := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, endpoint.TTL(recordTTL), ip)
endpoints = append(endpoints, ep)
}
}
@ -630,7 +653,7 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.example.org", RecordType: endpoint.RecordTypeCNAME},
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME},
}
require.NoError(t, provider.CreateRecords(records))
@ -655,7 +678,7 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) {
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.eu-central-1.elb.amazonaws.com", RecordType: endpoint.RecordTypeCNAME},
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME},
}
require.NoError(t, provider.CreateRecords(records))
@ -685,7 +708,7 @@ func TestAWSisLoadBalancer(t *testing.T) {
{"foo.example.org", endpoint.RecordTypeCNAME, false},
} {
ep := &endpoint.Endpoint{
Target: tc.target,
Targets: endpoint.Targets{tc.target},
RecordType: tc.recordType,
}
assert.Equal(t, tc.expected, isAWSLoadBalancer(ep))
@ -697,13 +720,15 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
hostname string
expected string
}{
{"foo.us-east-1.elb.amazonaws.com", "Z35SXDOTRQ7X7K"},
// Application Load Balancers and Classic Load Balancers
{"foo.us-east-2.elb.amazonaws.com", "Z3AADJGX6KTTL2"},
{"foo.us-east-1.elb.amazonaws.com", "Z35SXDOTRQ7X7K"},
{"foo.us-west-1.elb.amazonaws.com", "Z368ELLRRE2KJ0"},
{"foo.us-west-2.elb.amazonaws.com", "Z1H1FL5HABSF5"},
{"foo.ca-central-1.elb.amazonaws.com", "ZQSVJUPU6J1EY"},
{"foo.ap-south-1.elb.amazonaws.com", "ZP97RAFLXTNZK"},
{"foo.ap-northeast-2.elb.amazonaws.com", "ZWKZPGTI48KDX"},
{"foo.ap-northeast-3.elb.amazonaws.com", "Z5LXEXXYW11ES"},
{"foo.ap-southeast-1.elb.amazonaws.com", "Z1LMS91P8CMLE5"},
{"foo.ap-southeast-2.elb.amazonaws.com", "Z1GM3OXH4ZPM65"},
{"foo.ap-northeast-1.elb.amazonaws.com", "Z14GRHDCWA56QT"},
@ -712,6 +737,23 @@ func TestAWSCanonicalHostedZone(t *testing.T) {
{"foo.eu-west-2.elb.amazonaws.com", "ZHURV8PSTC4K8"},
{"foo.eu-west-3.elb.amazonaws.com", "Z3Q77PNBQS71R4"},
{"foo.sa-east-1.elb.amazonaws.com", "Z2P70J7HTTTPLU"},
// Network Load Balancers
{"foo.elb.us-east-2.amazonaws.com", "ZLMOA37VPKANP"},
{"foo.elb.us-east-1.amazonaws.com", "Z26RNL4JYFTOTI"},
{"foo.elb.us-west-1.amazonaws.com", "Z24FKFUX50B4VW"},
{"foo.elb.us-west-2.amazonaws.com", "Z18D5FSROUN65G"},
{"foo.elb.ca-central-1.amazonaws.com", "Z2EPGBW3API2WT"},
{"foo.elb.ap-south-1.amazonaws.com", "ZVDDRBQ08TROA"},
{"foo.elb.ap-northeast-2.amazonaws.com", "ZIBE1TIR4HY56"},
{"foo.elb.ap-southeast-1.amazonaws.com", "ZKVM4W9LS7TM"},
{"foo.elb.ap-southeast-2.amazonaws.com", "ZCT6FZBF4DROD"},
{"foo.elb.ap-northeast-1.amazonaws.com", "Z31USIVHYNEOWT"},
{"foo.elb.eu-central-1.amazonaws.com", "Z3F0SRJ5LGBH90"},
{"foo.elb.eu-west-1.amazonaws.com", "Z2IFOLAFXWLO4F"},
{"foo.elb.eu-west-2.amazonaws.com", "ZD4D7Y8KGAS4G"},
{"foo.elb.eu-west-3.amazonaws.com", "Z1CMS0P5QUZ6D5"},
{"foo.elb.sa-east-1.amazonaws.com", "ZTK26PT1VY4CU"},
// No Load Balancer
{"foo.example.org", ""},
} {
zone := canonicalHostedZone(tc.hostname)

View File

@ -112,9 +112,9 @@ func NewAzureProvider(configFile string, domainFilter DomainFilter, zoneIDFilter
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
zonesClient := dns.NewZonesClient(cfg.SubscriptionID)
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordsClient := dns.NewRecordSetsClient(cfg.SubscriptionID)
recordsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzureProvider{
@ -158,12 +158,12 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
ttl = endpoint.TTL(*recordSet.TTL)
}
ep := endpoint.NewEndpointWithTTL(name, target, recordType, endpoint.TTL(ttl))
ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), target)
log.Debugf(
"Found %s record for '%s' with target '%s'.",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
)
endpoints = append(endpoints, ep)
return true
@ -323,7 +323,7 @@ func (p *AzureProvider) updateRecords(updated azureChangeMap) {
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
name,
endpoint.Target,
endpoint.Targets,
zone,
)
continue
@ -333,7 +333,7 @@ func (p *AzureProvider) updateRecords(updated azureChangeMap) {
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
name,
endpoint.Target,
endpoint.Targets,
zone,
)
@ -354,7 +354,7 @@ func (p *AzureProvider) updateRecords(updated azureChangeMap) {
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
endpoint.RecordType,
name,
endpoint.Target,
endpoint.Targets,
zone,
err,
)
@ -388,7 +388,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
TTL: to.Int64Ptr(ttl),
ARecords: &[]dns.ARecord{
{
Ipv4Address: to.StringPtr(endpoint.Target),
Ipv4Address: to.StringPtr(endpoint.Targets[0]),
},
},
},
@ -398,7 +398,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
RecordSetProperties: &dns.RecordSetProperties{
TTL: to.Int64Ptr(ttl),
CnameRecord: &dns.CnameRecord{
Cname: to.StringPtr(endpoint.Target),
Cname: to.StringPtr(endpoint.Targets[0]),
},
},
}, nil
@ -409,7 +409,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
TxtRecords: &[]dns.TxtRecord{
{
Value: &[]string{
endpoint.Target,
endpoint.Targets[0],
},
},
},

View File

@ -129,8 +129,8 @@ func (client *mockRecordsClient) Delete(resourceGroupName string, zoneName strin
client.deletedEndpoints,
endpoint.NewEndpoint(
formatAzureDNSName(relativeRecordSetName, zoneName),
"",
string(recordType),
"",
),
)
return autorest.Response{}, nil
@ -145,9 +145,9 @@ func (client *mockRecordsClient) CreateOrUpdate(resourceGroupName string, zoneNa
client.updatedEndpoints,
endpoint.NewEndpointWithTTL(
formatAzureDNSName(relativeRecordSetName, zoneName),
extractAzureTarget(&parameters),
string(recordType),
ttl,
extractAzureTarget(&parameters),
),
)
return parameters, nil
@ -197,11 +197,11 @@ func TestAzureRecord(t *testing.T) {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", "123.123.123.122", endpoint.RecordTypeA),
endpoint.NewEndpoint("example.com", "heritage=external-dns,external-dns/owner=default", endpoint.RecordTypeTXT),
endpoint.NewEndpointWithTTL("nginx.example.com", "123.123.123.123", endpoint.RecordTypeA, 3600),
endpoint.NewEndpointWithTTL("nginx.example.com", "heritage=external-dns,external-dns/owner=default", endpoint.RecordTypeTXT, recordTTL),
endpoint.NewEndpointWithTTL("hack.example.com", "hack.azurewebsites.net", endpoint.RecordTypeCNAME, 10),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeA, 3600, "123.123.123.123"),
endpoint.NewEndpointWithTTL("nginx.example.com", endpoint.RecordTypeTXT, recordTTL, "heritage=external-dns,external-dns/owner=default"),
endpoint.NewEndpointWithTTL("hack.example.com", endpoint.RecordTypeCNAME, 10, "hack.azurewebsites.net"),
}
validateAzureEndpoints(t, actual, expected)
@ -214,23 +214,23 @@ func TestAzureApplyChanges(t *testing.T) {
testAzureApplyChangesInternal(t, false, &recordsClient)
validateAzureEndpoints(t, recordsClient.deletedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", "", endpoint.RecordTypeA),
endpoint.NewEndpoint("oldcname.example.com", "", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("deleted.example.com", "", endpoint.RecordTypeA),
endpoint.NewEndpoint("deletedcname.example.com", "", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
})
validateAzureEndpoints(t, recordsClient.updatedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("example.com", "tag", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("foo.example.com", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("foo.example.com", "tag", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("bar.example.com", "other.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("bar.example.com", "tag", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("other.com", "5.6.7.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("other.com", "tag", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL)),
endpoint.NewEndpointWithTTL("new.example.com", "111.222.111.222", endpoint.RecordTypeA, 3600),
endpoint.NewEndpointWithTTL("newcname.example.com", "other.com", endpoint.RecordTypeCNAME, 10),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("foo.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "other.com"),
endpoint.NewEndpointWithTTL("bar.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "5.6.7.8"),
endpoint.NewEndpointWithTTL("other.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
})
}
@ -262,33 +262,33 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClie
)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("foo.example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("foo.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("bar.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("bar.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("other.com", "5.6.7.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("other.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("nope.com", "4.4.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("nope.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", "121.212.121.212", endpoint.RecordTypeA),
endpoint.NewEndpoint("oldcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("old.nope.com", "121.212.121.212", endpoint.RecordTypeA),
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("new.example.com", "111.222.111.222", endpoint.RecordTypeA, 3600),
endpoint.NewEndpointWithTTL("newcname.example.com", "other.com", endpoint.RecordTypeCNAME, 10),
endpoint.NewEndpoint("new.nope.com", "222.111.222.111", endpoint.RecordTypeA),
endpoint.NewEndpointWithTTL("new.example.com", endpoint.RecordTypeA, 3600, "111.222.111.222"),
endpoint.NewEndpointWithTTL("newcname.example.com", endpoint.RecordTypeCNAME, 10, "other.com"),
endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.example.com", "111.222.111.222", endpoint.RecordTypeA),
endpoint.NewEndpoint("deletedcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("deleted.nope.com", "222.111.222.111", endpoint.RecordTypeA),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}
changes := &plan.Changes{

View File

@ -161,7 +161,7 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) {
for _, r := range records {
if supportedRecordType(r.Type) {
endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, r.Content, r.Type))
endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, r.Type, r.Content))
}
}
}
@ -290,7 +290,7 @@ func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxied boo
TTL: 1,
Proxied: proxied,
Type: endpoint.RecordType,
Content: endpoint.Target,
Content: endpoint.Targets[0],
},
}
}

View File

@ -21,11 +21,11 @@ import (
"os"
"testing"
"github.com/cloudflare/cloudflare-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -336,12 +336,12 @@ func (m *mockCloudFlareUpdateRecordsFail) ListZones(zoneID ...string) ([]cloudfl
}
func TestNewCloudFlareChanges(t *testing.T) {
endpoints := []*endpoint.Endpoint{{DNSName: "new", Target: "target"}}
endpoints := []*endpoint.Endpoint{{DNSName: "new", Targets: endpoint.Targets{"target"}}}
newCloudFlareChanges(cloudFlareCreate, endpoints, true)
}
func TestNewCloudFlareChangeNoProxied(t *testing.T) {
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Target: "target"}, false)
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}}, false)
assert.False(t, change.ResourceRecordSet.Proxied)
}
@ -361,7 +361,7 @@ func TestNewCloudFlareChangeProxiable(t *testing.T) {
}
for _, cloudFlareType := range cloudFlareTypes {
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: cloudFlareType.recordType, Target: "target"}, true)
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: cloudFlareType.recordType, Targets: endpoint.Targets{"target"}}, true)
if cloudFlareType.proxiable {
assert.True(t, change.ResourceRecordSet.Proxied)
@ -370,7 +370,7 @@ func TestNewCloudFlareChangeProxiable(t *testing.T) {
}
}
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "*.foo", RecordType: "A", Target: "target"}, true)
change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "*.foo", RecordType: "A", Targets: endpoint.Targets{"target"}}, true)
assert.False(t, change.ResourceRecordSet.Proxied)
}
@ -433,10 +433,10 @@ func TestApplyChanges(t *testing.T) {
provider := &CloudFlareProvider{
Client: &mockCloudFlareClient{},
}
changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.zalando.to.", Target: "target"}, {DNSName: "new.ext-dns-test.unrelated.to.", Target: "target"}}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target"}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target-old"}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target-new"}}
changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target"}}, {DNSName: "new.ext-dns-test.unrelated.to.", Targets: endpoint.Targets{"target"}}}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target"}}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-old"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-new"}}}
err := provider.ApplyChanges(changes)
if err != nil {
t.Errorf("should not fail, %s", err)

View File

@ -274,8 +274,8 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
if service.Host != "" {
ep := endpoint.NewEndpoint(
dnsName,
service.Host,
guessRecordType(service.Host),
service.Host,
)
ep.Labels["originalText"] = service.Text
ep.Labels["prefix"] = prefix
@ -284,8 +284,8 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
if service.Text != "" {
ep := endpoint.NewEndpoint(
dnsName,
service.Text,
endpoint.RecordTypeTXT,
service.Text,
)
ep.Labels["prefix"] = prefix
result = append(result, ep)
@ -318,7 +318,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
prefix = fmt.Sprintf("%08x", rand.Int31())
}
service := Service{
Host: ep.Target,
Host: ep.Targets[0],
Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
@ -340,7 +340,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
TargetStrip: strings.Count(prefix, ".") + 1,
})
}
services[index].Text = ep.Target
services[index].Text = ep.Targets[0]
index++
}

View File

@ -70,8 +70,8 @@ func TestAServiceTranslation(t *testing.T) {
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Target != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Target, expectedTarget)
if endpoints[0].Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
@ -99,8 +99,8 @@ func TestCNAMEServiceTranslation(t *testing.T) {
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Target != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Target, expectedTarget)
if endpoints[0].Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
@ -128,8 +128,8 @@ func TestTXTServiceTranslation(t *testing.T) {
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Target != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Target, expectedTarget)
if endpoints[0].Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
@ -169,8 +169,8 @@ func TestAWithTXTServiceTranslation(t *testing.T) {
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
}
if ep.Target != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Target, expectedTarget)
if ep.Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
}
}
}
@ -208,8 +208,8 @@ func TestCNAMEWithTXTServiceTranslation(t *testing.T) {
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
}
if ep.Target != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Target, expectedTarget)
if ep.Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
}
}
}
@ -222,9 +222,9 @@ func TestCoreDNSApplyChanges(t *testing.T) {
changes1 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain1.local", "5.5.5.5", endpoint.RecordTypeA),
endpoint.NewEndpoint("domain1.local", "string1", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("domain2.local", "site.local", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "5.5.5.5"),
endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeTXT, "string1"),
endpoint.NewEndpoint("domain2.local", endpoint.RecordTypeCNAME, "site.local"),
},
}
coredns.ApplyChanges(changes1)
@ -235,14 +235,14 @@ func TestCoreDNSApplyChanges(t *testing.T) {
}
validateServices(client.services, expectedServices1, t, 1)
updatedEp := endpoint.NewEndpoint("domain1.local", "6.6.6.6", endpoint.RecordTypeA)
updatedEp := endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "6.6.6.6")
updatedEp.Labels["originalText"] = "string1"
changes2 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain3.local", "7.7.7.7", endpoint.RecordTypeA),
endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "7.7.7.7"),
},
UpdateNew: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain1.local", "6.6.6.6", "A"),
endpoint.NewEndpoint("domain1.local", "A", "6.6.6.6"),
},
}
applyServiceChanges(coredns, changes2)
@ -256,9 +256,9 @@ func TestCoreDNSApplyChanges(t *testing.T) {
changes3 := &plan.Changes{
Delete: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain1.local", "6.6.6.6", endpoint.RecordTypeA),
endpoint.NewEndpoint("domain1.local", "string", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("domain3.local", "7.7.7.7", endpoint.RecordTypeA),
endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "6.6.6.6"),
endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeTXT, "string"),
endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "7.7.7.7"),
},
}

454
provider/designate.go Normal file
View File

@ -0,0 +1,454 @@
/*
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 names to FQDN
func canonicalizeDomainNames(domains []string) []string {
var cDomains []string
for _, d := range domains {
if !strings.HasSuffix(d, ".") {
d += "."
cDomains = append(cDomains, strings.ToLower(d))
}
}
return cDomains
}
// converts domain name to FQDN
func canonicalizeDomainName(d string) string {
if !strings.HasSuffix(d, ".") {
d += "."
}
return strings.ToLower(d)
}
// 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
}
}
targets := ep.Targets
if ep.RecordType == endpoint.RecordTypeCNAME {
targets = canonicalizeDomainNames(targets)
}
for _, t := range targets {
rs.names[t] = !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: endpoint.Targets{"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: endpoint.Targets{"text1"},
Labels: map[string]string{
designateRecordSetID: rs12ID,
designateZoneID: zone1ID,
designateOriginalRecords: "text1",
},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"10.1.1.1"},
Labels: map[string]string{},
},
{
DNSName: "www.example.com",
RecordType: endpoint.RecordTypeTXT,
Targets: endpoint.Targets{"text1"},
Labels: map[string]string{},
},
{
DNSName: "ftp.example.com",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"10.1.1.2"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"10.2.1.1"},
Labels: map[string]string{},
},
{
DNSName: "srv.test.net",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"10.2.1.2"},
Labels: map[string]string{},
},
{
DNSName: "db.test.net",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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: endpoint.Targets{"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

@ -19,6 +19,7 @@ package provider
import (
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
@ -105,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.Data, r.Type))
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))
}
}
}
@ -196,6 +205,15 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
if p.DryRun {
continue
}
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,
@ -258,7 +276,7 @@ func newDigitalOceanChange(action string, endpoint *endpoint.Endpoint) *DigitalO
ResourceRecordSet: godo.DomainRecord{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Data: endpoint.Target,
Data: endpoint.Targets[0],
},
}
return change

View File

@ -41,7 +41,7 @@ type mockDigitalOceanClient struct{}
func (m *mockDigitalOceanClient) List(ctx context.Context, opt *godo.ListOptions) ([]godo.Domain, *godo.Response, error) {
if opt == nil || opt.Page == 0 {
return []godo.Domain{{Name: "foo.com"}}, &godo.Response{
return []godo.Domain{{Name: "foo.com"}, {Name: "example.com"}}, &godo.Response{
Links: &godo.Links{
Pages: &godo.Pages{
Next: "http://example.com/v2/domains/?page=2",
@ -58,7 +58,7 @@ func (m *mockDigitalOceanClient) Create(context.Context, *godo.DomainCreateReque
}
func (m *mockDigitalOceanClient) CreateRecord(context.Context, string, *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) {
return &godo.DomainRecord{ID: 1}, nil, nil
return &godo.DomainRecord{ID: 1, Name: "new", Type: "CNAME"}, nil, nil
}
func (m *mockDigitalOceanClient) Delete(context.Context, string) (*godo.Response, error) {
@ -80,9 +80,26 @@ func (m *mockDigitalOceanClient) Record(ctx context.Context, domain string, id i
}
func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) {
if domain == "foo.com" {
switch domain {
case "foo.com":
if opt == nil || opt.Page == 0 {
return []godo.DomainRecord{{ID: 1, Name: "foo.ext-dns-test.foo.com.", Type: "CNAME"}, {ID: 2, Name: "bar.ext-dns-test.foo.com.", 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",
Last: "1234",
},
},
}, nil
}
return []godo.DomainRecord{{ID: 3, Name: "baz.ext-dns-test", Type: "A"}}, nil, nil
case "example.com":
if opt == nil || opt.Page == 0 {
return []godo.DomainRecord{{ID: 1, Name: "new", Type: "CNAME"}}, &godo.Response{
Links: &godo.Links{
Pages: &godo.Pages{
Next: "http://example.com/v2/domains/?page=2",
@ -91,9 +108,10 @@ func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt
},
}, nil
}
return []godo.DomainRecord{{ID: 3, Name: "baz.ext-dns-test.foo.com.", Type: "A"}}, nil, nil
return nil, nil, nil
default:
return nil, nil, nil
}
return nil, nil, nil
}
type mockDigitalOceanListFail struct{}
@ -386,7 +404,7 @@ func (m *mockDigitalOceanCreateRecordsFail) Records(ctx context.Context, domain
func TestNewDigitalOceanChanges(t *testing.T) {
action := DigitalOceanCreate
endpoints := []*endpoint.Endpoint{{DNSName: "new", Target: "target"}}
endpoints := []*endpoint.Endpoint{{DNSName: "new", Targets: endpoint.Targets{"target"}}}
_ = newDigitalOceanChanges(action, endpoints)
}
@ -402,38 +420,23 @@ func TestDigitalOceanZones(t *testing.T) {
}
validateDigitalOceanZones(t, zones, []godo.Domain{
{Name: "foo.com"}, {Name: "bar.com"},
{Name: "foo.com"}, {Name: "example.com"}, {Name: "bar.com"},
})
}
func TestDigitalOceanRecords(t *testing.T) {
provider := &DigitalOceanProvider{
Client: &mockDigitalOceanClient{},
}
records, err := provider.Records()
if err != nil {
t.Errorf("should not fail, %s", err)
}
assert.Equal(t, 3, len(records))
provider.Client = &mockDigitalOceanRecordsFail{}
_, err = provider.Records()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func TestDigitalOceanApplyChanges(t *testing.T) {
changes := &plan.Changes{}
provider := &DigitalOceanProvider{
Client: &mockDigitalOceanClient{},
}
changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.bar.com", Target: "target"}, {DNSName: "new.ext-dns-test.unexpected.com", Target: "target"}}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Target: "target"}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Target: "target-old"}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Target: "target-new"}}
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"}}}
err := provider.ApplyChanges(changes)
if err != nil {
t.Errorf("should not fail, %s", err)
@ -485,3 +488,37 @@ func validateDigitalOceanZones(t *testing.T, zones []godo.Domain, expected []god
assert.Equal(t, expected[i].Name, zone.Name)
}
}
func TestDigitalOceanRecord(t *testing.T) {
provider := &DigitalOceanProvider{
Client: &mockDigitalOceanClient{},
}
records, err := provider.fetchRecords("example.com")
if err != nil {
t.Fatal(err)
}
expected := []godo.DomainRecord{{ID: 1, Name: "new", Type: "CNAME"}}
require.Len(t, records, len(expected))
for i, record := range records {
assert.Equal(t, expected[i].Name, record.Name)
}
}
func TestDigitalOceanAllRecords(t *testing.T) {
provider := &DigitalOceanProvider{
Client: &mockDigitalOceanClient{},
}
records, err := provider.Records()
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, 5, len(records))
provider.Client = &mockDigitalOceanRecordsFail{}
_, err = provider.Records()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}

View File

@ -28,6 +28,8 @@ import (
log "github.com/sirupsen/logrus"
)
const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default)
type identityService struct {
service *dnsimple.IdentityService
}
@ -124,20 +126,30 @@ func NewDnsimpleProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, d
// Returns a list of filtered Zones
func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) {
zones := make(map[string]dnsimple.Zone)
zonesResponse, err := p.client.ListZones(p.accountID, &dnsimple.ZoneListOptions{})
if err != nil {
return nil, err
}
for _, zone := range zonesResponse.Data {
if !p.domainFilter.Match(zone.Name) {
continue
page := 1
listOptions := &dnsimple.ZoneListOptions{}
for {
listOptions.Page = page
zonesResponse, err := p.client.ListZones(p.accountID, listOptions)
if err != nil {
return nil, err
}
for _, zone := range zonesResponse.Data {
if !p.domainFilter.Match(zone.Name) {
continue
}
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
continue
}
zones[strconv.Itoa(zone.ID)] = zone
}
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
continue
page++
if page > zonesResponse.Pagination.TotalPages {
break
}
zones[strconv.Itoa(zone.ID)] = zone
}
return zones, nil
}
@ -149,18 +161,27 @@ func (p *dnsimpleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
return nil, err
}
for _, zone := range zones {
records, err := p.client.ListRecords(p.accountID, zone.Name, &dnsimple.ZoneRecordListOptions{})
if err != nil {
return nil, err
}
for _, record := range records.Data {
switch record.Type {
case "A", "CNAME", "TXT":
break
default:
continue
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{}
for {
listOptions.Page = page
records, err := p.client.ListRecords(p.accountID, zone.Name, listOptions)
if err != nil {
return nil, err
}
for _, record := range records.Data {
switch record.Type {
case "A", "CNAME", "TXT":
break
default:
continue
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(record.Name+"."+record.ZoneID, record.Type, endpoint.TTL(record.TTL), record.Content))
}
page++
if page > records.Pagination.TotalPages {
break
}
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name+"."+record.ZoneID, record.Content, record.Type))
}
}
return endpoints, nil
@ -168,12 +189,18 @@ func (p *dnsimpleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
// newDnsimpleChange initializes a new change to dns records
func newDnsimpleChange(action string, e *endpoint.Endpoint) *dnsimpleChange {
ttl := dnsimpleRecordTTL
if e.RecordTTL.IsConfigured() {
ttl = int(e.RecordTTL)
}
change := &dnsimpleChange{
Action: action,
ResourceRecordSet: dnsimple.ZoneRecord{
Name: e.DNSName,
Type: e.RecordType,
Content: e.Target,
Content: e.Targets[0],
TTL: ttl,
},
}
return change
@ -239,13 +266,24 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error {
// Returns the record ID for a given record name and zone
func (p *dnsimpleProvider) GetRecordID(zone string, recordName string) (recordID int, err error) {
records, err := p.client.ListRecords(p.accountID, zone, &dnsimple.ZoneRecordListOptions{})
if err != nil {
return 0, err
}
for _, record := range records.Data {
if record.Name == recordName {
return record.ID, nil
page := 1
listOptions := &dnsimple.ZoneRecordListOptions{Name: recordName}
for {
listOptions.Page = page
records, err := p.client.ListRecords(p.accountID, zone, listOptions)
if err != nil {
return 0, err
}
for _, record := range records.Data {
if record.Name == recordName {
return record.ID, nil
}
}
page++
if page > records.Pagination.TotalPages {
break
}
}
return 0, fmt.Errorf("No record id found")

View File

@ -51,7 +51,7 @@ func TestDnsimpleServices(t *testing.T) {
}
zones := []dnsimple.Zone{firstZone, secondZone}
dnsimpleListZonesResponse = dnsimple.ZonesResponse{
Response: dnsimple.Response{},
Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}},
Data: zones,
}
firstRecord := dnsimple.ZoneRecord{
@ -74,28 +74,48 @@ func TestDnsimpleServices(t *testing.T) {
Priority: 0,
Type: "A",
}
records := []dnsimple.ZoneRecord{firstRecord, secondRecord}
thirdRecord := dnsimple.ZoneRecord{
ID: 3,
ZoneID: "example.com",
ParentID: 0,
Name: "custom-ttl",
Content: "target",
TTL: 60,
Priority: 0,
Type: "CNAME",
}
records := []dnsimple.ZoneRecord{firstRecord, secondRecord, thirdRecord}
dnsimpleListRecordsResponse = dnsimple.ZoneRecordsResponse{
Response: dnsimple.Response{},
Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}},
Data: records,
}
// Setup mock services
mockDNS := &mockDnsimpleZoneServiceInterface{}
mockDNS.On("ListZones", "1", &dnsimple.ZoneListOptions{}).Return(&dnsimpleListZonesResponse, nil)
mockDNS.On("ListZones", "2", &dnsimple.ZoneListOptions{}).Return(nil, fmt.Errorf("Account ID not found"))
mockDNS.On("ListRecords", "1", "example.com", &dnsimple.ZoneRecordListOptions{}).Return(&dnsimpleListRecordsResponse, nil)
mockDNS.On("ListRecords", "1", "example-beta.com", &dnsimple.ZoneRecordListOptions{}).Return(&dnsimple.ZoneRecordsResponse{}, nil)
mockDNS.On("ListZones", "1", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListZonesResponse, nil)
mockDNS.On("ListZones", "2", &dnsimple.ZoneListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(nil, fmt.Errorf("Account ID not found"))
mockDNS.On("ListRecords", "1", "example.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleListRecordsResponse, nil)
mockDNS.On("ListRecords", "1", "example-beta.com", &dnsimple.ZoneRecordListOptions{ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimple.ZoneRecordsResponse{Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}}, nil)
for _, record := range records {
simpleRecord := dnsimple.ZoneRecord{
Name: record.Name,
Type: record.Type,
Content: record.Content,
TTL: record.TTL,
}
dnsimpleRecordResponse := dnsimple.ZoneRecordsResponse{
Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}},
Data: []dnsimple.ZoneRecord{record},
}
mockDNS.On("ListRecords", "1", record.ZoneID, &dnsimple.ZoneRecordListOptions{Name: record.Name, ListOptions: dnsimple.ListOptions{Page: 1}}).Return(&dnsimpleRecordResponse, nil)
mockDNS.On("CreateRecord", "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil)
mockDNS.On("DeleteRecord", "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil)
mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil)
mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil)
}
mockProvider = dnsimpleProvider{client: mockDNS}
@ -131,9 +151,12 @@ func testDnsimpleProviderRecords(t *testing.T) {
}
func testDnsimpleProviderApplyChanges(t *testing.T) {
changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{{DNSName: "example.example.com", Target: "target", RecordType: endpoint.RecordTypeCNAME}}
changes.Delete = []*endpoint.Endpoint{{DNSName: "example-beta.example.com", Target: "127.0.0.1", RecordType: endpoint.RecordTypeA}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "example.example.com", Target: "target", RecordType: endpoint.RecordTypeCNAME}}
changes.Create = []*endpoint.Endpoint{
{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME},
{DNSName: "custom-ttl.example.com", RecordTTL: 60, Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME},
}
changes.Delete = []*endpoint.Endpoint{{DNSName: "example-beta.example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}}
mockProvider.accountID = "1"
err := mockProvider.ApplyChanges(changes)

View File

@ -19,6 +19,7 @@ package provider
import (
"fmt"
"os"
"strconv"
"strings"
"time"
@ -37,6 +38,12 @@ const (
// may be made configurable in the future but 20K records seems like enough for a few zones
cacheMaxSize = 20000
// two consecutive bad logins happen at least this many seconds appart
// While it is easy to get the username right, misconfiguring the password
// can get account blocked. Exit(1) is not a good solution
// as k8s will restart the pod and another login attempt will be made
badLoginMinIntervalSeconds = 30 * 60
// this prefix must be stripped from resource links before feeding them to dynect.Client.Do()
restAPIPrefix = "/REST/"
)
@ -60,17 +67,21 @@ func (c *cache) Put(link string, ep *endpoint.Endpoint) {
c.contents[link] = &entry{
ep: ep,
expires: int64(time.Now().Unix()) + int64(ep.RecordTTL),
expires: unixNow() + int64(ep.RecordTTL),
}
}
func unixNow() int64 {
return int64(time.Now().Unix())
}
func (c *cache) Get(link string) *endpoint.Endpoint {
result, ok := c.contents[link]
if !ok {
return nil
}
now := int64(time.Now().Unix())
now := unixNow()
if result.expires < now {
delete(c.contents, link)
@ -82,20 +93,22 @@ func (c *cache) Get(link string) *endpoint.Endpoint {
// DynConfig hold connection parameters to dyn.com and interanl state
type DynConfig struct {
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
DryRun bool
CustomerName string
Username string
Password string
AppVersion string
DynVersion string
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
DryRun bool
CustomerName string
Username string
Password string
MinTTLSeconds int
AppVersion string
DynVersion string
}
// DynProvider is the actual interface impl.
type dynProviderState struct {
DynConfig
Cache *cache
Cache *cache
LastLoginErrorTime int64
}
// ZoneChange is missing from dynect: https://help.dyn.com/get-zone-changeset-api/
@ -166,13 +179,17 @@ func filterAndFixLinks(links []string, filter DomainFilter) []string {
return result
}
func fixMissingTTL(ttl endpoint.TTL) string {
func fixMissingTTL(ttl endpoint.TTL, minTTLSeconds int) string {
i := dynDefaultTTL
if ttl.IsConfigured() {
i = int(ttl)
if int(ttl) < minTTLSeconds {
i = minTTLSeconds
} else {
i = int(ttl)
}
}
return fmt.Sprintf("%d", i)
return strconv.Itoa(i)
}
// merge produces a singe list of records that can be used as a replacement.
@ -197,7 +214,7 @@ func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
continue
}
if matchingNew.Target != old.Target {
if !matchingNew.Targets.Same(old.Targets) {
// new target: always update, TTL will be overwritten too if necessary
result = append(result, matchingNew)
continue
@ -258,7 +275,7 @@ func (d *dynProviderState) recordLinkToEndpoint(client *dynect.Client, recordLin
DNSName: rec.Data.FQDN,
RecordTTL: endpoint.TTL(rec.Data.TTL),
RecordType: rec.Data.RecordType,
Target: target,
Targets: endpoint.Targets{target},
}
log.Debugf("Fetched new endpoint for %s: %+v", recordLink, result)
@ -280,11 +297,11 @@ func endpointToRecord(ep *endpoint.Endpoint) *dynect.DataBlock {
result := dynect.DataBlock{}
if ep.RecordType == endpoint.RecordTypeA {
result.Address = ep.Target
result.Address = ep.Targets[0]
} else if ep.RecordType == endpoint.RecordTypeCNAME {
result.CName = ep.Target
result.CName = ep.Targets[0]
} else if ep.RecordType == endpoint.RecordTypeTXT {
result.TxtData = ep.Target
result.TxtData = ep.Targets[0]
}
return &result
@ -320,7 +337,6 @@ func (d *dynProviderState) buildLinkToRecord(ep *endpoint.Endpoint) string {
}
if matchingZone == "" {
fmt.Printf("no zone")
// no matching zone, ignore
return ""
}
@ -337,6 +353,12 @@ func (d *dynProviderState) buildLinkToRecord(ep *endpoint.Endpoint) string {
// This method also stores the DynAPI version.
// Don't user the dynect.Client.Login()
func (d *dynProviderState) login() (*dynect.Client, error) {
if d.LastLoginErrorTime != 0 {
secondsSinceLastError := unixNow() - d.LastLoginErrorTime
if secondsSinceLastError < badLoginMinIntervalSeconds {
return nil, fmt.Errorf("will not attempt an API call as the last login failure occurred just %ds ago", secondsSinceLastError)
}
}
client := dynect.NewClient(d.CustomerName)
var req = dynect.LoginBlock{
@ -348,9 +370,11 @@ func (d *dynProviderState) login() (*dynect.Client, error) {
err := client.Do("POST", "Session", req, &resp)
if err != nil {
d.LastLoginErrorTime = unixNow()
return nil, err
}
d.LastLoginErrorTime = 0
client.Token = resp.Data.Token
// this is the only change from the original
@ -371,7 +395,7 @@ func (d *dynProviderState) buildRecordRequest(ep *endpoint.Endpoint) (string, *d
}
record := dynect.RecordRequest{
TTL: fixMissingTTL(ep.RecordTTL),
TTL: fixMissingTTL(ep.RecordTTL, d.MinTTLSeconds),
RData: *endpointToRecord(ep),
}
return link, &record

View File

@ -32,13 +32,13 @@ func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeA,
},
@ -47,13 +47,13 @@ func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) {
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
@ -66,13 +66,13 @@ func TestDynMerge_UpdateOnTTLChanges(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
@ -81,13 +81,13 @@ func TestDynMerge_UpdateOnTTLChanges(t *testing.T) {
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(77),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(10),
RecordType: endpoint.RecordTypeCNAME,
},
@ -102,13 +102,13 @@ func TestDynMerge_AlwaysUpdateTarget(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(1),
RecordType: endpoint.RecordTypeCNAME,
},
@ -117,13 +117,13 @@ func TestDynMerge_AlwaysUpdateTarget(t *testing.T) {
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1-changed",
Targets: endpoint.Targets{"target1-changed"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(0),
RecordType: endpoint.RecordTypeCNAME,
},
@ -131,20 +131,20 @@ func TestDynMerge_AlwaysUpdateTarget(t *testing.T) {
merged := merge(updateOld, updateNew)
assert.Equal(t, 1, len(merged))
assert.Equal(t, "target1-changed", merged[0].Target)
assert.Equal(t, "target1-changed", merged[0].Targets[0])
}
func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
updateOld := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
@ -153,13 +153,13 @@ func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
updateNew := []*endpoint.Endpoint{
{
DNSName: "name1",
Target: "target1",
Targets: endpoint.Targets{"target1"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "name2",
Target: "target2",
Targets: endpoint.Targets{"target2"},
RecordTTL: endpoint.TTL(55),
RecordType: endpoint.RecordTypeCNAME,
},
@ -190,9 +190,9 @@ func TestDyn_endpointToRecord(t *testing.T) {
ep *endpoint.Endpoint
extractor func(*dynect.DataBlock) string
}{
{endpoint.NewEndpoint("address", "the-target", "A"), func(b *dynect.DataBlock) string { return b.Address }},
{endpoint.NewEndpoint("cname", "the-target", "CNAME"), func(b *dynect.DataBlock) string { return b.CName }},
{endpoint.NewEndpoint("text", "the-target", "TXT"), func(b *dynect.DataBlock) string { return b.TxtData }},
{endpoint.NewEndpoint("address", "A", "the-target"), func(b *dynect.DataBlock) string { return b.Address }},
{endpoint.NewEndpoint("cname", "CNAME", "the-target"), func(b *dynect.DataBlock) string { return b.CName }},
{endpoint.NewEndpoint("text", "TXT", "the-target"), func(b *dynect.DataBlock) string { return b.TxtData }},
}
for _, tc := range tests {
@ -213,11 +213,11 @@ func TestDyn_buildLinkToRecord(t *testing.T) {
ep *endpoint.Endpoint
link string
}{
{endpoint.NewEndpoint("sub.the-target.example.com", "address", "A"), "ARecord/example.com/sub.the-target.example.com/"},
{endpoint.NewEndpoint("the-target.example.com", "cname", "CNAME"), "CNAMERecord/example.com/the-target.example.com/"},
{endpoint.NewEndpoint("the-target.example.com", "text", "TXT"), "TXTRecord/example.com/the-target.example.com/"},
{endpoint.NewEndpoint("the-target.google.com", "text", "TXT"), ""},
{endpoint.NewEndpoint("mail.example.com", "text", "TXT"), ""},
{endpoint.NewEndpoint("sub.the-target.example.com", "A", "address"), "ARecord/example.com/sub.the-target.example.com/"},
{endpoint.NewEndpoint("the-target.example.com", "CNAME", "cname"), "CNAMERecord/example.com/the-target.example.com/"},
{endpoint.NewEndpoint("the-target.example.com", "TXT", "text"), "TXTRecord/example.com/the-target.example.com/"},
{endpoint.NewEndpoint("the-target.google.com", "TXT", "text"), ""},
{endpoint.NewEndpoint("mail.example.com", "TXT", "text"), ""},
{nil, ""},
}
@ -255,10 +255,13 @@ func TestDyn_filterAndFixLinks(t *testing.T) {
}
func TestDyn_fixMissingTTL(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%v", dynDefaultTTL), fixMissingTTL(endpoint.TTL(0)))
assert.Equal(t, fmt.Sprintf("%v", dynDefaultTTL), fixMissingTTL(endpoint.TTL(0), 0))
// nothing to fix
assert.Equal(t, "111", fixMissingTTL(endpoint.TTL(111)))
assert.Equal(t, "111", fixMissingTTL(endpoint.TTL(111), 25))
// apply min TTL
assert.Equal(t, "1992", fixMissingTTL(endpoint.TTL(111), 1992))
}
func TestDyn_cachePut(t *testing.T) {
@ -268,7 +271,7 @@ func TestDyn_cachePut(t *testing.T) {
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Target: "target",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(10000),
RecordType: "A",
})
@ -284,7 +287,7 @@ func TestDyn_cachePutExpired(t *testing.T) {
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Target: "target",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(0),
RecordType: "A",
})

View File

@ -18,8 +18,10 @@ package provider
import (
"fmt"
"sort"
"strings"
"cloud.google.com/go/compute/metadata"
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
@ -132,6 +134,14 @@ func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter Z
return nil, err
}
if project == "" {
mProject, mErr := metadata.ProjectID()
if mErr == nil {
log.Infof("Google project auto-detected: %s", mProject)
project = mProject
}
}
provider := &GoogleProvider{
project: project,
domainFilter: domainFilter,
@ -151,7 +161,7 @@ func (p *GoogleProvider) Zones() (map[string]*dns.ManagedZone, error) {
f := func(resp *dns.ManagedZonesListResponse) error {
for _, zone := range resp.ManagedZones {
if p.domainFilter.Match(zone.DnsName) || p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) {
if p.domainFilter.Match(zone.DnsName) && p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) {
zones[zone.Name] = zone
log.Debugf("Matched %s (zone: %s)", zone.DnsName, zone.Name)
} else {
@ -191,13 +201,20 @@ func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
f := func(resp *dns.ResourceRecordSetsListResponse) error {
for _, r := range resp.Rrsets {
if !supportedRecordType(r.Type) {
continue
}
ep := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(r.Name, "."),
RecordType: r.Type,
Targets: make(endpoint.Targets, 0, len(r.Rrdatas)),
}
for _, rr := range r.Rrdatas {
// each page is processed sequentially, no need for a mutex here.
if supportedRecordType(r.Type) {
endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, rr, r.Type))
}
ep.Targets = append(ep.Targets, strings.TrimSuffix(rr, "."))
}
sort.Sort(ep.Targets)
endpoints = append(endpoints, ep)
}
return nil
@ -347,9 +364,10 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet {
// TODO(linki): works around appending a trailing dot to TXT records. I think
// we should go back to storing DNS names with a trailing dot internally. This
// way we can use it has is here and trim it off if it exists when necessary.
target := ep.Target
targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME {
target = ensureTrailingDot(target)
targets[0] = ensureTrailingDot(targets[0])
}
// no annotation results in a Ttl of 0, default to 300 for backwards-compatability
@ -360,7 +378,7 @@ func newRecord(ep *endpoint.Endpoint) *dns.ResourceRecordSet {
return &dns.ResourceRecordSet{
Name: ensureTrailingDot(ep.DNSName),
Rrdatas: []string{target},
Rrdatas: targets,
Ttl: ttl,
Type: ep.RecordType,
}

View File

@ -207,9 +207,9 @@ func TestGoogleZones(t *testing.T) {
func TestGoogleRecords(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints)
@ -222,12 +222,12 @@ func TestGoogleRecords(t *testing.T) {
func TestGoogleRecordsFilter(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
}
provider := newGoogleProvider(
@ -247,12 +247,12 @@ func TestGoogleRecordsFilter(t *testing.T) {
// these records should be filtered out since they don't match a hosted zone or domain filter.
ignoredEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("filter-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("filter-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("filter-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("filter-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
}
require.NoError(t, provider.CreateRecords(ignoredEndpoints))
@ -268,9 +268,9 @@ func TestGoogleCreateRecords(t *testing.T) {
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
records := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
}
require.NoError(t, provider.CreateRecords(records))
@ -279,28 +279,28 @@ func TestGoogleCreateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
})
}
func TestGoogleUpdateRecords(t *testing.T) {
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
})
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
}
require.NoError(t, provider.UpdateRecords(updatedRecords, currentRecords))
@ -309,17 +309,17 @@ func TestGoogleUpdateRecords(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
})
}
func TestGoogleDeleteRecords(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints)
@ -346,43 +346,43 @@ func TestGoogleApplyChanges(t *testing.T) {
NewZoneIDFilter([]string{""}),
false,
[]*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
},
)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("nomatch-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", "4.2.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("filter-create-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("nomatch-create-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.1"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", "5.6.7.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("nomatch-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", "8.7.6.5", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("filter-update-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "5.6.7.8"),
endpoint.NewEndpoint("nomatch-update-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.7.6.5"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", "4.2.2.2", endpoint.RecordTypeA),
endpoint.NewEndpoint("nomatch-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", "4.2.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
endpoint.NewEndpoint("filter-delete-test.zone-3.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.2"),
endpoint.NewEndpoint("nomatch-delete-test.zone-0.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.2.2.1"),
}
changes := &plan.Changes{
@ -398,48 +398,48 @@ func TestGoogleApplyChanges(t *testing.T) {
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
})
}
func TestGoogleApplyChangesDryRun(t *testing.T) {
originalEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
}
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), true, originalEndpoints)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
}
currentRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "4.3.2.1", endpoint.RecordTypeA),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
}
changes := &plan.Changes{
@ -466,13 +466,13 @@ func TestNewFilteredRecords(t *testing.T) {
provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{})
records := provider.newFilteredRecords([]*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA, 1),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA, 120),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "bar.elb.amazonaws.com", endpoint.RecordTypeCNAME, 4000),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 1, "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 120, "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, 4000, "bar.elb.amazonaws.com"),
// test fallback to Ttl:300 when Ttl==0 :
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA, 0),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, 0, "8.8.8.8"),
endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"),
})
validateChangeRecords(t, records, []*dns.ResourceRecordSet{
@ -593,6 +593,12 @@ func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter Zon
DnsName: "zone-3.ext-dns-test-2.gcp.zalan.do.",
})
// filtered out by domain filter
createZone(t, provider, &dns.ManagedZone{
Name: "zone-4-ext-dns-test-3-gcp-zalan-do",
DnsName: "zone-4.ext-dns-test-3.gcp.zalan.do.",
})
setupGoogleRecords(t, provider, records)
provider.dryRun = dryRun

View File

@ -110,7 +110,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
return nil, err
}
for _, res := range resA {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, res.Ipv4Addr, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr))
}
// Include Host records since they should be treated synonymously with A records
@ -126,7 +126,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
}
for _, res := range resH {
for _, ip := range res.Ipv4Addrs {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, ip.Ipv4Addr, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, ip.Ipv4Addr))
}
}
@ -141,7 +141,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
return nil, err
}
for _, res := range resC {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, res.Canonical, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical))
}
var resT []ibclient.RecordTXT
@ -160,7 +160,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
if _, err := strconv.Unquote(res.Text); err != nil {
res.Text = strconv.Quote(res.Text)
}
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, res.Text, endpoint.RecordTypeTXT))
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text))
}
}
return endpoints, nil
@ -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
}
@ -257,7 +257,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
obj := ibclient.NewRecordA(
ibclient.RecordA{
Name: ep.DNSName,
Ipv4Addr: ep.Target,
Ipv4Addr: ep.Targets[0],
},
)
if getObject {
@ -275,7 +275,7 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
obj := ibclient.NewRecordCNAME(
ibclient.RecordCNAME{
Name: ep.DNSName,
Canonical: ep.Target,
Canonical: ep.Targets[0],
},
)
if getObject {
@ -292,13 +292,13 @@ func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (rec
var res []ibclient.RecordTXT
// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
// Here we reconcile that fact by making this state match that reality.
if target, err2 := strconv.Unquote(ep.Target); err2 == nil && !strings.Contains(ep.Target, " ") {
ep.Target = target
if target, err2 := strconv.Unquote(ep.Targets[0]); err2 == nil && !strings.Contains(ep.Targets[0], " ") {
ep.Targets = endpoint.Targets{target}
}
obj := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Name: ep.DNSName,
Text: ep.Target,
Text: ep.Targets[0],
},
)
if getObject {
@ -323,7 +323,7 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
"Would create %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
zone,
)
continue
@ -333,7 +333,7 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
"Creating %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
zone,
)
@ -343,7 +343,7 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
zone,
err,
)
@ -355,7 +355,7 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
zone,
err,
)
@ -378,7 +378,7 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Target,
ep.Targets,
zone,
err,
)

View File

@ -44,8 +44,8 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string,
client.createdEndpoints,
endpoint.NewEndpoint(
obj.(*ibclient.RecordA).Name,
obj.(*ibclient.RecordA).Ipv4Addr,
endpoint.RecordTypeA,
obj.(*ibclient.RecordA).Ipv4Addr,
),
)
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordA).Name)), obj.(*ibclient.RecordA).Name)
@ -55,8 +55,8 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string,
client.createdEndpoints,
endpoint.NewEndpoint(
obj.(*ibclient.RecordCNAME).Name,
obj.(*ibclient.RecordCNAME).Canonical,
endpoint.RecordTypeCNAME,
obj.(*ibclient.RecordCNAME).Canonical,
),
)
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordCNAME).Name)), obj.(*ibclient.RecordCNAME).Name)
@ -67,8 +67,8 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string,
client.createdEndpoints,
endpoint.NewEndpoint(
obj.(*ibclient.RecordHost).Name,
i.Ipv4Addr,
endpoint.RecordTypeA,
i.Ipv4Addr,
),
)
}
@ -79,8 +79,8 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string,
client.createdEndpoints,
endpoint.NewEndpoint(
obj.(*ibclient.RecordTXT).Name,
obj.(*ibclient.RecordTXT).Text,
endpoint.RecordTypeTXT,
obj.(*ibclient.RecordTXT).Text,
),
)
obj.(*ibclient.RecordTXT).Ref = ref
@ -183,8 +183,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
client.deletedEndpoints,
endpoint.NewEndpoint(
record.Name,
"",
endpoint.RecordTypeA,
"",
),
)
}
@ -201,8 +201,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
client.deletedEndpoints,
endpoint.NewEndpoint(
record.Name,
"",
endpoint.RecordTypeCNAME,
"",
),
)
}
@ -219,8 +219,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
client.deletedEndpoints,
endpoint.NewEndpoint(
record.Name,
"",
endpoint.RecordTypeA,
"",
),
)
}
@ -237,8 +237,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
client.deletedEndpoints,
endpoint.NewEndpoint(
record.Name,
"",
endpoint.RecordTypeTXT,
"",
),
)
}
@ -359,13 +359,13 @@ func TestInfobloxRecords(t *testing.T) {
t.Fatal(err)
}
expected := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", "123.123.123.122", endpoint.RecordTypeA),
endpoint.NewEndpoint("example.com", "\"heritage=external-dns,external-dns/owner=default\"", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("nginx.example.com", "123.123.123.123", endpoint.RecordTypeA),
endpoint.NewEndpoint("nginx.example.com", "\"heritage=external-dns,external-dns/owner=default\"", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("whitespace.example.com", "123.123.123.124", endpoint.RecordTypeA),
endpoint.NewEndpoint("whitespace.example.com", "\"heritage=external-dns,external-dns/owner=white space\"", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("hack.example.com", "cerberus.infoblox.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "123.123.123.122"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("nginx.example.com", endpoint.RecordTypeA, "123.123.123.123"),
endpoint.NewEndpoint("nginx.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=white space\""),
endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
}
validateEndpoints(t, actual, expected)
}
@ -376,23 +376,23 @@ func TestInfobloxApplyChanges(t *testing.T) {
testInfobloxApplyChangesInternal(t, false, &client)
validateEndpoints(t, client.createdEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("foo.example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("foo.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("bar.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("bar.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("other.com", "5.6.7.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("other.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("new.example.com", "111.222.111.222", endpoint.RecordTypeA),
endpoint.NewEndpoint("newcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
})
validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", "", endpoint.RecordTypeA),
endpoint.NewEndpoint("oldcname.example.com", "", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("deleted.example.com", "", endpoint.RecordTypeA),
endpoint.NewEndpoint("deletedcname.example.com", "", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
})
validateEndpoints(t, client.updatedEndpoints, []*endpoint.Endpoint{})
@ -432,34 +432,34 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
)
createRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("foo.example.com", "1.2.3.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("foo.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("bar.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("bar.example.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("other.com", "5.6.7.8", endpoint.RecordTypeA),
endpoint.NewEndpoint("other.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("nope.com", "4.4.4.4", endpoint.RecordTypeA),
endpoint.NewEndpoint("nope.com", "tag", endpoint.RecordTypeTXT),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("bar.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeA, "5.6.7.8"),
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
}
updateOldRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", "121.212.121.212", endpoint.RecordTypeA),
endpoint.NewEndpoint("oldcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("old.nope.com", "121.212.121.212", endpoint.RecordTypeA),
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("old.nope.com", endpoint.RecordTypeA, "121.212.121.212"),
}
updateNewRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("new.example.com", "111.222.111.222", endpoint.RecordTypeA),
endpoint.NewEndpoint("newcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("new.nope.com", "222.111.222.111", endpoint.RecordTypeA),
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("new.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.example.com", "121.212.121.212", endpoint.RecordTypeA),
endpoint.NewEndpoint("deletedcname.example.com", "other.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("deleted.nope.com", "222.111.222.111", endpoint.RecordTypeA),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}
changes := &plan.Changes{

View File

@ -130,7 +130,7 @@ func (im *InMemoryProvider) Records() ([]*endpoint.Endpoint, error) {
}
for _, record := range records {
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Target, record.Type))
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, record.Target))
}
}
@ -203,7 +203,7 @@ func convertToInMemoryRecord(endpoints []*endpoint.Endpoint) []*inMemoryRecord {
records = append(records, &inMemoryRecord{
Type: ep.RecordType,
Name: ep.DNSName,
Target: ep.Target,
Target: ep.Targets[0],
})
}
return records

View File

@ -186,16 +186,17 @@ func testInMemoryRecords(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "example.org",
RecordType: endpoint.RecordTypeTXT,
Targets: endpoint.Targets{""},
},
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeCNAME,
},
},
@ -214,7 +215,7 @@ func testInMemoryRecords(t *testing.T) {
assert.EqualError(t, err, ErrZoneNotFound.Error())
} else {
require.NoError(t, err)
assert.True(t, testutils.SameEndpoints(ti.expected, records))
assert.True(t, testutils.SameEndpoints(ti.expected, records), "Endpoints not the same: Expected: %+v Records: %+v", ti.expected, records)
}
})
}
@ -314,7 +315,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -333,14 +334,14 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
},
@ -358,14 +359,14 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
},
@ -383,12 +384,12 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "foo.org",
Target: "4.4.4.4",
Targets: endpoint.Targets{"4.4.4.4"},
RecordType: endpoint.RecordTypeA,
},
},
@ -408,7 +409,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -416,7 +417,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Delete: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -433,12 +434,12 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -458,7 +459,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "new.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -478,7 +479,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Delete: []*endpoint.Endpoint{
{
DNSName: "new.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -497,7 +498,7 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Delete: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
},
},
@ -512,21 +513,21 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "foo.bar.new.org",
Target: "4.8.8.9",
Targets: endpoint.Targets{"4.8.8.9"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "4.8.8.4",
Targets: endpoint.Targets{"4.8.8.4"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
},
},
@ -608,7 +609,7 @@ func testInMemoryApplyChanges(t *testing.T) {
changes: &plan.Changes{
Create: []*endpoint.Endpoint{{
DNSName: "example.de",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
}},
UpdateNew: []*endpoint.Endpoint{},
@ -625,7 +626,7 @@ func testInMemoryApplyChanges(t *testing.T) {
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -633,7 +634,7 @@ func testInMemoryApplyChanges(t *testing.T) {
Delete: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},
@ -649,7 +650,7 @@ func testInMemoryApplyChanges(t *testing.T) {
Delete: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
},
},
@ -697,28 +698,28 @@ func testInMemoryApplyChanges(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "foo.bar.new.org",
Target: "4.8.8.9",
Targets: endpoint.Targets{"4.8.8.9"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "4.8.8.4",
Targets: endpoint.Targets{"4.8.8.4"},
RecordType: endpoint.RecordTypeA,
},
},
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "foo.bar.org",
Target: "5.5.5.5",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
},
},
Delete: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
},
},

406
provider/pdns.go Normal file
View File

@ -0,0 +1,406 @@
/*
Copyright 2018 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 (
"bytes"
"context"
"encoding/json"
"errors"
"math"
"net/http"
"sort"
"strings"
"time"
log "github.com/sirupsen/logrus"
pgo "github.com/ffledgling/pdns-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
type pdnsChangeType string
const (
apiBase = "/api/v1"
// Unless we use something like pdnsproxy (discontinued upsteam), this value will _always_ be localhost
defaultServerID = "localhost"
defaultTTL = 300
// PdnsDelete and PdnsReplace are effectively an enum for "pgo.RrSet.changetype"
// TODO: Can we somehow get this from the pgo swagger client library itself?
// PdnsDelete : PowerDNS changetype used for deleting rrsets
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#rrset (see "changetype")
PdnsDelete pdnsChangeType = "DELETE"
// PdnsReplace : PowerDNS changetype for creating, updating and patching rrsets
PdnsReplace pdnsChangeType = "REPLACE"
// Number of times to retry failed PDNS requests
retryLimit = 3
// time in milliseconds
retryAfterTime = 250 * time.Millisecond
)
// Function for debug printing
func stringifyHTTPResponseBody(r *http.Response) (body string) {
if r == nil {
return ""
}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
body = buf.String()
return body
}
// PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as
// well as mock APIClients used in testing
type PDNSAPIProvider interface {
ListZones() ([]pgo.Zone, *http.Response, error)
ListZone(zoneID string) (pgo.Zone, *http.Response, error)
PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error)
}
// PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details
type PDNSAPIClient struct {
dryRun bool
authCtx context.Context
client *pgo.APIClient
}
// ListZones : Method returns all enabled zones from PowerDNS
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones
func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, defaultServerID)
if err != nil {
log.Debugf("Unable to fetch zones %v", err)
log.Debugf("Retrying ListZones() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i)))
continue
}
return zones, resp, err
}
log.Errorf("Unable to fetch zones. %v", err)
return zones, resp, err
}
// ListZone : Method returns the details of a specific zone from PowerDNS
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id
func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, defaultServerID, zoneID)
if err != nil {
log.Debugf("Unable to fetch zone %v", err)
log.Debugf("Retrying ListZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i)))
continue
}
return zone, resp, err
}
log.Errorf("Unable to list zone. %v", err)
return zone, resp, err
}
// PatchZone : Method used to update the contents of a particular zone from PowerDNS
// ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id
func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) {
for i := 0; i < retryLimit; i++ {
resp, err = c.client.ZonesApi.PatchZone(c.authCtx, defaultServerID, zoneID, zoneStruct)
if err != nil {
log.Debugf("Unable to patch zone %v", err)
log.Debugf("Retrying PatchZone() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i)))
continue
}
return resp, err
}
log.Errorf("Unable to patch zone. %v", err)
return resp, err
}
// PDNSProvider is an implementation of the Provider interface for PowerDNS
type PDNSProvider struct {
client PDNSAPIProvider
}
// NewPDNSProvider initializes a new PowerDNS based Provider.
func NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error) {
// Do some input validation
if apikey == "" {
return nil, errors.New("Missing API Key for PDNS. Specify using --pdns-api-key=")
}
// The default for when no --domain-filter is passed is [""], instead of [], so we check accordingly.
if len(domainFilter.filters) != 1 && domainFilter.filters[0] != "" {
return nil, errors.New("PDNS Provider does not support domain filter")
}
// We do not support dry running, exit safely instead of surprising the user
// TODO: Add Dry Run support
if dryRun {
return nil, errors.New("PDNS Provider does not currently support dry-run")
}
if server == "localhost" {
log.Warnf("PDNS Server is set to localhost, this may not be what you want. Specify using --pdns-server=")
}
cfg := pgo.NewConfiguration()
cfg.Host = server
cfg.BasePath = server + apiBase
provider := &PDNSProvider{
client: &PDNSAPIClient{
dryRun: dryRun,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: apikey}),
client: pgo.NewAPIClient(cfg),
},
}
return provider, nil
}
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
endpoints = []*endpoint.Endpoint{}
for _, record := range rr.Records {
// If a record is "Disabled", it's not supposed to be "visible"
if !record.Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), record.Content))
}
}
return endpoints, nil
}
// ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs
func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) {
zonelist = []pgo.Zone{}
endpoints := make([]*endpoint.Endpoint, len(eps))
copy(endpoints, eps)
// Sort the endpoints array so we have deterministic inserts
sort.SliceStable(endpoints,
func(i, j int) bool {
// We only care about sorting endpoints with the same dnsname
if endpoints[i].DNSName == endpoints[j].DNSName {
return endpoints[i].RecordType < endpoints[j].RecordType
}
return endpoints[i].DNSName < endpoints[j].DNSName
})
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
}
// Sort the zone by length of the name in descending order, we use this
// property later to ensure we add a record to the longest matching zone
sort.SliceStable(zones, func(i, j int) bool { return len(zones[i].Name) > len(zones[j].Name) })
// NOTE: Complexity of this loop is O(Zones*Endpoints).
// A possibly faster implementation would be a search of the reversed
// DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not
// necessary.
for _, zone := range zones {
zone.Rrsets = []pgo.RrSet{}
for i := 0; i < len(endpoints); {
ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName)
if strings.HasSuffix(dnsname, zone.Name) {
// The assumption here is that there will only ever be one target
// per (ep.DNSName, ep.RecordType) tuple, which holds true for
// external-dns v5.0.0-alpha onwards
records := []pgo.Record{}
for _, t := range ep.Targets {
if "CNAME" == ep.RecordType {
t = ensureTrailingDot(t)
}
records = append(records, pgo.Record{Content: t})
}
rrset := pgo.RrSet{
Name: dnsname,
Type_: ep.RecordType,
Records: records,
Changetype: string(changetype),
}
// DELETEs explicitly forbid a TTL, therefore only PATCHes need the TTL
if changetype == PdnsReplace {
if int64(ep.RecordTTL) > int64(math.MaxInt32) {
return nil, errors.New("Value of record TTL overflows, limited to int32")
}
if ep.RecordTTL == 0 {
// No TTL was sepecified for the record, we use the default
rrset.Ttl = int32(defaultTTL)
} else {
rrset.Ttl = int32(ep.RecordTTL)
}
}
zone.Rrsets = append(zone.Rrsets, rrset)
// "pop" endpoint if it's matched
endpoints = append(endpoints[0:i], endpoints[i+1:]...)
} else {
// If we didn't pop anything, we move to the next item in the list
i++
}
}
if len(zone.Rrsets) > 0 {
zonelist = append(zonelist, zone)
}
}
// If we still have some endpoints left, it means we couldn't find a matching zone for them
// We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
if len(endpoints) > 0 {
log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints)
}
log.Debugf("Zone List generated from Endpoints: %+v", zonelist)
return zonelist, nil
}
// mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype
func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error {
zonelist, err := p.ConvertEndpointsToZones(endpoints, changetype)
if err != nil {
return err
}
for _, zone := range zonelist {
jso, err := json.Marshal(zone)
if err != nil {
log.Errorf("JSON Marshal for zone struct failed!")
} else {
log.Debugf("Struct for PatchZone:\n%s", string(jso))
}
resp, err := p.client.PatchZone(zone.Id, zone)
if err != nil {
log.Debugf("PDNS API response: %s", stringifyHTTPResponseBody(resp))
return err
}
}
return nil
}
// Records returns all DNS records controlled by the configured PDNS server (for all zones)
func (p *PDNSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
zones, _, err := p.client.ListZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
z, _, err := p.client.ListZone(zone.Id)
if err != nil {
log.Warnf("Unable to fetch Records")
return nil, err
}
for _, rr := range z.Rrsets {
e, err := p.convertRRSetToEndpoints(rr)
if err != nil {
return nil, err
}
endpoints = append(endpoints, e...)
}
}
log.Debugf("Records fetched:\n%+v", endpoints)
return endpoints, nil
}
// ApplyChanges takes a list of changes (endpoints) and updates the PDNS server
// by sending the correct HTTP PATCH requests to a matching zone
func (p *PDNSProvider) ApplyChanges(changes *plan.Changes) error {
startTime := time.Now()
// Create
for _, change := range changes.Create {
log.Debugf("CREATE: %+v", change)
}
// We only attempt to mutate records if there are any to mutate. A
// call to mutate records with an empty list of endpoints is still a
// valid call and a no-op, but we might as well not make the call to
// prevent unnecessary logging
if len(changes.Create) > 0 {
// "Replacing" non-existant records creates them
err := p.mutateRecords(changes.Create, PdnsReplace)
if err != nil {
return err
}
}
// Update
for _, change := range changes.UpdateOld {
// Since PDNS "Patches", we don't need to specify the "old"
// record. The Update New change type will automatically take
// care of replacing the old RRSet with the new one We simply
// leave this logging here for information
log.Debugf("UPDATE-OLD (ignored): %+v", change)
}
for _, change := range changes.UpdateNew {
log.Debugf("UPDATE-NEW: %+v", change)
}
if len(changes.UpdateNew) > 0 {
err := p.mutateRecords(changes.UpdateNew, PdnsReplace)
if err != nil {
return err
}
}
// Delete
for _, change := range changes.Delete {
log.Debugf("DELETE: %+v", change)
}
if len(changes.Delete) > 0 {
err := p.mutateRecords(changes.Delete, PdnsDelete)
if err != nil {
return err
}
}
log.Debugf("Changes pushed out to PowerDNS in %s\n", time.Since(startTime))
return nil
}

641
provider/pdns_test.go Normal file
View File

@ -0,0 +1,641 @@
/*
Copyright 2018 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 (
"errors"
//"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
pgo "github.com/ffledgling/pdns-go"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
// FIXME: What do we do about labels?
var (
// Simple RRSets that contain 1 A record and 1 TXT record
RRSetSimpleARecord = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
},
}
RRSetSimpleTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false},
},
}
RRSetLongARecord = pgo.RrSet{
Name: "a.very.long.domainname.example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
},
}
RRSetLongTXTRecord = pgo.RrSet{
Name: "a.very.long.domainname.example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", Disabled: false, SetPtr: false},
},
}
// RRSet with one record disabled
RRSetDisabledRecord = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
{Content: "8.8.4.4", Disabled: true, SetPtr: false},
},
}
RRSetCNAMERecord = pgo.RrSet{
Name: "cname.example.com.",
Type_: "CNAME",
Ttl: 300,
Records: []pgo.Record{
{Content: "example.by.any.other.name.com", Disabled: false, SetPtr: false},
},
}
RRSetTXTRecord = pgo.RrSet{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Records: []pgo.Record{
{Content: "'would smell as sweet'", Disabled: false, SetPtr: false},
},
}
// Multiple PDNS records in an RRSet of a single type
RRSetMultipleRecords = pgo.RrSet{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Records: []pgo.Record{
{Content: "8.8.8.8", Disabled: false, SetPtr: false},
{Content: "8.8.4.4", Disabled: false, SetPtr: false},
{Content: "4.4.4.4", Disabled: false, SetPtr: false},
},
}
endpointsDisabledRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
}
endpointsSimpleRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsLongRecord = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsNonexistantZone = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("does.not.exist.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("does.not.exist.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "4.4.4.4"),
}
endpointsMixedRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "'would smell as sweet'"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "4.4.4.4"),
}
endpointsMultipleZones = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("mock.test", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("mock.test", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZones2 = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("abcd.mock.test", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("abcd.mock.test", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleZonesWithNoExist = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"),
endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
ZoneEmpty = pgo.Zone{
// Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs.
Id: "example.com.",
// Name of the zone (e.g. “example.com.”) MUST have a trailing dot
Name: "example.com.",
// Set to “Zone”
Type_: "Zone",
// API endpoint for this zone
Url: "/api/v1/servers/localhost/zones/example.com.",
// Zone kind, one of “Native”, “Master”, “Slave”
Kind: "Native",
// RRSets in this zone
Rrsets: []pgo.RrSet{},
}
ZoneEmptyLong = pgo.Zone{
Id: "long.domainname.example.com.",
Name: "long.domainname.example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/long.domainname.example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{},
}
ZoneEmpty2 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{},
}
ZoneMixed = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{RRSetCNAMERecord, RRSetTXTRecord, RRSetMultipleRecords},
}
ZoneEmptyToSimplePatch = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "example.com.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToLongPatch = pgo.Zone{
Id: "long.domainname.example.com.",
Name: "long.domainname.example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/long.domainname.example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "a.very.long.domainname.example.com.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "a.very.long.domainname.example.com.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimplePatch2 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "mock.test.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "mock.test.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimplePatch3 = pgo.Zone{
Id: "mock.test.",
Name: "mock.test.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/mock.test.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "abcd.mock.test.",
Type_: "A",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "9.9.9.9",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "abcd.mock.test.",
Type_: "TXT",
Ttl: 300,
Changetype: "REPLACE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
ZoneEmptyToSimpleDelete = pgo.Zone{
Id: "example.com.",
Name: "example.com.",
Type_: "Zone",
Url: "/api/v1/servers/localhost/zones/example.com.",
Kind: "Native",
Rrsets: []pgo.RrSet{
{
Name: "example.com.",
Type_: "A",
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "8.8.8.8",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
{
Name: "example.com.",
Type_: "TXT",
Changetype: "DELETE",
Records: []pgo.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"",
Disabled: false,
SetPtr: false,
},
},
Comments: []pgo.Comment(nil),
},
},
}
)
/******************************************************************************/
// API that returns a zone with multiple record types
type PDNSAPIClientStub struct {
}
func (c *PDNSAPIClientStub) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneMixed}, nil, nil
}
func (c *PDNSAPIClientStub) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return ZoneMixed, nil, nil
}
func (c *PDNSAPIClientStub) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
return nil, nil
}
/******************************************************************************/
// API that returns a zones with no records
type PDNSAPIClientStubEmptyZones struct {
// Keep track of all zones we recieve via PatchZone
patchedZones []pgo.Zone
}
func (c *PDNSAPIClientStubEmptyZones) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2}, nil, nil
}
func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
if strings.Contains(zoneID, "example.com") {
return ZoneEmpty, nil, nil
} else if strings.Contains(zoneID, "mock.test") {
return ZoneEmpty2, nil, nil
} else if strings.Contains(zoneID, "long.domainname.example.com") {
return ZoneEmpty2, nil, nil
}
return pgo.Zone{}, nil, nil
}
func (c *PDNSAPIClientStubEmptyZones) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
c.patchedZones = append(c.patchedZones, zoneStruct)
return nil, nil
}
/******************************************************************************/
// API that returns error on PatchZone()
type PDNSAPIClientStubPatchZoneFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the PatchZone method to introduce a failure
func (c *PDNSAPIClientStubPatchZoneFailure) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) {
return nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
// API that returns error on ListZone()
type PDNSAPIClientStubListZoneFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the ListZone method to introduce a failure
func (c *PDNSAPIClientStubListZoneFailure) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return pgo.Zone{}, nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
// API that returns error on ListZones() (Zones - plural)
type PDNSAPIClientStubListZonesFailure struct {
// Anonymous struct for composition
PDNSAPIClientStubEmptyZones
}
// Just overwrite the ListZones method to introduce a failure
func (c *PDNSAPIClientStubListZonesFailure) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{}, nil, errors.New("Generic PDNS Error")
}
/******************************************************************************/
type NewPDNSProviderTestSuite struct {
suite.Suite
}
func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreate() {
// Function definition: NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error)
_, err := NewPDNSProvider("http://localhost:8081", "", NewDomainFilter([]string{""}), false)
assert.Error(suite.T(), err, "--pdns-api-key should be specified")
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{"example.com", "example.org"}), false)
assert.Error(suite.T(), err, "--domainfilter should raise an error")
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{""}), true)
assert.Error(suite.T(), err, "--dry-run should raise an error")
// This is our "regular" code path, no error should be thrown
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{""}), false)
assert.Nil(suite.T(), err, "Regular case should raise no error")
}
func (suite *NewPDNSProviderTestSuite) TestPDNSRRSetToEndpoints() {
// Function definition: convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStub{},
}
/* given an RRSet with three records, we test:
- We correctly create corresponding endpoints
*/
eps, err := p.convertRRSetToEndpoints(RRSetMultipleRecords)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsMultipleRecords, eps)
/* Given an RRSet with two records, one of which is disabled, we test:
- We can correctly convert the RRSet into a list of valid endpoints
- We correctly discard/ignore the disabled record.
*/
eps, err = p.convertRRSetToEndpoints(RRSetDisabledRecord)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsDisabledRecord, eps)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSRecords() {
// Function definition: Records() (endpoints []*endpoint.Endpoint, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStub{},
}
/* We test that endpoints are returned correctly for a Zone when Records() is called
*/
eps, err := p.Records()
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), endpointsMixedRecords, eps)
// Test failures are handled correctly
// Create a new provider to run tests against
p = &PDNSProvider{
client: &PDNSAPIClientStubListZoneFailure{},
}
eps, err = p.Records()
assert.NotNil(suite.T(), err)
p = &PDNSProvider{
client: &PDNSAPIClientStubListZonesFailure{},
}
eps, err = p.Records()
assert.NotNil(suite.T(), err)
}
func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() {
// Function definition: ConvertEndpointsToZones(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error)
// Create a new provider to run tests against
p := &PDNSProvider{
client: &PDNSAPIClientStubEmptyZones{},
}
// Check inserting endpoints from a single zone
zlist, err := p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check deleting endpoints from a single zone
zlist, err = p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsDelete)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimpleDelete}, zlist)
// Check endpoints from multiple zones #1
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch, ZoneEmptyToSimplePatch2}, zlist)
// Check endpoints from multiple zones #2
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones2, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch, ZoneEmptyToSimplePatch3}, zlist)
// Check endpoints from multiple zones where some endpoints which don't exist
zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithNoExist, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist)
// Check endpoints from a zone that does not exist
zlist, err = p.ConvertEndpointsToZones(endpointsNonexistantZone, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{}, zlist)
// Check endpoints that match multiple zones (one longer than other), is assigned to the right zone
zlist, err = p.ConvertEndpointsToZones(endpointsLongRecord, PdnsReplace)
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToLongPatch}, zlist)
// Check endpoints of type CNAME always have their target records end with a dot.
zlist, err = p.ConvertEndpointsToZones(endpointsMixedRecords, PdnsReplace)
assert.Nil(suite.T(), err)
for _, z := range zlist {
for _, rs := range z.Rrsets {
if "CNAME" == rs.Type_ {
for _, r := range rs.Records {
assert.Equal(suite.T(), uint8(0x2e), r.Content[len(r.Content)-1])
}
}
}
}
}
func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
// Function definition: mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error
// Create a new provider to run tests against
c := &PDNSAPIClientStubEmptyZones{}
p := &PDNSProvider{
client: c,
}
// Check inserting endpoints from a single zone
err := p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("REPLACE"))
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, c.patchedZones)
// Reset the "patchedZones"
c.patchedZones = []pgo.Zone{}
// Check deleting endpoints from a single zone
err = p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("DELETE"))
assert.Nil(suite.T(), err)
assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimpleDelete}, c.patchedZones)
// Check we fail correctly when patching fails for whatever reason
p = &PDNSProvider{
client: &PDNSAPIClientStubPatchZoneFailure{},
}
// Check inserting endpoints from a single zone
err = p.mutateRecords(endpointsSimpleRecord, pdnsChangeType("REPLACE"))
assert.NotNil(suite.T(), err)
}
func TestNewPDNSProviderTestSuite(t *testing.T) {
suite.Run(t, new(NewPDNSProviderTestSuite))
}

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

@ -49,7 +49,7 @@ func testNoopRecords(t *testing.T) {
providerRecords := []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "example-lb.com",
Targets: endpoint.Targets{"example-lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
}
@ -71,19 +71,19 @@ func testNoopApplyChanges(t *testing.T) {
providerRecords := []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "old-lb.com",
Targets: endpoint.Targets{"old-lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
}
expectedUpdate := []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "new-example-lb.com",
Targets: endpoint.Targets{"new-example-lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "new-record.org",
Target: "new-lb.org",
Targets: endpoint.Targets{"new-lb.org"},
RecordType: endpoint.RecordTypeCNAME,
},
}
@ -98,7 +98,7 @@ func testNoopApplyChanges(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "lb.com",
Targets: endpoint.Targets{"lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
},
@ -110,21 +110,21 @@ func testNoopApplyChanges(t *testing.T) {
Create: []*endpoint.Endpoint{
{
DNSName: "new-record.org",
Target: "new-lb.org",
Targets: endpoint.Targets{"new-lb.org"},
RecordType: endpoint.RecordTypeCNAME,
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "new-example-lb.com",
Targets: endpoint.Targets{"new-example-lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
},
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "old-lb.com",
Targets: endpoint.Targets{"old-lb.com"},
RecordType: endpoint.RecordTypeCNAME,
},
},

View File

@ -66,7 +66,8 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
endpoints = append(endpoints, record)
continue
}
labels, err := endpoint.NewLabelsFromString(record.Target)
// We simply assume that TXT records for the registry will always have only one target.
labels, err := endpoint.NewLabelsFromString(record.Targets[0])
if err == endpoint.ErrInvalidHeritage {
//if no heritage is found or it is invalid
//case when value of txt record cannot be identified
@ -104,12 +105,12 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
}
for _, r := range filteredChanges.Create {
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
filteredChanges.Create = append(filteredChanges.Create, txt)
}
for _, r := range filteredChanges.Delete {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
// when we delete TXT records for which value has changed (due to new label) this would still work because
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
@ -118,12 +119,12 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
// make sure TXT records are consistently updated as well
for _, r := range filteredChanges.UpdateNew {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
}
// make sure TXT records are consistently updated as well
for _, r := range filteredChanges.UpdateOld {
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
// when we updateOld TXT records for which value has changed (due to new label) this would still work because
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt)

View File

@ -82,7 +82,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
expectedRecords := []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Target: "foo.loadbalancer.com",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -90,7 +90,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
{
DNSName: "bar.test-zone.example.org",
Target: "my-domain.com",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
@ -98,7 +98,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
{
DNSName: "txt.bar.test-zone.example.org",
Target: "baz.test-zone.example.org",
Targets: endpoint.Targets{"baz.test-zone.example.org"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -106,7 +106,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
{
DNSName: "qux.test-zone.example.org",
Target: "random",
Targets: endpoint.Targets{"random"},
RecordType: endpoint.RecordTypeTXT,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -114,7 +114,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
{
DNSName: "tar.test-zone.example.org",
Target: "tar.loadbalancer.com",
Targets: endpoint.Targets{"tar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner-2",
@ -122,7 +122,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
{
DNSName: "foobar.test-zone.example.org",
Target: "foobar.loadbalancer.com",
Targets: endpoint.Targets{"foobar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -155,7 +155,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
expectedRecords := []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Target: "foo.loadbalancer.com",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -163,7 +163,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
{
DNSName: "bar.test-zone.example.org",
Target: "my-domain.com",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -171,7 +171,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
{
DNSName: "txt.bar.test-zone.example.org",
Target: "baz.test-zone.example.org",
Targets: endpoint.Targets{"baz.test-zone.example.org"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
@ -180,7 +180,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
{
DNSName: "qux.test-zone.example.org",
Target: "random",
Targets: endpoint.Targets{"random"},
RecordType: endpoint.RecordTypeTXT,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -188,7 +188,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
{
DNSName: "tar.test-zone.example.org",
Target: "tar.loadbalancer.com",
Targets: endpoint.Targets{"tar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
@ -196,7 +196,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
{
DNSName: "foobar.test-zone.example.org",
Target: "foobar.loadbalancer.com",
Targets: endpoint.Targets{"foobar.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
@ -354,13 +354,13 @@ helper methods
*/
func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, target, recordType)
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
return e
}
func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource string) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, target, recordType)
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
e.Labels[endpoint.ResourceLabelKey] = resource
return e

View File

@ -56,10 +56,10 @@ func legacyEndpointsFromMateService(svc *v1.Service) []*endpoint.Endpoint {
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, lb.IP))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeCNAME, lb.Hostname))
}
}
@ -88,10 +88,10 @@ func legacyEndpointsFromMoleculeService(svc *v1.Service) []*endpoint.Endpoint {
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, endpoint.RecordTypeA))
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, lb.IP))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, endpoint.RecordTypeCNAME))
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeCNAME, lb.Hostname))
}
}
}

65
source/connector.go Normal file
View File

@ -0,0 +1,65 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"encoding/gob"
"net"
"time"
"github.com/kubernetes-incubator/external-dns/endpoint"
log "github.com/sirupsen/logrus"
)
const (
dialTimeout = 30 * time.Second
)
// connectorSource is an implementation of Source that provides endpoints by connecting
// to a remote tcp server. The encoding/decoding is done using encoder/gob package.
type connectorSource struct {
remoteServer string
}
// NewConnectorSource creates a new connectorSource with the given config.
func NewConnectorSource(remoteServer string) (Source, error) {
return &connectorSource{
remoteServer: remoteServer,
}, nil
}
// Endpoints returns endpoint objects.
func (cs *connectorSource) Endpoints() ([]*endpoint.Endpoint, error) {
endpoints := []*endpoint.Endpoint{}
conn, err := net.DialTimeout("tcp", cs.remoteServer, dialTimeout)
if err != nil {
log.Errorf("Connection error: %v", err)
return nil, err
}
defer conn.Close()
decoder := gob.NewDecoder(conn)
if err := decoder.Decode(&endpoints); err != nil {
log.Errorf("Decode error: %v", err)
return nil, err
}
log.Debugf("Recieved endpoints: %#v", endpoints)
return endpoints, nil
}

137
source/connector_test.go Normal file
View File

@ -0,0 +1,137 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"encoding/gob"
"net"
"testing"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ConnectorSuite struct {
suite.Suite
}
func (suite *ConnectorSuite) SetupTest() {
}
func startServerToServeTargets(t *testing.T, server string, endpoints []*endpoint.Endpoint) {
ln, err := net.Listen("tcp", server)
if err != nil {
t.Fatal(err)
}
go func() {
conn, err := ln.Accept()
if err != nil {
ln.Close()
return
}
enc := gob.NewEncoder(conn)
enc.Encode(endpoints)
ln.Close()
}()
t.Logf("Server listening on %s", server)
}
func TestConnectorSource(t *testing.T) {
suite.Run(t, new(ConnectorSuite))
t.Run("Interface", testConnectorSourceImplementsSource)
t.Run("Endpoints", testConnectorSourceEndpoints)
}
// testConnectorSourceImplementsSource tests that connectorSource is a valid Source.
func testConnectorSourceImplementsSource(t *testing.T) {
assert.Implements(t, (*Source)(nil), new(connectorSource))
}
// testConnectorSourceEndpoints tests that NewConnectorSource doesn't return an error.
func testConnectorSourceEndpoints(t *testing.T) {
for _, ti := range []struct {
title string
serverListenAddress string
serverAddress string
expected []*endpoint.Endpoint
expectError bool
}{
{
title: "invalid remote server",
serverListenAddress: "",
serverAddress: "localhost:8091",
expectError: true,
},
{
title: "valid remote server with no endpoints",
serverListenAddress: "127.0.0.1:8080",
serverAddress: "127.0.0.1:8080",
expectError: false,
},
{
title: "valid remote server",
serverListenAddress: "127.0.0.1:8081",
serverAddress: "127.0.0.1:8081",
expected: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 180,
},
},
expectError: false,
},
{
title: "valid remote server with multiple endpoints",
serverListenAddress: "127.0.0.1:8082",
serverAddress: "127.0.0.1:8082",
expected: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 180,
},
{DNSName: "xyz.example.org",
Targets: endpoint.Targets{"abc.example.org"},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 180,
},
},
expectError: false,
},
} {
t.Run(ti.title, func(t *testing.T) {
if ti.serverListenAddress != "" {
startServerToServeTargets(t, ti.serverListenAddress, ti.expected)
}
cs, _ := NewConnectorSource(ti.serverAddress)
endpoints, err := cs.Endpoints()
if ti.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
// Validate returned endpoints against expected endpoints.
validateEndpoints(t, endpoints, ti.expected)
})
}
}

View File

@ -43,7 +43,7 @@ func (ms *dedupSource) Endpoints() ([]*endpoint.Endpoint, error) {
}
for _, ep := range endpoints {
identifier := ep.DNSName + " / " + ep.Target
identifier := ep.DNSName + " / " + ep.Targets.String()
if _, ok := collected[identifier]; ok {
log.Debugf("Removing duplicate endpoint %s", ep)

View File

@ -40,53 +40,53 @@ func testDedupEndpoints(t *testing.T) {
{
"one endpoint returns one endpoint",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
"two different endpoints return two endpoints",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "4.5.6.7"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"4.5.6.7"}},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "4.5.6.7"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"4.5.6.7"}},
},
},
{
"two endpoints with same dnsname and different targets return two endpoints",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "4.5.6.7"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"4.5.6.7"}},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "4.5.6.7"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"4.5.6.7"}},
},
},
{
"two endpoints with different dnsname and same target return two endpoints",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
"two endpoints with same dnsname and same target return one endpoint",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
},
} {

View File

@ -68,8 +68,8 @@ func (sc *fakeSource) Endpoints() ([]*endpoint.Endpoint, error) {
func (sc *fakeSource) generateEndpoint() (*endpoint.Endpoint, error) {
ep := endpoint.NewEndpoint(
generateDNSName(4, sc.dnsName),
generateIPAddress(),
endpoint.RecordTypeA,
generateIPAddress(),
)
return ep, nil

View File

@ -60,7 +60,7 @@ func TestFakeEndpointsResolveToIPAddresses(t *testing.T) {
endpoints := generateTestEndpoints()
for _, e := range endpoints {
ip := net.ParseIP(e.Target)
ip := net.ParseIP(e.Targets[0])
if ip == nil {
t.Error(e)

View File

@ -19,6 +19,7 @@ package source
import (
"bytes"
"fmt"
"sort"
"strings"
"text/template"
@ -37,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
@ -59,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
}
@ -92,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 {
@ -109,61 +118,59 @@ func (sc *ingressSource) Endpoints() ([]*endpoint.Endpoint, error) {
endpoints = append(endpoints, ingEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
// get endpoints from optional "target" annotation
// Returns empty endpoints array if none are found.
func getEndpointsFromTargetAnnotation(ing *v1beta1.Ingress, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
func getTargetsFromTargetAnnotation(ing *v1beta1.Ingress) endpoint.Targets {
var targets endpoint.Targets
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, exists := ing.Annotations[targetAnnotationKey]
if exists {
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
// splits the hostname annotation and removes the trailing periods
targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, targetHostname, suitableType(targetHostname), ttl))
targets = append(targets, targetHostname)
}
}
return endpoints
return targets
}
func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
// 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()
endpoints = getEndpointsFromTargetAnnotation(ing, hostname)
if len(endpoints) != 0 {
return endpoints, nil
}
hostnames := buf.String()
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
for _, lb := range ing.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.IP, endpoint.RecordTypeA, ttl))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
}
targets := getTargetsFromTargetAnnotation(ing)
if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
}
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
}
@ -208,32 +215,83 @@ func (sc *ingressSource) setResourceLabel(ingress v1beta1.Ingress, endpoints []*
func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
targets := getTargetsFromTargetAnnotation(ing)
if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
}
for _, rule := range ing.Spec.Rules {
if rule.Host == "" {
continue
}
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl)...)
}
annotationEndpoints := getEndpointsFromTargetAnnotation(ing, rule.Host)
if len(annotationEndpoints) != 0 {
endpoints = append(endpoints, annotationEndpoints...)
continue
}
ttl, err := getTTLFromAnnotations(ing.Annotations)
if err != nil {
log.Warn(err)
}
for _, lb := range ing.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rule.Host, lb.IP, endpoint.RecordTypeA, ttl))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rule.Host, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
}
}
hostnameList := getHostnamesFromAnnotations(ing.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...)
}
return endpoints
}
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
var aTargets endpoint.Targets
var cnameTargets endpoint.Targets
for _, t := range targets {
switch suitableType(t) {
case endpoint.RecordTypeA:
aTargets = append(aTargets, t)
default:
cnameTargets = append(cnameTargets, t)
}
}
if len(aTargets) > 0 {
epA := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: aTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epA)
}
if len(cnameTargets) > 0 {
epCNAME := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: cnameTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epCNAME)
}
return endpoints
}
func targetsFromIngressStatus(status v1beta1.IngressStatus) endpoint.Targets {
var targets endpoint.Targets
for _, lb := range status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
return targets
}

View File

@ -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)
@ -134,7 +148,7 @@ func testEndpointsFromIngress(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "foo.bar",
Target: "lb.com",
Targets: endpoint.Targets{"lb.com"},
},
},
},
@ -147,7 +161,7 @@ func testEndpointsFromIngress(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "foo.bar",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
@ -161,19 +175,11 @@ func testEndpointsFromIngress(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "foo.bar",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"},
},
{
DNSName: "foo.bar",
Target: "127.0.0.1",
},
{
DNSName: "foo.bar",
Target: "elb.com",
},
{
DNSName: "foo.bar",
Target: "alb.com",
Targets: endpoint.Targets{"elb.com", "alb.com"},
},
},
},
@ -212,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",
@ -244,11 +251,11 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "new.org",
Target: "lb.com",
Targets: endpoint.Targets{"lb.com"},
},
},
},
@ -272,11 +279,11 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "new.org",
Target: "lb.com",
Targets: endpoint.Targets{"lb.com"},
},
},
},
@ -300,7 +307,7 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
@ -322,7 +329,7 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
@ -379,7 +386,7 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
@ -417,7 +424,7 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
@ -455,11 +462,11 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "fake1.ext-dns.test.com",
Target: "8.8.8.8",
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "fake1.ext-dns.test.com",
Target: "elb.com",
Targets: endpoint.Targets{"elb.com"},
},
},
fqdnTemplate: "{{.Name}}.ext-dns.test.com",
@ -481,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: "",
@ -516,21 +600,108 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "example2.org",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "example3.org",
Target: "1.2.3.4",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
},
},
{
title: "ingress rules with hostname annotation",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{
hostnameAnnotationKey: "dns-through-hostname.com",
},
dnsnames: []string{"example.org"},
ips: []string{"1.2.3.4"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "dns-through-hostname.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
},
},
{
title: "ingress rules with hostname annotation having multiple hostnames",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{
hostnameAnnotationKey: "dns-through-hostname.com, another-dns-through-hostname.com",
},
dnsnames: []string{"example.org"},
ips: []string{"1.2.3.4"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "dns-through-hostname.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
{
DNSName: "another-dns-through-hostname.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
},
},
{
title: "ingress rules with hostname and target annotation",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
annotations: map[string]string{
hostnameAnnotationKey: "dns-through-hostname.com",
targetAnnotationKey: "ingress-target.com",
},
dnsnames: []string{"example.org"},
ips: []string{},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "dns-through-hostname.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
},
},
{
title: "ingress rules with annotation and custom TTL",
targetNamespace: "",
@ -559,12 +730,12 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordTTL: endpoint.TTL(6),
},
{
DNSName: "example2.org",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordTTL: endpoint.TTL(1),
},
},
@ -606,17 +777,17 @@ func testIngressEndpoints(t *testing.T) {
expected: []*endpoint.Endpoint{
{
DNSName: "fake1.ext-dns.test.com",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "fake2.ext-dns.test.com",
Target: "ingress-target.com",
Targets: endpoint.Targets{"ingress-target.com"},
RecordType: endpoint.RecordTypeCNAME,
},
{
DNSName: "fake3.ext-dns.test.com",
Target: "1.2.3.4",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
},
},
@ -635,6 +806,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

@ -40,8 +40,8 @@ func testMultiSourceImplementsSource(t *testing.T) {
// testMultiSourceEndpoints tests merged endpoints from children are returned.
func testMultiSourceEndpoints(t *testing.T) {
foo := &endpoint.Endpoint{DNSName: "foo", Target: "8.8.8.8"}
bar := &endpoint.Endpoint{DNSName: "bar", Target: "8.8.4.4"}
foo := &endpoint.Endpoint{DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}}
bar := &endpoint.Endpoint{DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}}
for _, tc := range []struct {
title string

View File

@ -19,6 +19,7 @@ package source
import (
"bytes"
"fmt"
"sort"
"strings"
"text/template"
@ -32,6 +33,10 @@ import (
"github.com/kubernetes-incubator/external-dns/endpoint"
)
const (
defaultTargetsCapacity = 10
)
// serviceSource is an implementation of Source for Kubernetes service objects.
// It will find all services that are under our jurisdiction, i.e. annotated
// desired hostname and matching or no controller annotation. For each of the
@ -42,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
@ -63,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
}
@ -102,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 {
@ -119,15 +132,17 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
endpoints = append(endpoints, svcEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
func (sc *serviceSource) extractHeadlessEndpoint(svc *v1.Service, hostname string) []*endpoint.Endpoint {
func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
pods, err := sc.client.CoreV1().Pods(svc.Namespace).List(metav1.ListOptions{LabelSelector: labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()})
if err != nil {
log.Errorf("List Pods of service[%s] error:%v", svc.GetName(), err)
return endpoints
@ -138,10 +153,15 @@ func (sc *serviceSource) extractHeadlessEndpoint(svc *v1.Service, hostname strin
if v.Spec.Hostname != "" {
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, v.Status.HostIP, endpoint.RecordTypeA))
if ttl.IsConfigured() {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, v.Status.PodIP))
} else {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.PodIP))
}
} else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
}
@ -152,15 +172,17 @@ func (sc *serviceSource) extractHeadlessEndpoint(svc *v1.Service, hostname strin
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
}
@ -169,13 +191,7 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
// Get the desired hostname of the service from the annotation.
hostnameAnnotation, exists := svc.Annotations[hostnameAnnotationKey]
if !exists {
return nil
}
hostnameList := strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
hostnameList := getHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
}
@ -221,54 +237,82 @@ func (sc *serviceSource) setResourceLabel(service v1.Service, endpoints []*endpo
}
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
hostname = strings.TrimSuffix(hostname, ".")
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
log.Warn(err)
}
epA := &endpoint.Endpoint{
RecordTTL: ttl,
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname,
}
epCNAME := &endpoint.Endpoint{
RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname,
}
var endpoints []*endpoint.Endpoint
var targets endpoint.Targets
switch svc.Spec.Type {
case v1.ServiceTypeLoadBalancer:
endpoints = append(endpoints, extractLoadBalancerEndpoints(svc, hostname)...)
targets = append(targets, extractLoadBalancerTargets(svc)...)
case v1.ServiceTypeClusterIP:
if sc.publishInternal {
endpoints = append(endpoints, extractServiceIps(svc, hostname)...)
targets = append(targets, extractServiceIps(svc)...)
}
if svc.Spec.ClusterIP == v1.ClusterIPNone {
endpoints = append(endpoints, sc.extractHeadlessEndpoint(svc, hostname)...)
endpoints = append(endpoints, sc.extractHeadlessEndpoints(svc, hostname, ttl)...)
}
}
for _, t := range targets {
if suitableType(t) == endpoint.RecordTypeA {
epA.Targets = append(epA.Targets, t)
}
if suitableType(t) == endpoint.RecordTypeCNAME {
epCNAME.Targets = append(epCNAME.Targets, t)
}
}
if len(epA.Targets) > 0 {
endpoints = append(endpoints, epA)
}
if len(epCNAME.Targets) > 0 {
endpoints = append(endpoints, epCNAME)
}
return endpoints
}
func extractServiceIps(svc *v1.Service, hostname string) []*endpoint.Endpoint {
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
log.Warn(err)
}
func extractServiceIps(svc *v1.Service) endpoint.Targets {
if svc.Spec.ClusterIP == v1.ClusterIPNone {
log.Debugf("Unable to associate %s headless service with a Cluster IP", svc.Name)
return []*endpoint.Endpoint{}
return endpoint.Targets{}
}
return []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(hostname, svc.Spec.ClusterIP, endpoint.RecordTypeA, ttl)}
return endpoint.Targets{svc.Spec.ClusterIP}
}
func extractLoadBalancerEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
var targets endpoint.Targets
ttl, err := getTTLFromAnnotations(svc.Annotations)
if err != nil {
log.Warn(err)
}
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
//TODO(ideahitme): consider retrieving record type from resource annotation instead of empty
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.IP, endpoint.RecordTypeA, ttl))
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(hostname, lb.Hostname, endpoint.RecordTypeCNAME, ttl))
targets = append(targets, lb.Hostname)
}
}
return endpoints
return targets
}

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,
)
@ -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.",
@ -191,7 +196,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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.",
@ -229,8 +280,8 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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",
@ -250,8 +302,8 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -264,6 +316,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -271,7 +324,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"lb.example.com"}, // Kubernetes omits the trailing dot
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "lb.example.com"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}},
},
false,
},
@ -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
@ -291,8 +345,8 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "lb.example.com"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}},
},
false,
},
@ -305,6 +359,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
controllerAnnotationKey: controllerAnnotationValue,
@ -313,7 +368,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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.",
@ -352,7 +409,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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.",
@ -390,7 +449,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -403,6 +462,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -411,7 +471,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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.",
@ -470,7 +533,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -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.",
@ -512,7 +577,7 @@ func testServiceSourceEndpoints(t *testing.T) {
false,
},
{
"multiple external entrypoints return multiple endpoints",
"multiple external entrypoints return a single endpoint with multiple targets",
"",
"",
"testing",
@ -520,6 +585,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -527,8 +593,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4", "8.8.8.8"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "8.8.8.8"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}},
},
false,
},
@ -541,6 +606,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
"zalando.org/dnsname": "foo.example.org.",
@ -559,6 +625,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"mate",
"",
false,
map[string]string{},
map[string]string{
"zalando.org/dnsname": "foo.example.org.",
@ -566,7 +633,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -579,6 +646,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"molecule",
"",
false,
map[string]string{
"dns": "route53",
},
@ -588,8 +656,8 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "bar.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -602,13 +670,14 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"{{.Name}}.bar.example.com",
false,
map[string]string{},
map[string]string{},
"",
[]string{"1.2.3.4", "elb.com"},
[]*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", Target: "1.2.3.4"},
{DNSName: "foo.bar.example.com", Target: "elb.com"},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"elb.com"}},
},
false,
},
@ -621,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.",
@ -628,8 +698,8 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4", "elb.com"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "elb.com"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"elb.com"}},
},
false,
},
@ -642,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.",
@ -649,7 +720,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "mate.example.org", Target: "1.2.3.4"},
{DNSName: "mate.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -662,6 +733,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"{{.Calibre}}.bar.example.com",
false,
map[string]string{},
map[string]string{},
"",
@ -678,6 +750,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -685,7 +758,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
false,
},
@ -698,6 +771,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -706,7 +780,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
false,
},
@ -719,6 +793,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -727,7 +802,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(10)},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)},
},
false,
},
@ -740,6 +815,7 @@ func testServiceSourceEndpoints(t *testing.T) {
v1.ServiceTypeLoadBalancer,
"",
"",
false,
map[string]string{},
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
@ -748,7 +824,7 @@ func testServiceSourceEndpoints(t *testing.T) {
"",
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4", RecordTTL: endpoint.TTL(0)},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
false,
},
@ -794,6 +870,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
tc.combineFQDNAndAnnotation,
tc.compatibility,
false,
)
@ -846,7 +923,7 @@ func TestClusterIpServices(t *testing.T) {
"1.2.3.4",
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
@ -864,7 +941,7 @@ func TestClusterIpServices(t *testing.T) {
"4.5.6.7",
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", Target: "4.5.6.7"},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"4.5.6.7"}},
},
false,
},
@ -926,6 +1003,7 @@ func TestClusterIpServices(t *testing.T) {
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
false,
tc.compatibility,
true,
)
@ -957,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
@ -988,8 +1066,36 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Target: "1.1.1.1"},
{DNSName: "foo-1.service.example.org", Target: "1.1.1.1"},
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
{
"annotated Headless services return endpoints with TTL for each selected Pod",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
ttlAnnotationKey: "1",
},
v1.ClusterIPNone,
"1.1.1.1",
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
},
false,
},
@ -1015,7 +1121,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodFailed},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Target: "1.1.1.1"},
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
@ -1041,8 +1147,8 @@ func TestHeadlessServices(t *testing.T) {
[]string{"", ""},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Target: "1.1.1.1"},
{DNSName: "service.example.org", Target: "1.1.1.1"},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
@ -1081,8 +1187,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],
},
}
@ -1096,6 +1202,7 @@ func TestHeadlessServices(t *testing.T) {
tc.targetNamespace,
"",
tc.fqdnTemplate,
false,
tc.compatibility,
true,
)
@ -1138,7 +1245,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

@ -39,8 +39,8 @@ func validateEndpoint(t *testing.T, endpoint, expected *endpoint.Endpoint) {
t.Errorf("expected %s, got %s", expected.DNSName, endpoint.DNSName)
}
if endpoint.Target != expected.Target {
t.Errorf("expected %s, got %s", expected.Target, endpoint.Target)
if !endpoint.Targets.Same(expected.Targets) {
t.Errorf("expected %s, got %s", expected.Targets, endpoint.Targets)
}
if endpoint.RecordTTL != expected.RecordTTL {

View File

@ -21,6 +21,7 @@ import (
"math"
"net"
"strconv"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
@ -64,6 +65,15 @@ func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error)
return endpoint.TTL(ttlValue), nil
}
func getHostnamesFromAnnotations(annotations map[string]string) []string {
hostnameAnnotation, exists := annotations[hostnameAnnotationKey]
if !exists {
return nil
}
return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
}
// suitableType returns the DNS resource record type suitable for the target.
// In this case type A for IPs and type CNAME for everything else.
func suitableType(target string) string {

View File

@ -35,11 +35,13 @@ 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
ConnectorServer string
}
// ClientGenerator provides clients
@ -87,15 +89,17 @@ 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)
case "connector":
return NewConnectorSource(cfg.ConnectorServer)
}
return nil, ErrSourceNotFound
}

View File

@ -1,11 +0,0 @@
sudo: false
language: go
go:
- 1.6
- 1.7
install:
- go get -v cloud.google.com/go/...
script:
- openssl aes-256-cbc -K $encrypted_912ff8fa81ad_key -iv $encrypted_912ff8fa81ad_iv -in key.json.enc -out key.json -d
- GCLOUD_TESTS_GOLANG_PROJECT_ID="dulcet-port-762" GCLOUD_TESTS_GOLANG_KEY="$(pwd)/key.json"
go test -race -v cloud.google.com/go/...

15
vendor/cloud.google.com/go/AUTHORS generated vendored
View File

@ -1,15 +0,0 @@
# This is the official list of cloud authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as:
# Name or Organization <email address>
# The email address is not required for organizations.
Filippo Valsorda <hi@filippo.io>
Google Inc.
Ingo Oeser <nightlyone@googlemail.com>
Palm Stone Games, Inc.
Paweł Knap <pawelknap88@gmail.com>
Péter Szilágyi <peterke@gmail.com>
Tyler Treat <ttreat31@gmail.com>

View File

@ -1,126 +0,0 @@
# Contributing
1. Sign one of the contributor license agreements below.
1. `go get golang.org/x/review/git-codereview` to install the code reviewing tool.
1. Get the cloud package by running `go get -d cloud.google.com/go`.
1. If you have already checked out the source, make sure that the remote git
origin is https://code.googlesource.com/gocloud:
git remote set-url origin https://code.googlesource.com/gocloud
1. Make sure your auth is configured correctly by visiting
https://code.googlesource.com, clicking "Generate Password", and following
the directions.
1. Make changes and create a change by running `git codereview change <name>`,
provide a commit message, and use `git codereview mail` to create a Gerrit CL.
1. Keep amending to the change and mail as your receive feedback.
## Integration Tests
In addition to the unit tests, you may run the integration test suite.
To run the integrations tests, creating and configuration of a project in the
Google Developers Console is required.
After creating a project, you must [create a service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount).
Ensure the project-level **Owner** [IAM role](console.cloud.google.com/iam-admin/iam/project)
(or **Editor** and **Logs Configuration Writer** roles) are added to the
service account.
Once you create a project, set the following environment variables to be able to
run the against the actual APIs.
- **GCLOUD_TESTS_GOLANG_PROJECT_ID**: Developers Console project's ID (e.g. bamboo-shift-455)
- **GCLOUD_TESTS_GOLANG_KEY**: The path to the JSON key file.
Install the [gcloud command-line tool][gcloudcli] to your machine and use it
to create the indexes used in the datastore integration tests with indexes
found in `datastore/testdata/index.yaml`:
From the project's root directory:
``` sh
# Set the default project in your env
$ gcloud config set project $GCLOUD_TESTS_GOLANG_PROJECT_ID
# Authenticate the gcloud tool with your account
$ gcloud auth login
# Create the indexes
$ gcloud preview datastore create-indexes datastore/testdata/index.yaml
```
The Sink integration tests in preview/logging require a Google Cloud storage
bucket with the same name as your test project, and with the Stackdriver Logging
service account as owner:
``` sh
$ gsutil mb gs://$GCLOUD_TESTS_GOLANG_PROJECT_ID
$ gsutil acl ch -g cloud-logs@google.com:O gs://$GCLOUD_TESTS_GOLANG_PROJECT_ID
```
Once you've set the environment variables, you can run the integration tests by
running:
``` sh
$ go test -v cloud.google.com/go/...
```
## Contributor License Agreements
Before we can accept your pull requests you'll need to sign a Contributor
License Agreement (CLA):
- **If you are an individual writing original source code** and **you own the
- intellectual property**, then you'll need to sign an [individual CLA][indvcla].
- **If you work for a company that wants to allow you to contribute your work**,
then you'll need to sign a [corporate CLA][corpcla].
You can sign these electronically (just scroll to the bottom). After that,
we'll be able to accept your pull requests.
## Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
[gcloudcli]: https://developers.google.com/cloud/sdk/gcloud/
[indvcla]: https://developers.google.com/open-source/cla/individual
[corpcla]: https://developers.google.com/open-source/cla/corporate

View File

@ -1,34 +0,0 @@
# People who have agreed to one of the CLAs and can contribute patches.
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees are listed here
# but not in AUTHORS, because Google holds the copyright.
#
# https://developers.google.com/open-source/cla/individual
# https://developers.google.com/open-source/cla/corporate
#
# Names should be added to this file as:
# Name <email address>
# Keep the list alphabetically sorted.
Andreas Litt <andreas.litt@gmail.com>
Andrew Gerrand <adg@golang.org>
Brad Fitzpatrick <bradfitz@golang.org>
Burcu Dogan <jbd@google.com>
Dave Day <djd@golang.org>
David Sansome <me@davidsansome.com>
David Symonds <dsymonds@golang.org>
Filippo Valsorda <hi@filippo.io>
Glenn Lewis <gmlewis@google.com>
Ingo Oeser <nightlyone@googlemail.com>
Johan Euphrosine <proppy@google.com>
Jonathan Amsterdam <jba@google.com>
Luna Duclos <luna.duclos@palmstonegames.com>
Michael McGreevy <mcgreevy@golang.org>
Omar Jarjur <ojarjur@google.com>
Paweł Knap <pawelknap88@gmail.com>
Péter Szilágyi <peterke@gmail.com>
Sarah Adams <shadams@google.com>
Toby Burress <kurin@google.com>
Tuo Shan <shantuo@google.com>
Tyler Treat <ttreat31@gmail.com>

202
vendor/cloud.google.com/go/LICENSE generated vendored
View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Google Inc.
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.

245
vendor/cloud.google.com/go/README.md generated vendored
View File

@ -1,245 +0,0 @@
# Google Cloud for Go
[![Build Status](https://travis-ci.org/GoogleCloudPlatform/google-cloud-go.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/google-cloud-go)
[![GoDoc](https://godoc.org/cloud.google.com/go?status.svg)](https://godoc.org/cloud.google.com/go)
``` go
import "cloud.google.com/go"
```
Go packages for Google Cloud Platform services.
**NOTE:** These packages are under development, and may occasionally make
backwards-incompatible changes.
**NOTE:** Github repo is a mirror of [https://code.googlesource.com/gocloud](https://code.googlesource.com/gocloud).
## News
_September 8, 2016_
* New clients for some of Google's Machine Learning APIs: Vision, Speech, and
Natural Language.
* Preview version of a new [Stackdriver Logging][cloud-logging] client in
[`cloud.google.com/go/preview/logging`](https://godoc.org/cloud.google.com/go/preview/logging).
This client uses gRPC as its transport layer, and supports log reading, sinks
and metrics. It will replace the current client at `cloud.google.com/go/logging` shortly.
## Supported APIs
Google API | Status | Package
-------------------------------|--------------|-----------------------------------------------------------
[Datastore][cloud-datastore] | beta | [`cloud.google.com/go/datastore`][cloud-datastore-ref]
[Storage][cloud-storage] | beta | [`cloud.google.com/go/storage`][cloud-storage-ref]
[Pub/Sub][cloud-pubsub] | experimental | [`cloud.google.com/go/pubsub`][cloud-pubsub-ref]
[Bigtable][cloud-bigtable] | beta | [`cloud.google.com/go/bigtable`][cloud-bigtable-ref]
[BigQuery][cloud-bigquery] | experimental | [`cloud.google.com/go/bigquery`][cloud-bigquery-ref]
[Logging][cloud-logging] | experimental | [`cloud.google.com/go/logging`][cloud-logging-ref]
[Vision][cloud-vision] | experimental | [`cloud.google.com/go/vision`][cloud-vision-ref]
[Language][cloud-language] | experimental | [`cloud.google.com/go/language/apiv1beta1`][cloud-language-ref]
[Speech][cloud-speech] | experimental | [`cloud.google.com/go/speech/apiv1beta`][cloud-speech-ref]
> **Experimental status**: the API is still being actively developed. As a
> result, it might change in backward-incompatible ways and is not recommended
> for production use.
>
> **Beta status**: the API is largely complete, but still has outstanding
> features and bugs to be addressed. There may be minor backwards-incompatible
> changes where necessary.
>
> **Stable status**: the API is mature and ready for production use. We will
> continue addressing bugs and feature requests.
Documentation and examples are available at
https://godoc.org/cloud.google.com/go
Visit or join the
[google-api-go-announce group](https://groups.google.com/forum/#!forum/google-api-go-announce)
for updates on these packages.
## Go Versions Supported
We support the two most recent major versions of Go. If Google App Engine uses
an older version, we support that as well. You can see which versions are
currently supported by looking at the lines following `go:` in
[`.travis.yml`](.travis.yml).
## Authorization
By default, each API will use [Google Application Default Credentials][default-creds]
for authorization credentials used in calling the API endpoints. This will allow your
application to run in many environments without requiring explicit configuration.
Manually-configured authorization can be achieved using the
[`golang.org/x/oauth2`](https://godoc.org/golang.org/x/oauth2) package to
create an `oauth2.TokenSource`. This token source can be passed to the `NewClient`
function for the relevant API using a
[`option.WithTokenSource`](https://godoc.org/google.golang.org/api/option#WithTokenSource)
option.
## Google Cloud Datastore [![GoDoc](https://godoc.org/cloud.google.com/go/datastore?status.svg)](https://godoc.org/cloud.google.com/go/datastore)
[Google Cloud Datastore][cloud-datastore] ([docs][cloud-datastore-docs]) is a fully-
managed, schemaless database for storing non-relational data. Cloud Datastore
automatically scales with your users and supports ACID transactions, high availability
of reads and writes, strong consistency for reads and ancestor queries, and eventual
consistency for all other queries.
Follow the [activation instructions][cloud-datastore-activation] to use the Google
Cloud Datastore API with your project.
First create a `datastore.Client` to use throughout your application:
```go
client, err := datastore.NewClient(ctx, "my-project-id")
if err != nil {
log.Fatalln(err)
}
```
Then use that client to interact with the API:
```go
type Post struct {
Title string
Body string `datastore:",noindex"`
PublishedAt time.Time
}
keys := []*datastore.Key{
datastore.NewKey(ctx, "Post", "post1", 0, nil),
datastore.NewKey(ctx, "Post", "post2", 0, nil),
}
posts := []*Post{
{Title: "Post 1", Body: "...", PublishedAt: time.Now()},
{Title: "Post 2", Body: "...", PublishedAt: time.Now()},
}
if _, err := client.PutMulti(ctx, keys, posts); err != nil {
log.Fatal(err)
}
```
## Google Cloud Storage [![GoDoc](https://godoc.org/cloud.google.com/go/storage?status.svg)](https://godoc.org/cloud.google.com/go/storage)
[Google Cloud Storage][cloud-storage] ([docs][cloud-storage-docs]) allows you to store
data on Google infrastructure with very high reliability, performance and availability,
and can be used to distribute large data objects to users via direct download.
https://godoc.org/cloud.google.com/go/storage
First create a `storage.Client` to use throughout your application:
```go
client, err := storage.NewClient(ctx)
if err != nil {
log.Fatal(err)
}
```
```go
// Read the object1 from bucket.
rc, err := client.Bucket("bucket").Object("object1").NewReader(ctx)
if err != nil {
log.Fatal(err)
}
defer rc.Close()
body, err := ioutil.ReadAll(rc)
if err != nil {
log.Fatal(err)
}
```
## Google Cloud Pub/Sub [![GoDoc](https://godoc.org/cloud.google.com/go/pubsub?status.svg)](https://godoc.org/cloud.google.com/go/pubsub)
[Google Cloud Pub/Sub][cloud-pubsub] ([docs][cloud-pubsub-docs]) allows you to connect
your services with reliable, many-to-many, asynchronous messaging hosted on Google's
infrastructure. Cloud Pub/Sub automatically scales as you need it and provides a foundation
for building your own robust, global services.
First create a `pubsub.Client` to use throughout your application:
```go
client, err := pubsub.NewClient(ctx, "project-id")
if err != nil {
log.Fatal(err)
}
```
```go
// Publish "hello world" on topic1.
topic := client.Topic("topic1")
msgIDs, err := topic.Publish(ctx, &pubsub.Message{
Data: []byte("hello world"),
})
if err != nil {
log.Fatal(err)
}
// Create an iterator to pull messages via subscription1.
it, err := client.Subscription("subscription1").Pull(ctx)
if err != nil {
log.Println(err)
}
defer it.Stop()
// Consume N messages from the iterator.
for i := 0; i < N; i++ {
msg, err := it.Next()
if err == pubsub.Done {
break
}
if err != nil {
log.Fatalf("Failed to retrieve message: %v", err)
}
fmt.Printf("Message %d: %s\n", i, msg.Data)
msg.Done(true) // Acknowledge that we've consumed the message.
}
```
## Contributing
Contributions are welcome. Please, see the
[CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/CONTRIBUTING.md)
document for details. We're using Gerrit for our code reviews. Please don't open pull
requests against this repo, new pull requests will be automatically closed.
Please note that this project is released with a Contributor Code of Conduct.
By participating in this project you agree to abide by its terms.
See [Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/CONTRIBUTING.md#contributor-code-of-conduct)
for more information.
[cloud-datastore]: https://cloud.google.com/datastore/
[cloud-datastore-ref]: https://godoc.org/cloud.google.com/go/datastore
[cloud-datastore-docs]: https://cloud.google.com/datastore/docs
[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate
[cloud-pubsub]: https://cloud.google.com/pubsub/
[cloud-pubsub-ref]: https://godoc.org/cloud.google.com/go/pubsub
[cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs
[cloud-storage]: https://cloud.google.com/storage/
[cloud-storage-ref]: https://godoc.org/cloud.google.com/go/storage
[cloud-storage-docs]: https://cloud.google.com/storage/docs/overview
[cloud-storage-create-bucket]: https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets
[cloud-bigtable]: https://cloud.google.com/bigtable/
[cloud-bigtable-ref]: https://godoc.org/cloud.google.com/go/bigtable
[cloud-bigquery]: https://cloud.google.com/bigquery/
[cloud-bigquery-ref]: https://godoc.org/cloud.google.com/go/bigquery
[cloud-logging]: https://cloud.google.com/logging/
[cloud-logging-ref]: https://godoc.org/cloud.google.com/go/logging
[cloud-vision]: https://cloud.google.com/vision/
[cloud-vision-ref]: https://godoc.org/cloud.google.com/go/vision
[cloud-language]: https://cloud.google.com/natural-language
[cloud-language-ref]: https://godoc.org/cloud.google.com/go/language/apiv1beta1
[cloud-speech]: https://cloud.google.com/speech
[cloud-speech-ref]: https://godoc.org/cloud.google.com/go/speech/apiv1beta1
[default-creds]: https://developers.google.com/identity/protocols/application-default-credentials

View File

@ -1,26 +0,0 @@
# This file configures AppVeyor (http://www.appveyor.com),
# a Windows-based CI service similar to Travis.
# Identifier for this run
version: "{build}"
# Clone the repo into this path, which conforms to the standard
# Go workspace structure.
clone_folder: c:\gopath\src\cloud.google.com\go
environment:
GOPATH: c:\gopath
install:
# Info for debugging.
- echo %PATH%
- go version
- go env
- go get -v -d -t ./...
# Provide a build script, or AppVeyor will call msbuild.
build_script:
- go install -v ./...
test_script:
- go test -short -v ./...

View File

@ -1,60 +0,0 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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 cloud_test
import (
"cloud.google.com/go/datastore"
"golang.org/x/net/context"
"google.golang.org/api/option"
)
func Example_applicationDefaultCredentials() {
ctx := context.Background()
// Use Google Application Default Credentials to authorize and authenticate the client.
// More information about Application Default Credentials and how to enable is at
// https://developers.google.com/identity/protocols/application-default-credentials.
//
// This is the recommended way of authorizing and authenticating.
//
// Note: The example uses the datastore client, but the same steps apply to
// the other client libraries underneath this package.
client, err := datastore.NewClient(ctx, "project-id")
if err != nil {
// TODO: handle error.
}
// Use the client.
_ = client
}
func Example_serviceAccountFile() {
// Warning: The better way to use service accounts is to set GOOGLE_APPLICATION_CREDENTIALS
// and use the Application Default Credentials.
ctx := context.Background()
// Use a JSON key file associated with a Google service account to
// authenticate and authorize.
// Go to https://console.developers.google.com/permissions/serviceaccounts to create
// and download a service account key for your project.
//
// Note: The example uses the datastore client, but the same steps apply to
// the other client libraries underneath this package.
client, err := datastore.NewClient(ctx,
"project-id",
option.WithServiceAccountFile("/path/to/service-account-key.json"))
if err != nil {
// TODO: handle error.
}
// Use the client.
_ = client
}

View File

@ -1,175 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
// TODO(mcgreevy): support dry-run mode when creating jobs.
import (
"fmt"
"google.golang.org/api/option"
"google.golang.org/api/transport"
"golang.org/x/net/context"
bq "google.golang.org/api/bigquery/v2"
)
const prodAddr = "https://www.googleapis.com/bigquery/v2/"
// A Source is a source of data for the Copy function.
type Source interface {
implementsSource()
}
// A Destination is a destination of data for the Copy function.
type Destination interface {
implementsDestination()
}
// An Option is an optional argument to Copy.
type Option interface {
implementsOption()
}
// A ReadSource is a source of data for the Read function.
type ReadSource interface {
implementsReadSource()
}
// A ReadOption is an optional argument to Read.
type ReadOption interface {
customizeRead(conf *pagingConf)
}
const Scope = "https://www.googleapis.com/auth/bigquery"
const userAgent = "gcloud-golang-bigquery/20160429"
// Client may be used to perform BigQuery operations.
type Client struct {
service service
projectID string
}
// NewClient constructs a new Client which can perform BigQuery operations.
// Operations performed via the client are billed to the specified GCP project.
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
o := []option.ClientOption{
option.WithEndpoint(prodAddr),
option.WithScopes(Scope),
option.WithUserAgent(userAgent),
}
o = append(o, opts...)
httpClient, endpoint, err := transport.NewHTTPClient(ctx, o...)
if err != nil {
return nil, fmt.Errorf("dialing: %v", err)
}
s, err := newBigqueryService(httpClient, endpoint)
if err != nil {
return nil, fmt.Errorf("constructing bigquery client: %v", err)
}
c := &Client{
service: s,
projectID: projectID,
}
return c, nil
}
// initJobProto creates and returns a bigquery Job proto.
// The proto is customized using any jobOptions in options.
// The list of Options is returned with the jobOptions removed.
func initJobProto(projectID string, options []Option) (*bq.Job, []Option) {
job := &bq.Job{}
var other []Option
for _, opt := range options {
if o, ok := opt.(jobOption); ok {
o.customizeJob(job, projectID)
} else {
other = append(other, opt)
}
}
return job, other
}
// Copy starts a BigQuery operation to copy data from a Source to a Destination.
func (c *Client) Copy(ctx context.Context, dst Destination, src Source, options ...Option) (*Job, error) {
switch dst := dst.(type) {
case *Table:
switch src := src.(type) {
case *GCSReference:
return c.load(ctx, dst, src, options)
case *Table:
return c.cp(ctx, dst, Tables{src}, options)
case Tables:
return c.cp(ctx, dst, src, options)
case *Query:
return c.query(ctx, dst, src, options)
}
case *GCSReference:
if src, ok := src.(*Table); ok {
return c.extract(ctx, dst, src, options)
}
}
return nil, fmt.Errorf("no Copy operation matches dst/src pair: dst: %T ; src: %T", dst, src)
}
// Query creates a query with string q. You may optionally set
// DefaultProjectID and DefaultDatasetID on the returned query before using it.
func (c *Client) Query(q string) *Query {
return &Query{Q: q, client: c}
}
// Read submits a query for execution and returns the results via an Iterator.
//
// Read uses a temporary table to hold the results of the query job.
//
// For more control over how a query is performed, don't use this method but
// instead pass the Query as a Source to Client.Copy, and call Read on the
// resulting Job.
func (q *Query) Read(ctx context.Context, options ...ReadOption) (*Iterator, error) {
dest := &Table{}
job, err := q.client.Copy(ctx, dest, q, WriteTruncate)
if err != nil {
return nil, err
}
return job.Read(ctx, options...)
}
// executeQuery submits a query for execution and returns the results via an Iterator.
func (c *Client) executeQuery(ctx context.Context, q *Query, options ...ReadOption) (*Iterator, error) {
dest := &Table{}
job, err := c.Copy(ctx, dest, q, WriteTruncate)
if err != nil {
return nil, err
}
return c.Read(ctx, job, options...)
}
// Dataset creates a handle to a BigQuery dataset in the client's project.
func (c *Client) Dataset(id string) *Dataset {
return c.DatasetInProject(c.projectID, id)
}
// DatasetInProject creates a handle to a BigQuery dataset in the specified project.
func (c *Client) DatasetInProject(projectID, datasetID string) *Dataset {
return &Dataset{
projectID: projectID,
id: datasetID,
service: c.service,
}
}

View File

@ -1,47 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import (
"fmt"
"golang.org/x/net/context"
bq "google.golang.org/api/bigquery/v2"
)
type copyOption interface {
customizeCopy(conf *bq.JobConfigurationTableCopy)
}
func (c *Client) cp(ctx context.Context, dst *Table, src Tables, options []Option) (*Job, error) {
job, options := initJobProto(c.projectID, options)
payload := &bq.JobConfigurationTableCopy{}
dst.customizeCopyDst(payload)
src.customizeCopySrc(payload)
for _, opt := range options {
o, ok := opt.(copyOption)
if !ok {
return nil, fmt.Errorf("option (%#v) not applicable to dst/src pair: dst: %T ; src: %T", opt, dst, src)
}
o.customizeCopy(payload)
}
job.Configuration = &bq.JobConfiguration{
Copy: payload,
}
return c.service.insertJob(ctx, job, c.projectID)
}

View File

@ -1,104 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import (
"reflect"
"testing"
"golang.org/x/net/context"
bq "google.golang.org/api/bigquery/v2"
)
func defaultCopyJob() *bq.Job {
return &bq.Job{
Configuration: &bq.JobConfiguration{
Copy: &bq.JobConfigurationTableCopy{
DestinationTable: &bq.TableReference{
ProjectId: "d-project-id",
DatasetId: "d-dataset-id",
TableId: "d-table-id",
},
SourceTables: []*bq.TableReference{
{
ProjectId: "s-project-id",
DatasetId: "s-dataset-id",
TableId: "s-table-id",
},
},
},
},
}
}
func TestCopy(t *testing.T) {
testCases := []struct {
dst *Table
src Tables
options []Option
want *bq.Job
}{
{
dst: &Table{
ProjectID: "d-project-id",
DatasetID: "d-dataset-id",
TableID: "d-table-id",
},
src: Tables{
{
ProjectID: "s-project-id",
DatasetID: "s-dataset-id",
TableID: "s-table-id",
},
},
want: defaultCopyJob(),
},
{
dst: &Table{
ProjectID: "d-project-id",
DatasetID: "d-dataset-id",
TableID: "d-table-id",
},
src: Tables{
{
ProjectID: "s-project-id",
DatasetID: "s-dataset-id",
TableID: "s-table-id",
},
},
options: []Option{CreateNever, WriteTruncate},
want: func() *bq.Job {
j := defaultCopyJob()
j.Configuration.Copy.CreateDisposition = "CREATE_NEVER"
j.Configuration.Copy.WriteDisposition = "WRITE_TRUNCATE"
return j
}(),
},
}
for _, tc := range testCases {
s := &testService{}
c := &Client{
service: s,
}
if _, err := c.Copy(context.Background(), tc.dst, tc.src, tc.options...); err != nil {
t.Errorf("err calling cp: %v", err)
continue
}
if !reflect.DeepEqual(s.Job, tc.want) {
t.Errorf("copying: got:\n%v\nwant:\n%v", s.Job, tc.want)
}
}
}

View File

@ -1,79 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import (
"reflect"
"testing"
"time"
"golang.org/x/net/context"
bq "google.golang.org/api/bigquery/v2"
)
type createTableRecorder struct {
conf *createTableConf
service
}
func (rec *createTableRecorder) createTable(ctx context.Context, conf *createTableConf) error {
rec.conf = conf
return nil
}
func TestCreateTableOptions(t *testing.T) {
s := &createTableRecorder{}
c := &Client{
projectID: "p",
service: s,
}
ds := c.Dataset("d")
table := ds.Table("t")
exp := time.Now()
q := "query"
if err := table.Create(context.Background(), TableExpiration(exp), ViewQuery(q)); err != nil {
t.Fatalf("err calling Table.Create: %v", err)
}
want := createTableConf{
projectID: "p",
datasetID: "d",
tableID: "t",
expiration: exp,
viewQuery: q,
}
if !reflect.DeepEqual(*s.conf, want) {
t.Errorf("createTableConf: got:\n%v\nwant:\n%v", *s.conf, want)
}
sc := Schema{fieldSchema("desc", "name", "STRING", false, true)}
if err := table.Create(context.Background(), TableExpiration(exp), sc); err != nil {
t.Fatalf("err calling Table.Create: %v", err)
}
want = createTableConf{
projectID: "p",
datasetID: "d",
tableID: "t",
expiration: exp,
// No need for an elaborate schema, that is tested in schema_test.go.
schema: &bq.TableSchema{
Fields: []*bq.TableFieldSchema{
bqTableFieldSchema("desc", "name", "STRING", "REQUIRED"),
},
},
}
if !reflect.DeepEqual(*s.conf, want) {
t.Errorf("createTableConf: got:\n%v\nwant:\n%v", *s.conf, want)
}
}

View File

@ -1,55 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import "golang.org/x/net/context"
// Dataset is a reference to a BigQuery dataset.
type Dataset struct {
projectID string
id string
service service
}
// ListTables returns a list of all the tables contained in the Dataset.
func (d *Dataset) ListTables(ctx context.Context) ([]*Table, error) {
var tables []*Table
err := getPages("", func(pageToken string) (string, error) {
ts, tok, err := d.service.listTables(ctx, d.projectID, d.id, pageToken)
if err == nil {
tables = append(tables, ts...)
}
return tok, err
})
if err != nil {
return nil, err
}
return tables, nil
}
// Create creates a dataset in the BigQuery service. An error will be returned
// if the dataset already exists.
func (d *Dataset) Create(ctx context.Context) error {
return d.service.insertDataset(ctx, d.id, d.projectID)
}
// Table creates a handle to a BigQuery table in the dataset.
// To determine if a table exists, call Table.Metadata.
// If the table does not already exist, use Table.Create to create it.
func (d *Dataset) Table(tableID string) *Table {
return &Table{ProjectID: d.projectID, DatasetID: d.id, TableID: tableID, service: d.service}
}

View File

@ -1,105 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import (
"errors"
"reflect"
"testing"
"golang.org/x/net/context"
)
// readServiceStub services read requests by returning data from an in-memory list of values.
type listTablesServiceStub struct {
expectedProject, expectedDataset string
values [][]*Table // contains pages of tables.
pageTokens map[string]string // maps incoming page token to returned page token.
service
}
func (s *listTablesServiceStub) listTables(ctx context.Context, projectID, datasetID, pageToken string) ([]*Table, string, error) {
if projectID != s.expectedProject {
return nil, "", errors.New("wrong project id")
}
if datasetID != s.expectedDataset {
return nil, "", errors.New("wrong dataset id")
}
tables := s.values[0]
s.values = s.values[1:]
return tables, s.pageTokens[pageToken], nil
}
func TestListTables(t *testing.T) {
t1 := &Table{ProjectID: "p1", DatasetID: "d1", TableID: "t1"}
t2 := &Table{ProjectID: "p1", DatasetID: "d1", TableID: "t2"}
t3 := &Table{ProjectID: "p1", DatasetID: "d1", TableID: "t3"}
testCases := []struct {
data [][]*Table
pageTokens map[string]string
want []*Table
}{
{
data: [][]*Table{{t1, t2}, {t3}},
pageTokens: map[string]string{"": "a", "a": ""},
want: []*Table{t1, t2, t3},
},
{
data: [][]*Table{{t1, t2}, {t3}},
pageTokens: map[string]string{"": ""}, // no more pages after first one.
want: []*Table{t1, t2},
},
}
for _, tc := range testCases {
c := &Client{
service: &listTablesServiceStub{
expectedProject: "x",
expectedDataset: "y",
values: tc.data,
pageTokens: tc.pageTokens,
},
projectID: "x",
}
got, err := c.Dataset("y").ListTables(context.Background())
if err != nil {
t.Errorf("err calling ListTables: %v", err)
continue
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("reading: got:\n%v\nwant:\n%v", got, tc.want)
}
}
}
func TestListTablesError(t *testing.T) {
c := &Client{
service: &listTablesServiceStub{
expectedProject: "x",
expectedDataset: "y",
},
projectID: "x",
}
// Test that service read errors are propagated back to the caller.
// Passing "not y" as the dataset id will cause the service to return an error.
_, err := c.Dataset("not y").ListTables(context.Background())
if err == nil {
// Read should not return an error; only Err should.
t.Errorf("ListTables expected: non-nil err, got: nil")
}
}

View File

@ -1,18 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery provides a client for the BigQuery service.
//
// Note: This package is a work-in-progress. Backwards-incompatible changes should be expected.
package bigquery // import "cloud.google.com/go/bigquery"

View File

@ -1,82 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 bigquery
import (
"fmt"
bq "google.golang.org/api/bigquery/v2"
)
// An Error contains detailed information about a failed bigquery operation.
type Error struct {
// Mirrors bq.ErrorProto, but drops DebugInfo
Location, Message, Reason string
}
func (e Error) Error() string {
return fmt.Sprintf("{Location: %q; Message: %q; Reason: %q}", e.Location, e.Message, e.Reason)
}
func errorFromErrorProto(ep *bq.ErrorProto) *Error {
if ep == nil {
return nil
}
return &Error{
Location: ep.Location,
Message: ep.Message,
Reason: ep.Reason,
}
}
// A MultiError contains multiple related errors.
type MultiError []error
func (m MultiError) Error() string {
switch len(m) {
case 0:
return "(0 errors)"
case 1:
return m[0].Error()
case 2:
return m[0].Error() + " (and 1 other error)"
}
return fmt.Sprintf("%s (and %d other errors)", m[0].Error(), len(m)-1)
}
// RowInsertionError contains all errors that occurred when attempting to insert a row.
type RowInsertionError struct {
InsertID string // The InsertID associated with the affected row.
RowIndex int // The 0-based index of the affected row in the batch of rows being inserted.
Errors MultiError
}
func (e *RowInsertionError) Error() string {
errFmt := "insertion of row [insertID: %q; insertIndex: %v] failed with error: %s"
return fmt.Sprintf(errFmt, e.InsertID, e.RowIndex, e.Errors.Error())
}
// PutMultiError contains an error for each row which was not successfully inserted
// into a BigQuery table.
type PutMultiError []RowInsertionError
func (pme PutMultiError) Error() string {
plural := "s"
if len(pme) == 1 {
plural = ""
}
return fmt.Sprintf("%v row insertion%s failed", len(pme), plural)
}

Some files were not shown because too many files have changed in this diff Show More