mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Merge branch 'master' into txtRecordHandling
This commit is contained in:
commit
b5b8d618e9
@ -21,8 +21,6 @@ before_install:
|
||||
|
||||
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)
|
||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -1,3 +1,34 @@
|
||||
- Add aws evaluate target health flag (#628) @peterbale
|
||||
|
||||
## v0.5.4 - 2018-06-28
|
||||
|
||||
- Only store endpoints with their labels in the cache (#612) @njuettner
|
||||
- Read hostnames from spec.tls.hosts on Ingress object (#611) @ysoldak
|
||||
- Reorder provider/aws suitable-zones tests (#608) @elordahl
|
||||
- Adds TLS flags for pdns provider (#607) @jhoch-palantir
|
||||
- Update RBAC for external-dns to list nodes (#600) @njuettner
|
||||
- Add aws max change count flag (#596) @peterbale
|
||||
- AWS provider: Properly check suitable domains (#594) @elordahl
|
||||
- Annotation with upper-case hostnames block further updates (#579) @njuettner
|
||||
|
||||
## v0.5.3 - 2018-06-15
|
||||
|
||||
- Print a message if no hosted zones match (aws provider) (#592) @svend
|
||||
- Add support for NodePort services (#559) @grimmy
|
||||
- Update azure.md to fix protocol value (#593) @JasonvanBrackel
|
||||
- Add cache to limit calls to providers (#589) @jessfraz
|
||||
- Add Azure MSI support (#578) @r7vme
|
||||
- CoreDNS/SkyDNS provider (#253) @istalker2
|
||||
|
||||
## v0.5.2 - 2018-05-31
|
||||
|
||||
- DNSimple: Make DNSimple tolerant of unknown zones (#574) @jbowes
|
||||
- Cloudflare: Custom record TTL (#572) @njuettner
|
||||
- AWS ServiceDiscovery: Implementation of AWS ServiceDiscovery provider (#483) @vanekjar
|
||||
- Update docs to latest changes (#563) @Raffo
|
||||
- New source - connector (#552) @shashidharatd
|
||||
- Update AWS SDK dependency to v1.13.7 @vanekjar
|
||||
|
||||
## v0.5.1 - 2018-05-16
|
||||
|
||||
- Refactor implementation of sync loop to use `time.Ticker` (#553) @r0fls
|
||||
|
69
Gopkg.lock
generated
69
Gopkg.lock
generated
@ -24,8 +24,8 @@
|
||||
"autorest/date",
|
||||
"autorest/to"
|
||||
]
|
||||
revision = "58f6f26e200fa5dfb40c9cd1c83f3e2c860d779d"
|
||||
version = "v8.0.0"
|
||||
revision = "aa2a4534ab680e938d933870f58f23f77e0e208e"
|
||||
version = "v10.9.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
@ -108,6 +108,18 @@
|
||||
revision = "4c6994ac3877fbb627766edadc67f4e816e8c890"
|
||||
version = "v0.7.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/etcd"
|
||||
packages = [
|
||||
"client",
|
||||
"pkg/pathutil",
|
||||
"pkg/srv",
|
||||
"pkg/types",
|
||||
"version"
|
||||
]
|
||||
revision = "1b3ac99e8a431b381e633802cc42fe70e663baf5"
|
||||
version = "v3.2.15"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/go-oidc"
|
||||
packages = [
|
||||
@ -119,6 +131,12 @@
|
||||
]
|
||||
revision = "be73733bb8cc830d0205609b95d125215f8e9c70"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/go-semver"
|
||||
packages = ["semver"]
|
||||
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/pkg"
|
||||
packages = [
|
||||
@ -131,13 +149,14 @@
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c"
|
||||
version = "v3.0.0"
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/digitalocean/godo"
|
||||
@ -171,6 +190,12 @@
|
||||
]
|
||||
revision = "09691a3b6378b740595c1002f40c34dd5f218a22"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/exoscale/egoscale"
|
||||
packages = ["."]
|
||||
revision = "631ee6ea16ccb48a0c98054fdbf0f6e94d8f4a8c"
|
||||
version = "v0.9.31"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ffledgling/pdns-go"
|
||||
@ -267,7 +292,13 @@
|
||||
branch = "master"
|
||||
name = "github.com/infobloxopen/infoblox-go-client"
|
||||
packages = ["."]
|
||||
revision = "e2811d86bed7bb487eeb0806337b6f9e9d93d5e7"
|
||||
revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/copier"
|
||||
packages = ["."]
|
||||
revision = "7e38e58719c33e0d44d585c4ab477a30f8cb82dd"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
@ -287,7 +318,7 @@
|
||||
[[projects]]
|
||||
name = "github.com/kubernetes/repo-infra"
|
||||
packages = ["verify/boilerplate/test"]
|
||||
revision = "2d2eb5e12b4663fc4d764b5db9daab39334d3f37"
|
||||
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/linki/instrumented_http"
|
||||
@ -316,6 +347,15 @@
|
||||
revision = "cdd946344b54bdf7dbeac406c2f1fe93150f08ea"
|
||||
version = "v0.6.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/oracle/oci-go-sdk"
|
||||
packages = [
|
||||
"common",
|
||||
"dns"
|
||||
]
|
||||
revision = "a2ded717dc4bb4916c0416ec79f81718b576dbc4"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
@ -324,7 +364,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
@ -373,7 +414,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/objx"
|
||||
packages = ["."]
|
||||
revision = "cbeaeb16a013161a98496fad62933b1d21786672"
|
||||
revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
|
||||
version = "v0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
@ -383,8 +425,8 @@
|
||||
"require",
|
||||
"suite"
|
||||
]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@ -410,7 +452,8 @@
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"lex/httplex"
|
||||
"lex/httplex",
|
||||
"publicsuffix"
|
||||
]
|
||||
revision = "e90d6d0afc4c315a0d87a568ae68577cc15149a0"
|
||||
|
||||
@ -648,6 +691,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "d5deea43eb04e9ef3a6ecb3589b91c149e092505f66905baa01c67379776d231"
|
||||
inputs-digest = "d704eb6432ef9b41338900e647421a195366f87134918f9feb023fc377064f57"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
18
Gopkg.toml
18
Gopkg.toml
@ -10,7 +10,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/Azure/go-autorest"
|
||||
version = "~8.0.0"
|
||||
version = "~10.9.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/alecthomas/kingpin"
|
||||
@ -24,6 +24,10 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
|
||||
name = "github.com/cloudflare/cloudflare-go"
|
||||
version = "0.7.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coreos/etcd"
|
||||
version = "~3.2.15"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/digitalocean/godo"
|
||||
version = "~1.1.0"
|
||||
@ -50,7 +54,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "~1.1.4"
|
||||
version = "~1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/client-go"
|
||||
@ -58,8 +62,16 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
|
||||
|
||||
[[override]]
|
||||
name = "github.com/kubernetes/repo-infra"
|
||||
revision = "2d2eb5e12b4663fc4d764b5db9daab39334d3f37"
|
||||
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nesv/go-dynect"
|
||||
version = "0.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/exoscale/egoscale"
|
||||
version = "~0.9.31"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/oracle/oci-go-sdk"
|
||||
version = "1.8.0"
|
||||
|
@ -26,6 +26,7 @@ To see ExternalDNS in action, have a look at this [video](https://www.youtube.co
|
||||
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/)
|
||||
* [AWS Service Discovery](https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html)
|
||||
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
|
||||
* [CloudFlare](https://www.cloudflare.com/de/dns)
|
||||
* [DigitalOcean](https://www.digitalocean.com/products/networking)
|
||||
@ -34,6 +35,9 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected
|
||||
* [Dyn](https://dyn.com/dns/)
|
||||
* [OpenStack Designate](https://docs.openstack.org/designate/latest/)
|
||||
* [PowerDNS](https://www.powerdns.com/)
|
||||
* [CoreDNS](https://coredns.io/)
|
||||
* [Exoscale](https://www.exoscale.com/dns/)
|
||||
* [Oracle Cloud Infrastructure DNS](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm)
|
||||
|
||||
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.
|
||||
|
||||
@ -55,6 +59,8 @@ The following tutorials are provided:
|
||||
* Google Container Engine
|
||||
* [Using Google's Default Ingress Controller](docs/tutorials/gke.md)
|
||||
* [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
|
||||
* [Exoscale](docs/tutorials/exoscale.md)
|
||||
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
|
||||
|
||||
## Running Locally
|
||||
|
||||
|
@ -9,10 +9,11 @@ build_steps:
|
||||
apt-get install -y docker-ce
|
||||
- desc: Build and push docker image
|
||||
cmd: |
|
||||
image=registry-write.opensource.zalan.do/teapot/external-dns:$(git describe --always --dirty --tags)
|
||||
docker build --squash --tag $image .
|
||||
IS_PR_BUILD=${CDP_PULL_REQUEST_NUMBER+"true"}
|
||||
if [[ ${CDP_TARGET_BRANCH} == "master" && ${IS_PR_BUILD} != "true" ]]
|
||||
then
|
||||
docker push $image
|
||||
if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then
|
||||
RELEASE_VERSION=$(git describe --tags --always --dirty)
|
||||
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION}
|
||||
else
|
||||
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION}
|
||||
fi
|
||||
docker build --squash --tag "$IMAGE" .
|
||||
docker push "$IMAGE"
|
||||
|
@ -169,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.5.1
|
||||
registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
time="2017-08-08T14:10:26Z" level=info msg="config: &{Master: KubeConfig: Sources:[service ingress] Namespace: ...
|
||||
```
|
||||
|
||||
|
@ -37,7 +37,9 @@ New cloud providers should be easily pluggable. Initially only AWS/Google platfo
|
||||
|
||||
DNS records will be automatically created in multiple situations:
|
||||
1. Setting `spec.rules.host` on an ingress object.
|
||||
2. Adding the annotation `external-dns.alpha.kubernetes.io/hostname` on a `type=LoadBalancer` service object.
|
||||
2. Setting `spec.tls.hosts` on an ingress object.
|
||||
3. Adding the annotation `external-dns.alpha.kubernetes.io/hostname` on an ingress object.
|
||||
4. Adding the annotation `external-dns.alpha.kubernetes.io/hostname` on a `type=LoadBalancer` service object.
|
||||
|
||||
### Annotations
|
||||
|
||||
|
@ -77,7 +77,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:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -197,4 +197,4 @@ $ aws servicediscovery list-namespaces
|
||||
|
||||
```console
|
||||
$ aws servicediscovery delete-namespace --id ns-durf2oxu4gxcgo6z
|
||||
```
|
||||
```
|
||||
|
@ -83,7 +83,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -117,6 +117,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -146,7 +149,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
@ -103,6 +103,16 @@ If the Kubernetes cluster is not hosted by Azure Container Services and you stil
|
||||
"resourceGroup": "MyDnsResourceGroup",
|
||||
}
|
||||
```
|
||||
If [Azure Managed Service Identity (MSI)](https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview) is enabled for virtual machines, then there is no need to create separate service principal. The contents of `azure.json` should be similar to this:
|
||||
```
|
||||
{
|
||||
"tenantId": "AzureAD tenant Id",
|
||||
"subscriptionId": "Id",
|
||||
"resourceGroup": "MyDnsResourceGroup",
|
||||
"useManagedIdentityExtension": true,
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
@ -143,7 +153,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -181,6 +191,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -210,7 +223,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -261,7 +274,7 @@ metadata:
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: tcp
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
|
@ -42,7 +42,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
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.
|
||||
@ -77,6 +77,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -106,7 +109,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
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.
|
||||
@ -179,7 +182,7 @@ the Cloudflare DNS records.
|
||||
|
||||
## Verifying Cloudflare DNS records
|
||||
|
||||
Check your [Cloudflare dasbhboard](https://www.cloudflare.com/a/dns/example.com) to view the records for your Cloudflare DNS zone.
|
||||
Check your [Cloudflare dashboard](https://www.cloudflare.com/a/dns/example.com) to view the records for your Cloudflare DNS zone.
|
||||
|
||||
Substitute the zone for the one created above if a different domain was used.
|
||||
|
||||
|
@ -39,7 +39,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
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.
|
||||
@ -70,6 +70,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -99,7 +102,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
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.
|
||||
|
@ -41,7 +41,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=ingress
|
||||
- --txt-prefix=_d
|
||||
@ -142,4 +142,4 @@ Login to the console at https://portal.dynect.net/login/ and verify records are
|
||||
## Clean up
|
||||
|
||||
Login to the console at https://portal.dynect.net/login/ and delete the records created. Alternatively, just delete the sample
|
||||
Ingress resources and external-dns will delete the records.
|
||||
Ingress resources and external-dns will delete the records.
|
||||
|
158
docs/tutorials/exoscale.md
Normal file
158
docs/tutorials/exoscale.md
Normal file
@ -0,0 +1,158 @@
|
||||
# Setting up ExternalDNS for Exoscale
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Exoscale provider support was added via [this PR](https://github.com/kubernetes-incubator/external-dns/pull/625), thus you need to use external-dns v0.5.5.
|
||||
|
||||
The Exoscale provider expects that your Exoscale zones, you wish to add records to, already exists
|
||||
and are configured correctly. It does not add, remove or configure new zones in anyway.
|
||||
|
||||
To do this pease refer to the [Exoscale DNS documentation](https://community.exoscale.com/documentation/dns/).
|
||||
|
||||
Additionally you will have to provide the Exoscale...:
|
||||
|
||||
* API Key
|
||||
* API Secret
|
||||
* API Endpoint
|
||||
* Elastic IP address, to access the workers
|
||||
|
||||
## Deployment
|
||||
|
||||
Deploying external DNS for Exoscale 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: 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.5
|
||||
args:
|
||||
- --source=ingress # or service or both
|
||||
- --provider=exoscale
|
||||
- --domain-filter={{ my-domain }}
|
||||
- --policy=sync # if you want DNS entries to get deleted as well
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
- --exoscale-endpoint={{ endpoint }} # usually https://api.exoscale.ch/dns
|
||||
- --exoscale-apikey={{ api-key}}
|
||||
- --exoscale-apisecret={{ api-secret }}
|
||||
```
|
||||
|
||||
## 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
|
||||
namespace: default
|
||||
|
||||
---
|
||||
|
||||
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"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["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 nginx HTTP server with the following spec (`kubectl apply -f`):
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }}
|
||||
spec:
|
||||
rules:
|
||||
- host: via-ingress.example.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: nginx
|
||||
servicePort: 80
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
|
||||
---
|
||||
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
Wait about 30s-1m (interval for external-dns to kick in), then check Exoscales [portal](https://portal.exoscale.com/dns/example.com)... via-ingress.example.com should appear as a A and TXT record with your Elastic-IP-address.
|
@ -88,7 +88,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -121,6 +121,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -150,7 +153,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
@ -25,7 +25,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --debug
|
||||
- --source=service
|
||||
@ -58,6 +58,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -84,7 +87,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --debug
|
||||
- --source=service
|
||||
|
@ -66,7 +66,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains.
|
||||
@ -114,6 +114,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -143,7 +146,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains.
|
||||
|
@ -222,6 +222,9 @@ rules:
|
||||
- apiGroups: ["extensions"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
@ -251,7 +254,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=ingress
|
||||
- --domain-filter=external-dns-test.gcp.zalan.do
|
||||
|
155
docs/tutorials/oracle.md
Normal file
155
docs/tutorials/oracle.md
Normal file
@ -0,0 +1,155 @@
|
||||
# Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI)
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OCI DNS.
|
||||
|
||||
Make sure to use the latest version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating an OCI DNS Zone
|
||||
|
||||
Create a DNS zone which will contain the managed DNS records. Let's use `example.com` as an reference here.
|
||||
|
||||
For more information about OCI DNS see the documentation [here][1].
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
|
||||
We first need to create a config file containing the information needed to connect with the OCI API.
|
||||
|
||||
Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
region: us-phoenix-1
|
||||
tenancy: ocid1.tenancy.oc1...
|
||||
user: ocid1.user.oc1...
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
-----END RSA PRIVATE KEY-----
|
||||
fingerprint: af:81:71:8e...
|
||||
compartment: ocid1.compartment.oc1...
|
||||
```
|
||||
|
||||
Create a secret using the config file above:
|
||||
|
||||
```shell
|
||||
$ kubectl create secret generic external-dns-config --from-file=oci.yaml
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
|
||||
Apply the following manifest 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"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["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:latest
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --provider=oci
|
||||
- --policy=upsert-only # prevent ExternalDNSfrom deleting any records, omit to enable full synchronization
|
||||
- --txt-owner-id=my-identifier
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/kubernetes/
|
||||
volumes:
|
||||
- name: config
|
||||
secret:
|
||||
secretName: external-dns-config
|
||||
```
|
||||
|
||||
## Verify ExternalDNS works (Service example)
|
||||
|
||||
Create the following sample application to test that ExternalDNS works.
|
||||
|
||||
> For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: example.com
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- port: 80
|
||||
name: http
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
```
|
||||
|
||||
Apply the manifest above and wait roughly two minutes and check that a corresponding DNS record for your service was created.
|
||||
|
||||
```
|
||||
$ kubectl apply -f nginx.yaml
|
||||
```
|
||||
|
||||
[1]: https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm
|
@ -40,7 +40,7 @@ spec:
|
||||
# serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:v0.5.1
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=pdns
|
||||
@ -74,6 +74,9 @@ rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
|
@ -243,7 +243,7 @@ spec:
|
||||
- --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
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
name: external-dns-public
|
||||
```
|
||||
|
||||
@ -281,7 +281,7 @@ spec:
|
||||
- --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
|
||||
image: registry.opensource.zalan.do/teapot/external-dns:latest
|
||||
name: external-dns-private
|
||||
```
|
||||
|
||||
|
@ -29,6 +29,8 @@ const (
|
||||
RecordTypeCNAME = "CNAME"
|
||||
// RecordTypeTXT is a RecordType enum value
|
||||
RecordTypeTXT = "TXT"
|
||||
// RecordTypeSRV is a RecordType enum value
|
||||
RecordTypeSRV = "SRV"
|
||||
)
|
||||
|
||||
// TTL is a structure defining the TTL of a DNS record
|
||||
|
23
internal/testresources/ca.pem
Normal file
23
internal/testresources/ca.pem
Normal file
@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwDCCAqigAwIBAgIUYsFawvERY3xGTHmKWq/78Cp70AIwDQYJKoZIhvcNAQEL
|
||||
BQAweDEQMA4GA1UEBhMHQ291bnRyeTERMA8GA1UEBxMITG9jYWxpdHkxFTATBgNV
|
||||
BAoTDE9yZ2FuaXphdGlvbjEbMBkGA1UECxMST3JnYW5pemF0aW9uYWxVbml0MR0w
|
||||
GwYDVQQDExRleHRlcm5hbC1kbnMgdGVzdCBDQTAeFw0xODA2MTQyMTE5MDBaFw0y
|
||||
MzA2MTMyMTE5MDBaMHgxEDAOBgNVBAYTB0NvdW50cnkxETAPBgNVBAcTCExvY2Fs
|
||||
aXR5MRUwEwYDVQQKEwxPcmdhbml6YXRpb24xGzAZBgNVBAsTEk9yZ2FuaXphdGlv
|
||||
bmFsVW5pdDEdMBsGA1UEAxMUZXh0ZXJuYWwtZG5zIHRlc3QgQ0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMEaMix69YrVXPtfUsOgz5RJqaAlitw+HV
|
||||
BaYv7BbcChiidYQ+/iKo26HA5vjBP5xOMNYTnVowXA+q+RmMGfhSc+j9CiJeADP0
|
||||
oxjSNq/w5Xb/IFIHSjr+dEdavcdsV95y3BYB8PkopjXEmNgbEgPbHNuJMQkd89rC
|
||||
2ztSIPHbjhorrauAGm8cgzdKK6Tnxhey9yQralIgdrOHXMTOZrWywTPiUtIuxrn5
|
||||
XfIaylfqQO+Q79rEGhk9YQuFUqs+GDDp/PiCGC56/IbF7NVLEdrJIc6Tf9Rg2/K/
|
||||
9ydeZ5hcaM542Q2UoXIbRp8jn/J1Xr3mcxhpnhJN4TcjLalnSY6tAgMBAAGjQjBA
|
||||
MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTym+5y
|
||||
SEghmx5Xr+wd5Z0V+9AWxDANBgkqhkiG9w0BAQsFAAOCAQEAlGaKZY40apdmROAx
|
||||
usSLJjgVIw5GjX7lNw6BqorbQavexPjghfhB1TSpFOvHCz7s164Eq18+wfbzfnR5
|
||||
L+Xza+eibvkgO8ZojGMFXR+5NCbM2cPBTmWZVNVRoZQX6j5RT/DeLcjEKDBWZBdP
|
||||
IxAQCFprxHgBZeHOfzvombzdbDM5To9ff+3gaunMbWs7YAmpv1czRc0F0arXg+mA
|
||||
AzG4fc94lJ5oMF6sClZ1rbJjLcohtINx6HstUzLxlAcgJMJcvvJrrdTkzJSCOmE6
|
||||
a52RJX6qVaZNHCXDooiy2uDapXyA4sPCt4n3KhRfP+JGp6Xmg10rm7ga8+ZUCYae
|
||||
UyE4mA==
|
||||
-----END CERTIFICATE-----
|
27
internal/testresources/client-cert-key.pem
Normal file
27
internal/testresources/client-cert-key.pem
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAykDCIA6Q0p8HJLx3njenAqvKvHGwJzjLP+tw4iq8J3//aEon
|
||||
e23t9CwGW9YVNTM4Xu6n0+exUsUK4LweHRTYLmYkTHUc+pB9+XpQ+wKutmxv+zhk
|
||||
uWi+4u6pqC6e8TSuURs3c9ML2TxCTIRubd3hPRGMizo3POuQ8iaRo8icfdQcv/XE
|
||||
bieB8ma5ZUkRxkY+Mu+MYOsleoIpSByej18ycJkVkipV6CeSxenCsQaeAaJPTOVO
|
||||
9rYZL3F3fNHXezIpCmw3sgCiudQ1MvPKGs4G5UahCO0OSp42IMUodYxvQ0G1o5uZ
|
||||
9k9R8Xcumso61xHM/ZCkzqJ89P/2zXyUC7c2OQIDAQABAoIBABoHL3RUq4qPcKnn
|
||||
nzU7UEDlvtd1ggfqJS36rLJOcZxbupC/Skl2IjNUHxVefag1CUIeUHbS0F0ognfd
|
||||
fbqcXh3Kg01bnPkZ8zxR424KMFXFqruXzE6YDkjCEg9UwJul/fDuIbrEJDg2qwmR
|
||||
2WxGK6BiS8X3Hfi3EBY5pHCBdrIyiVWZn8CYYmiBhJehdNHEwtIk+Bo8mWBTOOpS
|
||||
x9e1czYICM39zyZQtuLvI/CIcSg1uyRL19r7KrAoBS2o6ijDrp5KqCyAVL9UYq5m
|
||||
B3k/KltYi6d3HtcHwuMHPKNpWfOQCKu7MDX+ZNi7E6LrPxWqrPoiZ5xzAQPQHD3B
|
||||
e3fmt6ECgYEA5z5z3kxjF6EYttzDMylUghTfPs/vyhecFKzbcipdMJNs8o9KnHF9
|
||||
WgH7JaPU8cYe8CluQWZn6ByxdaB7G56wfHrYYCm5pbsuxlkoqqLMmAmVBPZVMhEy
|
||||
thoxi4PxAdcs5HsqwYbazpToYZ6ktvknIUKOp24oQgYUG5T0mkNm910CgYEA3+fE
|
||||
4Mh2rikegQLYar9gsFAXpBjxiMRQlUH23Qk9p21AkczTjGpYeV4v2LxxTYKiaZRJ
|
||||
8X4Ab745j+yLbsYMxZKihNCQSLTK44FSgK6fSEs+yHLpQjT4V5IyvD+tHMan9n/s
|
||||
YqDppse1GHGGxF7N4FatrQk5Kz8FG9EYa6BNWI0CgYBYXALGRIXwt3vME9r6p6ZE
|
||||
9li/lZDYteDL/aj0nL9zGkIdBSfLU4pEZFFk9o8du0iDGDGrB8hYZu5gNewUh7SE
|
||||
PCSFyivH6hhbbiId4Ysv5Slt9fpj4TJxZtzbpJTo0SG0RGP4AuGE4l1RP99MkzOi
|
||||
f94ml+8GG3B/gZFdiLfFeQKBgQC9pDxoduGuWT1w38wVfcqTCwM7BbVttXjbMme4
|
||||
hx8lM6/Azc9P2rLc+R1lYRZGJCMTcXm/hI0yF9gBQsRGKpCetrfX7h6Gtjoxv1L1
|
||||
kvFt9e1TMaDHZr4Azd8S+ovRF6Ejcu3wC4JatEN6VI1kvTd6j2/CY1F8g3/8M3mP
|
||||
jtJz8QKBgCL2XDev1Vls2hzqrNqjNehYVAqTNdNNr2jzCR1g8AMFqSy7k/4gFtu/
|
||||
bXBnSKnGrtmb+VWKDMwNy7oe6g6haFLTjPbnl8/afKBbH0WQzlvVJgKWSX1faWTG
|
||||
1WMRAqD8nIdcYbfj7AhmNGL6zYGr0g+YP9CF+j6je2Rb0so+S5cZ
|
||||
-----END RSA PRIVATE KEY-----
|
24
internal/testresources/client-cert.pem
Normal file
24
internal/testresources/client-cert.pem
Normal file
@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEEzCCAvugAwIBAgIUJxJZIg35oCO6747gR3c9ZkQc8TMwDQYJKoZIhvcNAQEL
|
||||
BQAweDEQMA4GA1UEBhMHQ291bnRyeTERMA8GA1UEBxMITG9jYWxpdHkxFTATBgNV
|
||||
BAoTDE9yZ2FuaXphdGlvbjEbMBkGA1UECxMST3JnYW5pemF0aW9uYWxVbml0MR0w
|
||||
GwYDVQQDExRleHRlcm5hbC1kbnMgdGVzdCBDQTAeFw0xODA2MTQyMTIzMDBaFw0y
|
||||
MzA2MTMyMTIzMDBaMIGIMRAwDgYDVQQGEwdDb3VudHJ5MREwDwYDVQQHEwhMb2Nh
|
||||
bGl0eTEVMBMGA1UEChMMT3JnYW5pemF0aW9uMRswGQYDVQQLExJPcmdhbml6YXRp
|
||||
b25hbFVuaXQxLTArBgNVBAMTJGV4dGVybmFsLWRucyB0ZXN0IGNsaWVudCBjZXJ0
|
||||
aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMpAwiAOkNKf
|
||||
ByS8d543pwKryrxxsCc4yz/rcOIqvCd//2hKJ3tt7fQsBlvWFTUzOF7up9PnsVLF
|
||||
CuC8Hh0U2C5mJEx1HPqQffl6UPsCrrZsb/s4ZLlovuLuqagunvE0rlEbN3PTC9k8
|
||||
QkyEbm3d4T0RjIs6NzzrkPImkaPInH3UHL/1xG4ngfJmuWVJEcZGPjLvjGDrJXqC
|
||||
KUgcno9fMnCZFZIqVegnksXpwrEGngGiT0zlTva2GS9xd3zR13syKQpsN7IAornU
|
||||
NTLzyhrOBuVGoQjtDkqeNiDFKHWMb0NBtaObmfZPUfF3LprKOtcRzP2QpM6ifPT/
|
||||
9s18lAu3NjkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI
|
||||
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUA+Do/9mVdkyYJAPYUzbr
|
||||
9FBi8gMwHwYDVR0jBBgwFoAU8pvuckhIIZseV6/sHeWdFfvQFsQwCwYDVR0RBAQw
|
||||
AoIAMA0GCSqGSIb3DQEBCwUAA4IBAQBFzN/cqkVjGYaQzCpOWVgizIwBhGFRfYGY
|
||||
Cw5m9HaZIMjbSxt55NhRTFm6Q5qFfD2KXXPueEUL4U5iPg+LPHrUfNmKiJtUcKc8
|
||||
M2FJimb7nRsw46F+tRt0lgM5sDeqe2QUlNTKFKaxnHUDqWt4mW7adzog3sX7UfGB
|
||||
yvbJR9Y6cEMlG2it3rl9/ZiAJnTJSvCqBV9QlBAkCCh0JgJEtPLubz97BVGkMORh
|
||||
+ZgHCw/A9sew/7Krpbyp/NtHeFVsa8tH8wivnaGeITGD4J23U9E3YYUaNPN7kBcX
|
||||
DuFCSEKHGsbAvH2Igxkk+rD5T8d6RwJ98jkXOYXCxGmGBuDEkyGZ
|
||||
-----END CERTIFICATE-----
|
38
main.go
38
main.go
@ -95,7 +95,16 @@ func main() {
|
||||
var p provider.Provider
|
||||
switch cfg.Provider {
|
||||
case "aws":
|
||||
p, err = provider.NewAWSProvider(domainFilter, zoneIDFilter, zoneTypeFilter, cfg.AWSAssumeRole, cfg.DryRun)
|
||||
p, err = provider.NewAWSProvider(
|
||||
provider.AWSConfig{
|
||||
DomainFilter: domainFilter,
|
||||
ZoneIDFilter: zoneIDFilter,
|
||||
ZoneTypeFilter: zoneTypeFilter,
|
||||
MaxChangeCount: cfg.AWSMaxChangeCount,
|
||||
AssumeRole: cfg.AWSAssumeRole,
|
||||
DryRun: cfg.DryRun,
|
||||
},
|
||||
)
|
||||
case "aws-sd":
|
||||
// Check that only compatible Registry is used with AWS-SD
|
||||
if cfg.Registry != "noop" && cfg.Registry != "aws-sd" {
|
||||
@ -140,12 +149,35 @@ func main() {
|
||||
AppVersion: externaldns.Version,
|
||||
},
|
||||
)
|
||||
case "coredns", "skydns":
|
||||
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.DryRun)
|
||||
case "exoscale":
|
||||
p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil
|
||||
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)
|
||||
p, err = provider.NewPDNSProvider(
|
||||
provider.PDNSConfig{
|
||||
DomainFilter: domainFilter,
|
||||
DryRun: cfg.DryRun,
|
||||
Server: cfg.PDNSServer,
|
||||
APIKey: cfg.PDNSAPIKey,
|
||||
TLSConfig: provider.TLSConfig{
|
||||
TLSEnabled: cfg.PDNSTLSEnabled,
|
||||
CAFilePath: cfg.TLSCA,
|
||||
ClientCertFilePath: cfg.TLSClientCert,
|
||||
ClientCertKeyFilePath: cfg.TLSClientCertKey,
|
||||
},
|
||||
},
|
||||
)
|
||||
case "oci":
|
||||
var config *provider.OCIConfig
|
||||
config, err = provider.LoadOCIConfig(cfg.OCIConfigFile)
|
||||
if err == nil {
|
||||
p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
}
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
@ -158,7 +190,7 @@ func main() {
|
||||
case "noop":
|
||||
r, err = registry.NewNoopRegistry(p)
|
||||
case "txt":
|
||||
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID)
|
||||
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
|
||||
case "aws-sd":
|
||||
r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID)
|
||||
default:
|
||||
|
@ -52,6 +52,8 @@ type Config struct {
|
||||
ZoneIDFilter []string
|
||||
AWSZoneType string
|
||||
AWSAssumeRole string
|
||||
AWSMaxChangeCount int
|
||||
AWSEvaluateTargetHealth bool
|
||||
AzureConfigFile string
|
||||
AzureResourceGroup string
|
||||
CloudflareProxied bool
|
||||
@ -65,9 +67,14 @@ type Config struct {
|
||||
DynUsername string
|
||||
DynPassword string
|
||||
DynMinTTLSeconds int
|
||||
OCIConfigFile string
|
||||
InMemoryZones []string
|
||||
PDNSServer string
|
||||
PDNSAPIKey string
|
||||
PDNSTLSEnabled bool
|
||||
TLSCA string
|
||||
TLSClientCert string
|
||||
TLSClientCertKey string
|
||||
Policy string
|
||||
Registry string
|
||||
TXTOwnerID string
|
||||
@ -78,6 +85,10 @@ type Config struct {
|
||||
LogFormat string
|
||||
MetricsAddress string
|
||||
LogLevel string
|
||||
TXTCacheInterval time.Duration
|
||||
ExoscaleEndpoint string
|
||||
ExoscaleAPIKey string
|
||||
ExoscaleAPISecret string
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
@ -96,6 +107,8 @@ var defaultConfig = &Config{
|
||||
DomainFilter: []string{},
|
||||
AWSZoneType: "",
|
||||
AWSAssumeRole: "",
|
||||
AWSMaxChangeCount: 4000,
|
||||
AWSEvaluateTargetHealth: true,
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
CloudflareProxied: false,
|
||||
@ -105,19 +118,28 @@ var defaultConfig = &Config{
|
||||
InfobloxWapiPassword: "",
|
||||
InfobloxWapiVersion: "2.3.1",
|
||||
InfobloxSSLVerify: true,
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{},
|
||||
PDNSServer: "http://localhost:8081",
|
||||
PDNSAPIKey: "",
|
||||
PDNSTLSEnabled: false,
|
||||
TLSCA: "",
|
||||
TLSClientCert: "",
|
||||
TLSClientCertKey: "",
|
||||
Policy: "sync",
|
||||
Registry: "txt",
|
||||
TXTOwnerID: "default",
|
||||
TXTPrefix: "",
|
||||
TXTCacheInterval: 0,
|
||||
Interval: time.Minute,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
LogFormat: "text",
|
||||
MetricsAddress: ":7979",
|
||||
LogLevel: logrus.InfoLevel.String(),
|
||||
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
|
||||
ExoscaleAPIKey: "",
|
||||
ExoscaleAPISecret: "",
|
||||
}
|
||||
|
||||
// NewConfig returns new Config object
|
||||
@ -134,6 +156,9 @@ func (cfg *Config) String() string {
|
||||
if temp.InfobloxWapiPassword != "" {
|
||||
temp.InfobloxWapiPassword = passwordMask
|
||||
}
|
||||
if temp.PDNSAPIKey != "" {
|
||||
temp.PDNSAPIKey = ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%+v", temp)
|
||||
}
|
||||
@ -168,12 +193,14 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
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, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, inmemory, pdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "inmemory", "pdns")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale")
|
||||
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, 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("aws-max-change-count", "When using the AWS provider, set the maximum number of changes that will be applied.").Default(strconv.Itoa(defaultConfig.AWSMaxChangeCount)).IntVar(&cfg.AWSMaxChangeCount)
|
||||
app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth)
|
||||
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)
|
||||
@ -187,10 +214,21 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
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("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
|
||||
|
||||
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)
|
||||
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
|
||||
app.Flag("pdns-tls-enabled", "When using the PowerDNS/PDNS provider, specify whether to use TLS (default: false, requires --tls-ca, optionally specify --tls-client-cert and --tls-client-cert-key)").Default(strconv.FormatBool(defaultConfig.PDNSTLSEnabled)).BoolVar(&cfg.PDNSTLSEnabled)
|
||||
|
||||
// Flags related to TLS communication
|
||||
app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA)
|
||||
app.Flag("tls-client-cert", "When using TLS communication, the path to the certificate to present as a client (not required for TLS)").Default(defaultConfig.TLSClientCert).StringVar(&cfg.TLSClientCert)
|
||||
app.Flag("tls-client-cert-key", "When using TLS communication, the path to the certificate key to use with the client certificate (not required for TLS)").Default(defaultConfig.TLSClientCertKey).StringVar(&cfg.TLSClientCertKey)
|
||||
|
||||
app.Flag("exoscale-endpoint", "Provide the endpoint for the Exoscale provider").Default(defaultConfig.ExoscaleEndpoint).StringVar(&cfg.ExoscaleEndpoint)
|
||||
app.Flag("exoscale-apikey", "Provide your API Key for the Exoscale provider").Default(defaultConfig.ExoscaleAPIKey).StringVar(&cfg.ExoscaleAPIKey)
|
||||
app.Flag("exoscale-apisecret", "Provide your API Secret for the Exoscale provider").Default(defaultConfig.ExoscaleAPISecret).StringVar(&cfg.ExoscaleAPISecret)
|
||||
|
||||
// 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")
|
||||
@ -201,6 +239,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional)").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
|
||||
|
||||
// Flags related to the main control loop
|
||||
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
|
||||
app.Flag("interval", "The interval between two consecutive synchronizations in duration format (default: 1m)").Default(defaultConfig.Interval.String()).DurationVar(&cfg.Interval)
|
||||
app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once)
|
||||
app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun)
|
||||
|
@ -29,79 +29,97 @@ import (
|
||||
|
||||
var (
|
||||
minimalConfig = &Config{
|
||||
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",
|
||||
Master: "",
|
||||
KubeConfig: "",
|
||||
Sources: []string{"service"},
|
||||
Namespace: "",
|
||||
FQDNTemplate: "",
|
||||
Compatibility: "",
|
||||
Provider: "google",
|
||||
GoogleProject: "",
|
||||
DomainFilter: []string{""},
|
||||
ZoneIDFilter: []string{""},
|
||||
AWSZoneType: "",
|
||||
AWSAssumeRole: "",
|
||||
AWSMaxChangeCount: 4000,
|
||||
AWSEvaluateTargetHealth: true,
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
CloudflareProxied: false,
|
||||
InfobloxGridHost: "",
|
||||
InfobloxWapiPort: 443,
|
||||
InfobloxWapiUsername: "admin",
|
||||
InfobloxWapiPassword: "",
|
||||
InfobloxWapiVersion: "2.3.1",
|
||||
InfobloxSSLVerify: true,
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{""},
|
||||
PDNSServer: "http://localhost:8081",
|
||||
PDNSAPIKey: "",
|
||||
Policy: "sync",
|
||||
Registry: "txt",
|
||||
TXTOwnerID: "default",
|
||||
TXTPrefix: "",
|
||||
TXTCacheInterval: 0,
|
||||
Interval: time.Minute,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
LogFormat: "text",
|
||||
MetricsAddress: ":7979",
|
||||
LogLevel: logrus.InfoLevel.String(),
|
||||
ConnectorSourceServer: "localhost:8080",
|
||||
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
|
||||
ExoscaleAPIKey: "",
|
||||
ExoscaleAPISecret: "",
|
||||
}
|
||||
|
||||
overriddenConfig = &Config{
|
||||
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",
|
||||
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",
|
||||
AWSMaxChangeCount: 100,
|
||||
AWSEvaluateTargetHealth: false,
|
||||
AzureConfigFile: "azure.json",
|
||||
AzureResourceGroup: "arg",
|
||||
CloudflareProxied: true,
|
||||
InfobloxGridHost: "127.0.0.1",
|
||||
InfobloxWapiPort: 8443,
|
||||
InfobloxWapiUsername: "infoblox",
|
||||
InfobloxWapiPassword: "infoblox",
|
||||
InfobloxWapiVersion: "2.6.1",
|
||||
InfobloxSSLVerify: false,
|
||||
OCIConfigFile: "oci.yaml",
|
||||
InMemoryZones: []string{"example.org", "company.com"},
|
||||
PDNSServer: "http://ns.example.com:8081",
|
||||
PDNSAPIKey: "some-secret-key",
|
||||
PDNSTLSEnabled: true,
|
||||
TLSCA: "/path/to/ca.crt",
|
||||
TLSClientCert: "/path/to/cert.pem",
|
||||
TLSClientCertKey: "/path/to/key.pem",
|
||||
Policy: "upsert-only",
|
||||
Registry: "noop",
|
||||
TXTOwnerID: "owner-1",
|
||||
TXTPrefix: "associated-txt-record",
|
||||
TXTCacheInterval: 12 * time.Hour,
|
||||
Interval: 10 * time.Minute,
|
||||
Once: true,
|
||||
DryRun: true,
|
||||
LogFormat: "json",
|
||||
MetricsAddress: "127.0.0.1:9099",
|
||||
LogLevel: logrus.DebugLevel.String(),
|
||||
ConnectorSourceServer: "localhost:8081",
|
||||
ExoscaleEndpoint: "https://api.foo.ch/dns",
|
||||
ExoscaleAPIKey: "1",
|
||||
ExoscaleAPISecret: "2",
|
||||
}
|
||||
)
|
||||
|
||||
@ -146,6 +164,11 @@ func TestParseFlags(t *testing.T) {
|
||||
"--inmemory-zone=company.com",
|
||||
"--pdns-server=http://ns.example.com:8081",
|
||||
"--pdns-api-key=some-secret-key",
|
||||
"--pdns-tls-enabled",
|
||||
"--oci-config-file=oci.yaml",
|
||||
"--tls-ca=/path/to/ca.crt",
|
||||
"--tls-client-cert=/path/to/cert.pem",
|
||||
"--tls-client-cert-key=/path/to/key.pem",
|
||||
"--no-infoblox-ssl-verify",
|
||||
"--domain-filter=example.org",
|
||||
"--domain-filter=company.com",
|
||||
@ -153,10 +176,13 @@ func TestParseFlags(t *testing.T) {
|
||||
"--zone-id-filter=/hostedzone/ZTST2",
|
||||
"--aws-zone-type=private",
|
||||
"--aws-assume-role=some-other-role",
|
||||
"--aws-max-change-count=100",
|
||||
"--no-aws-evaluate-target-health",
|
||||
"--policy=upsert-only",
|
||||
"--registry=noop",
|
||||
"--txt-owner-id=owner-1",
|
||||
"--txt-prefix=associated-txt-record",
|
||||
"--txt-cache-interval=12h",
|
||||
"--interval=10m",
|
||||
"--once",
|
||||
"--dry-run",
|
||||
@ -164,6 +190,9 @@ func TestParseFlags(t *testing.T) {
|
||||
"--metrics-address=127.0.0.1:9099",
|
||||
"--log-level=debug",
|
||||
"--connector-source-server=localhost:8081",
|
||||
"--exoscale-endpoint=https://api.foo.ch/dns",
|
||||
"--exoscale-apikey=1",
|
||||
"--exoscale-apisecret=2",
|
||||
},
|
||||
envVars: map[string]string{},
|
||||
expected: overriddenConfig,
|
||||
@ -172,41 +201,52 @@ 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\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",
|
||||
"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_OCI_CONFIG_FILE": "oci.yaml",
|
||||
"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_PDNS_TLS_ENABLED": "1",
|
||||
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
|
||||
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
|
||||
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
|
||||
"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_AWS_MAX_CHANGE_COUNT": "100",
|
||||
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
|
||||
"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_TXT_CACHE_INTERVAL": "12h",
|
||||
"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",
|
||||
"EXTERNAL_DNS_EXOSCALE_ENDPOINT": "https://api.foo.ch/dns",
|
||||
"EXTERNAL_DNS_EXOSCALE_APIKEY": "1",
|
||||
"EXTERNAL_DNS_EXOSCALE_APISECRET": "2",
|
||||
},
|
||||
expected: overriddenConfig,
|
||||
},
|
||||
@ -245,10 +285,12 @@ func TestPasswordsNotLogged(t *testing.T) {
|
||||
cfg := Config{
|
||||
DynPassword: "dyn-pass",
|
||||
InfobloxWapiPassword: "infoblox-pass",
|
||||
PDNSAPIKey: "pdns-api-key",
|
||||
}
|
||||
|
||||
s := cfg.String()
|
||||
|
||||
assert.False(t, strings.Contains(s, "dyn-pass"))
|
||||
assert.False(t, strings.Contains(s, "infoblox-pass"))
|
||||
assert.False(t, strings.Contains(s, "pdns-api-key"))
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const defaultMinVersion = 0
|
||||
|
||||
// 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))
|
||||
@ -34,14 +36,15 @@ func CreateTLSConfig(prefix string) (*tls.Config, error) {
|
||||
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)
|
||||
tlsConfig, err := NewTLSConfig(certFile, keyFile, caFile, serverName, isInsecure, defaultMinVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
|
||||
// NewTLSConfig creates a tls.Config instance from directly-passed parameters, loading the ca, cert, and key from disk
|
||||
func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, minVersion uint16) (*tls.Config, error) {
|
||||
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
|
||||
return nil, errors.New("either both cert and key or none must be provided")
|
||||
}
|
||||
@ -59,6 +62,7 @@ func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
MinVersion: minVersion,
|
||||
Certificates: certificates,
|
||||
RootCAs: roots,
|
||||
InsecureSkipVerify: insecure,
|
||||
|
22
plan/plan.go
22
plan/plan.go
@ -17,6 +17,8 @@ limitations under the License.
|
||||
package plan
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
)
|
||||
|
||||
@ -77,17 +79,19 @@ type planTableRow struct {
|
||||
}
|
||||
|
||||
func (t planTable) addCurrent(e *endpoint.Endpoint) {
|
||||
if _, ok := t.rows[e.DNSName]; !ok {
|
||||
t.rows[e.DNSName] = &planTableRow{}
|
||||
dnsName := sanitizeDNSName(e.DNSName)
|
||||
if _, ok := t.rows[dnsName]; !ok {
|
||||
t.rows[dnsName] = &planTableRow{}
|
||||
}
|
||||
t.rows[e.DNSName].current = e
|
||||
t.rows[dnsName].current = e
|
||||
}
|
||||
|
||||
func (t planTable) addCandidate(e *endpoint.Endpoint) {
|
||||
if _, ok := t.rows[e.DNSName]; !ok {
|
||||
t.rows[e.DNSName] = &planTableRow{}
|
||||
dnsName := sanitizeDNSName(e.DNSName)
|
||||
if _, ok := t.rows[dnsName]; !ok {
|
||||
t.rows[dnsName] = &planTableRow{}
|
||||
}
|
||||
t.rows[e.DNSName].candidates = append(t.rows[e.DNSName].candidates, e)
|
||||
t.rows[dnsName].candidates = append(t.rows[dnsName].candidates, e)
|
||||
}
|
||||
|
||||
// TODO: allows record type change, which might not be supported by all dns providers
|
||||
@ -199,3 +203,9 @@ func filterRecordsForPlan(records []*endpoint.Endpoint) []*endpoint.Endpoint {
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// sanitizeDNSName checks if the DNS name is correct
|
||||
// for now it only removes space and lower case
|
||||
func sanitizeDNSName(dnsName string) string {
|
||||
return strings.TrimSpace(strings.ToLower(dnsName))
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@ -383,3 +384,55 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
|
||||
t.Fatalf("expected %q to match %q", entries, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeDNSName(t *testing.T) {
|
||||
records := []struct {
|
||||
dnsName string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
"3AAAA.FOO.BAR.COM ",
|
||||
"3aaaa.foo.bar.com",
|
||||
},
|
||||
{
|
||||
" example.foo.com",
|
||||
"example.foo.com",
|
||||
},
|
||||
{
|
||||
"example123.foo.com ",
|
||||
"example123.foo.com",
|
||||
},
|
||||
{
|
||||
"foo",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"123foo.bar",
|
||||
"123foo.bar",
|
||||
},
|
||||
{
|
||||
"foo.com",
|
||||
"foo.com",
|
||||
},
|
||||
{
|
||||
"foo123.COM",
|
||||
"foo123.com",
|
||||
},
|
||||
{
|
||||
"my-exaMple3.FOO.BAR.COM",
|
||||
"my-example3.foo.bar.com",
|
||||
},
|
||||
{
|
||||
" my-example1214.FOO-1235.BAR-foo.COM ",
|
||||
"my-example1214.foo-1235.bar-foo.com",
|
||||
},
|
||||
{
|
||||
"my-example-my-example-1214.FOO-1235.BAR-foo.COM",
|
||||
"my-example-my-example-1214.foo-1235.bar-foo.com",
|
||||
},
|
||||
}
|
||||
for _, r := range records {
|
||||
gotName := sanitizeDNSName(r.dnsName)
|
||||
assert.Equal(t, r.expect, gotName)
|
||||
}
|
||||
}
|
||||
|
154
provider/aws.go
154
provider/aws.go
@ -31,9 +31,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
evaluateTargetHealth = true
|
||||
recordTTL = 300
|
||||
maxChangeCount = 4000
|
||||
recordTTL = 300
|
||||
)
|
||||
|
||||
var (
|
||||
@ -86,8 +84,10 @@ type Route53API interface {
|
||||
|
||||
// AWSProvider is an implementation of Provider for AWS Route53.
|
||||
type AWSProvider struct {
|
||||
client Route53API
|
||||
dryRun bool
|
||||
client Route53API
|
||||
dryRun bool
|
||||
maxChangeCount int
|
||||
evaluateTargetHealth bool
|
||||
// only consider hosted zones managing domains ending in this suffix
|
||||
domainFilter DomainFilter
|
||||
// filter hosted zones by id
|
||||
@ -96,8 +96,19 @@ type AWSProvider struct {
|
||||
zoneTypeFilter ZoneTypeFilter
|
||||
}
|
||||
|
||||
// AWSConfig contains configuration to create a new AWS provider.
|
||||
type AWSConfig struct {
|
||||
DomainFilter DomainFilter
|
||||
ZoneIDFilter ZoneIDFilter
|
||||
ZoneTypeFilter ZoneTypeFilter
|
||||
MaxChangeCount int
|
||||
EvaluateTargetHealth bool
|
||||
AssumeRole string
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// NewAWSProvider initializes a new AWS Route53 based Provider.
|
||||
func NewAWSProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, assumeRole string, dryRun bool) (*AWSProvider, error) {
|
||||
func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
|
||||
config := aws.NewConfig()
|
||||
|
||||
config.WithHTTPClient(
|
||||
@ -117,17 +128,19 @@ 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))
|
||||
if awsConfig.AssumeRole != "" {
|
||||
log.Infof("Assuming role: %s", awsConfig.AssumeRole)
|
||||
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole))
|
||||
}
|
||||
|
||||
provider := &AWSProvider{
|
||||
client: route53.New(session),
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
zoneTypeFilter: zoneTypeFilter,
|
||||
dryRun: dryRun,
|
||||
client: route53.New(session),
|
||||
domainFilter: awsConfig.DomainFilter,
|
||||
zoneIDFilter: awsConfig.ZoneIDFilter,
|
||||
zoneTypeFilter: awsConfig.ZoneTypeFilter,
|
||||
maxChangeCount: awsConfig.MaxChangeCount,
|
||||
evaluateTargetHealth: awsConfig.EvaluateTargetHealth,
|
||||
dryRun: awsConfig.DryRun,
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
@ -231,26 +244,26 @@ func (p *AWSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
|
||||
|
||||
// CreateRecords creates a given set of DNS records in the given hosted zone.
|
||||
func (p *AWSProvider) CreateRecords(endpoints []*endpoint.Endpoint) error {
|
||||
return p.submitChanges(newChanges(route53.ChangeActionCreate, endpoints))
|
||||
return p.submitChanges(p.newChanges(route53.ChangeActionCreate, endpoints))
|
||||
}
|
||||
|
||||
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
|
||||
func (p *AWSProvider) UpdateRecords(endpoints, _ []*endpoint.Endpoint) error {
|
||||
return p.submitChanges(newChanges(route53.ChangeActionUpsert, endpoints))
|
||||
return p.submitChanges(p.newChanges(route53.ChangeActionUpsert, endpoints))
|
||||
}
|
||||
|
||||
// DeleteRecords deletes a given set of DNS records in a given zone.
|
||||
func (p *AWSProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error {
|
||||
return p.submitChanges(newChanges(route53.ChangeActionDelete, endpoints))
|
||||
return p.submitChanges(p.newChanges(route53.ChangeActionDelete, endpoints))
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *AWSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionUpsert, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, newChanges(route53.ChangeActionDelete, changes.Delete)...)
|
||||
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionUpsert, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(combinedChanges)
|
||||
}
|
||||
@ -270,9 +283,12 @@ func (p *AWSProvider) submitChanges(changes []*route53.Change) error {
|
||||
|
||||
// separate into per-zone change sets to be passed to the API.
|
||||
changesByZone := changesByZone(zones, changes)
|
||||
if len(changesByZone) == 0 {
|
||||
log.Info("All records are already up to date, there are no changes for the matching hosted zones")
|
||||
}
|
||||
|
||||
for z, cs := range changesByZone {
|
||||
limCs := limitChangeSet(cs, maxChangeCount)
|
||||
limCs := limitChangeSet(cs, p.maxChangeCount)
|
||||
|
||||
for _, c := range limCs {
|
||||
log.Infof("Desired change: %s %s %s", *c.Action, *c.ResourceRecordSet.Name, *c.ResourceRecordSet.Type)
|
||||
@ -297,6 +313,53 @@ func (p *AWSProvider) submitChanges(changes []*route53.Change) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newChanges returns a collection of Changes based on the given records and action.
|
||||
func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint) []*route53.Change {
|
||||
changes := make([]*route53.Change, 0, len(endpoints))
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
changes = append(changes, p.newChange(action, endpoint))
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// newChange returns a Change of the given record by the given action, e.g.
|
||||
// action=ChangeActionCreate returns a change for creation of the record and
|
||||
// action=ChangeActionDelete returns a change for deletion of the record.
|
||||
func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
|
||||
change := &route53.Change{
|
||||
Action: aws.String(action),
|
||||
ResourceRecordSet: &route53.ResourceRecordSet{
|
||||
Name: aws.String(endpoint.DNSName),
|
||||
},
|
||||
}
|
||||
|
||||
if isAWSLoadBalancer(endpoint) {
|
||||
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
|
||||
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
|
||||
DNSName: aws.String(endpoint.Targets[0]),
|
||||
HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])),
|
||||
EvaluateTargetHealth: aws.Bool(p.evaluateTargetHealth),
|
||||
}
|
||||
} else {
|
||||
change.ResourceRecordSet.Type = aws.String(endpoint.RecordType)
|
||||
if !endpoint.RecordTTL.IsConfigured() {
|
||||
change.ResourceRecordSet.TTL = aws.Int64(recordTTL)
|
||||
} else {
|
||||
change.ResourceRecordSet.TTL = aws.Int64(int64(endpoint.RecordTTL))
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
func limitChangeSet(cs []*route53.Change, limit int) []*route53.Change {
|
||||
if len(cs) <= limit {
|
||||
return cs
|
||||
@ -381,53 +444,6 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet []*route53.Ch
|
||||
return changes
|
||||
}
|
||||
|
||||
// newChanges returns a collection of Changes based on the given records and action.
|
||||
func newChanges(action string, endpoints []*endpoint.Endpoint) []*route53.Change {
|
||||
changes := make([]*route53.Change, 0, len(endpoints))
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
changes = append(changes, newChange(action, endpoint))
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// newChange returns a Change of the given record by the given action, e.g.
|
||||
// action=ChangeActionCreate returns a change for creation of the record and
|
||||
// action=ChangeActionDelete returns a change for deletion of the record.
|
||||
func newChange(action string, endpoint *endpoint.Endpoint) *route53.Change {
|
||||
change := &route53.Change{
|
||||
Action: aws.String(action),
|
||||
ResourceRecordSet: &route53.ResourceRecordSet{
|
||||
Name: aws.String(endpoint.DNSName),
|
||||
},
|
||||
}
|
||||
|
||||
if isAWSLoadBalancer(endpoint) {
|
||||
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
|
||||
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
|
||||
DNSName: aws.String(endpoint.Targets[0]),
|
||||
HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])),
|
||||
EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
|
||||
}
|
||||
} else {
|
||||
change.ResourceRecordSet.Type = aws.String(endpoint.RecordType)
|
||||
if !endpoint.RecordTTL.IsConfigured() {
|
||||
change.ResourceRecordSet.TTL = aws.Int64(recordTTL)
|
||||
} else {
|
||||
change.ResourceRecordSet.TTL = aws.Int64(int64(endpoint.RecordTTL))
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
// suitableZones returns all suitable private zones and the most suitable public zone
|
||||
// for a given hostname and a set of zones.
|
||||
func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*route53.HostedZone {
|
||||
@ -435,7 +451,7 @@ func suitableZones(hostname string, zones map[string]*route53.HostedZone) []*rou
|
||||
var publicZone *route53.HostedZone
|
||||
|
||||
for _, z := range zones {
|
||||
if strings.HasSuffix(hostname, aws.StringValue(z.Name)) {
|
||||
if aws.StringValue(z.Name) == hostname || strings.HasSuffix(hostname, "."+aws.StringValue(z.Name)) {
|
||||
if z.Config == nil || !aws.BoolValue(z.Config.PrivateZone) {
|
||||
// Only select the best matching public zone
|
||||
if publicZone == nil || len(aws.StringValue(z.Name)) > len(aws.StringValue(publicZone.Name)) {
|
||||
|
@ -32,6 +32,11 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxChangeCount = 4000
|
||||
defaultEvaluateTargetHealth = true
|
||||
)
|
||||
|
||||
// Compile time check for interface conformance
|
||||
var _ Route53API = &Route53APIStub{}
|
||||
|
||||
@ -194,7 +199,7 @@ func TestAWSZones(t *testing.T) {
|
||||
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), noZones},
|
||||
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), privateZones},
|
||||
} {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, false, []*endpoint.Endpoint{})
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
|
||||
zones, err := provider.Zones()
|
||||
require.NoError(t, err)
|
||||
@ -204,7 +209,7 @@ 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{
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
|
||||
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"),
|
||||
@ -228,7 +233,7 @@ func TestAWSRecords(t *testing.T) {
|
||||
|
||||
func TestAWSCreateRecords(t *testing.T) {
|
||||
customTTL := endpoint.TTL(60)
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
|
||||
records := []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
|
||||
@ -253,7 +258,7 @@ func TestAWSCreateRecords(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAWSUpdateRecords(t *testing.T) {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
|
||||
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"),
|
||||
@ -296,7 +301,7 @@ func TestAWSDeleteRecords(t *testing.T) {
|
||||
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)
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, originalEndpoints)
|
||||
|
||||
require.NoError(t, provider.DeleteRecords(originalEndpoints))
|
||||
|
||||
@ -308,7 +313,7 @@ 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{
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{
|
||||
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"),
|
||||
@ -392,7 +397,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
|
||||
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)
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, true, originalEndpoints)
|
||||
|
||||
createRecords := []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
|
||||
@ -538,9 +543,9 @@ func TestAWSChangesByZones(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAWSsubmitChanges(t *testing.T) {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
const subnets = 16
|
||||
const hosts = maxChangeCount / subnets
|
||||
const hosts = defaultMaxChangeCount / subnets
|
||||
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
for i := 0; i < subnets; i++ {
|
||||
@ -553,7 +558,7 @@ func TestAWSsubmitChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
cs := make([]*route53.Change, 0, len(endpoints))
|
||||
cs = append(cs, newChanges(route53.ChangeActionCreate, endpoints)...)
|
||||
cs = append(cs, provider.newChanges(route53.ChangeActionCreate, endpoints)...)
|
||||
|
||||
require.NoError(t, provider.submitChanges(cs))
|
||||
|
||||
@ -566,7 +571,7 @@ func TestAWSsubmitChanges(t *testing.T) {
|
||||
func TestAWSLimitChangeSet(t *testing.T) {
|
||||
var cs []*route53.Change
|
||||
|
||||
for i := 1; i <= maxChangeCount; i += 2 {
|
||||
for i := 1; i <= defaultMaxChangeCount; i += 2 {
|
||||
cs = append(cs, &route53.Change{
|
||||
Action: aws.String(route53.ChangeActionCreate),
|
||||
ResourceRecordSet: &route53.ResourceRecordSet{
|
||||
@ -583,7 +588,7 @@ func TestAWSLimitChangeSet(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
limCs := limitChangeSet(cs, maxChangeCount)
|
||||
limCs := limitChangeSet(cs, defaultMaxChangeCount)
|
||||
|
||||
// sorting cs not needed as it should be returned as is
|
||||
validateAWSChangeRecords(t, limCs, cs)
|
||||
@ -650,7 +655,7 @@ func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *rou
|
||||
}
|
||||
|
||||
func TestAWSCreateRecordsWithCNAME(t *testing.T) {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
|
||||
records := []*endpoint.Endpoint{
|
||||
{DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Targets: endpoint.Targets{"foo.example.org"}, RecordType: endpoint.RecordTypeCNAME},
|
||||
@ -675,27 +680,32 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAWSCreateRecordsWithALIAS(t *testing.T) {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{})
|
||||
for _, evaluateTargetHealth := range []bool{
|
||||
true,
|
||||
false,
|
||||
} {
|
||||
provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), evaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
|
||||
records := []*endpoint.Endpoint{
|
||||
{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},
|
||||
}
|
||||
records := []*endpoint.Endpoint{
|
||||
{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))
|
||||
require.NoError(t, provider.CreateRecords(records))
|
||||
|
||||
recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
|
||||
recordSets := listAWSRecords(t, provider.client, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.")
|
||||
|
||||
validateRecords(t, recordSets, []*route53.ResourceRecordSet{
|
||||
{
|
||||
AliasTarget: &route53.AliasTarget{
|
||||
DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."),
|
||||
EvaluateTargetHealth: aws.Bool(true),
|
||||
HostedZoneId: aws.String("Z215JYRZR1TBD5"),
|
||||
validateRecords(t, recordSets, []*route53.ResourceRecordSet{
|
||||
{
|
||||
AliasTarget: &route53.AliasTarget{
|
||||
DNSName: aws.String("foo.eu-central-1.elb.amazonaws.com."),
|
||||
EvaluateTargetHealth: aws.Bool(evaluateTargetHealth),
|
||||
HostedZoneId: aws.String("Z215JYRZR1TBD5"),
|
||||
},
|
||||
Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: aws.String(endpoint.RecordTypeA),
|
||||
},
|
||||
Name: aws.String("create-test.zone-1.ext-dns-test-2.teapot.zalan.do."),
|
||||
Type: aws.String(endpoint.RecordTypeA),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSisLoadBalancer(t *testing.T) {
|
||||
@ -779,6 +789,13 @@ func TestAWSSuitableZones(t *testing.T) {
|
||||
hostname string
|
||||
expected []*route53.HostedZone
|
||||
}{
|
||||
// bar.example.org is NOT suitable
|
||||
{"foobar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}},
|
||||
|
||||
// all matching private zones are suitable
|
||||
// https://github.com/kubernetes-incubator/external-dns/pull/356
|
||||
{"bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
|
||||
|
||||
{"foo.bar.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["bar-example-org-private"], zones["bar-example-org"]}},
|
||||
{"foo.example.org.", []*route53.HostedZone{zones["example-org-private"], zones["example-org"]}},
|
||||
{"foo.kubernetes.io.", nil},
|
||||
@ -859,15 +876,17 @@ func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *AWSProvider {
|
||||
func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) *AWSProvider {
|
||||
client := NewRoute53APIStub()
|
||||
|
||||
provider := &AWSProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
zoneTypeFilter: zoneTypeFilter,
|
||||
dryRun: false,
|
||||
client: client,
|
||||
maxChangeCount: defaultMaxChangeCount,
|
||||
evaluateTargetHealth: evaluateTargetHealth,
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
zoneTypeFilter: zoneTypeFilter,
|
||||
dryRun: false,
|
||||
}
|
||||
|
||||
createAWSZone(t, provider, &route53.HostedZone{
|
||||
|
@ -40,13 +40,14 @@ const (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Cloud string `json:"cloud" yaml:"cloud"`
|
||||
TenantID string `json:"tenantId" yaml:"tenantId"`
|
||||
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
|
||||
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
|
||||
Location string `json:"location" yaml:"location"`
|
||||
ClientID string `json:"aadClientId" yaml:"aadClientId"`
|
||||
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
|
||||
Cloud string `json:"cloud" yaml:"cloud"`
|
||||
TenantID string `json:"tenantId" yaml:"tenantId"`
|
||||
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
|
||||
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
|
||||
Location string `json:"location" yaml:"location"`
|
||||
ClientID string `json:"aadClientId" yaml:"aadClientId"`
|
||||
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
|
||||
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
|
||||
}
|
||||
|
||||
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
|
||||
@ -102,14 +103,9 @@ func NewAzureProvider(configFile string, domainFilter DomainFilter, zoneIDFilter
|
||||
}
|
||||
}
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
|
||||
token, err := getAccessToken(cfg, environment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
|
||||
}
|
||||
|
||||
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service principal token: %v", err)
|
||||
return nil, fmt.Errorf("failed to get token: %v", err)
|
||||
}
|
||||
|
||||
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
@ -128,6 +124,41 @@ func NewAzureProvider(configFile string, domainFilter DomainFilter, zoneIDFilter
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// getAccessToken retrieves Azure API access token.
|
||||
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
|
||||
// Try to retrive token with MSI.
|
||||
if cfg.UseManagedIdentityExtension {
|
||||
log.Info("Using managed identity extension to retrieve access token for Azure API.")
|
||||
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
|
||||
}
|
||||
|
||||
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Try to retrieve token with service principal credentials.
|
||||
if len(cfg.ClientID) > 0 && len(cfg.ClientSecret) > 0 {
|
||||
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
|
||||
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
|
||||
}
|
||||
|
||||
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service principal token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no credentials provided for Azure API")
|
||||
}
|
||||
|
||||
// Records gets the current records.
|
||||
//
|
||||
// Returns the current records or an error if the operation failed.
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/arm/dns"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
@ -302,3 +303,18 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClie
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureGetAccessToken(t *testing.T) {
|
||||
env := azure.PublicCloud
|
||||
cfg := config{
|
||||
ClientID: "",
|
||||
ClientSecret: "",
|
||||
TenantID: "",
|
||||
UseManagedIdentityExtension: false,
|
||||
}
|
||||
|
||||
_, err := getAccessToken(cfg, env)
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail, but got no error")
|
||||
}
|
||||
}
|
||||
|
398
provider/coredns.go
Normal file
398
provider/coredns.go
Normal file
@ -0,0 +1,398 @@
|
||||
/*
|
||||
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 (
|
||||
"container/list"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// skyDNSClient is an interface to work with SkyDNS service records in etcd
|
||||
type skyDNSClient interface {
|
||||
GetServices(prefix string) ([]*Service, error)
|
||||
SaveService(value *Service) error
|
||||
DeleteService(key string) error
|
||||
}
|
||||
|
||||
type coreDNSProvider struct {
|
||||
dryRun bool
|
||||
domainFilter DomainFilter
|
||||
client skyDNSClient
|
||||
}
|
||||
|
||||
// Service represents SkyDNS/CoreDNS etcd record
|
||||
type Service struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
|
||||
// When a SRV record with a "Host: IP-address" is added, we synthesize
|
||||
// a srv.Target domain name. Normally we convert the full Key where
|
||||
// the record lives to a DNS name and use this as the srv.Target. When
|
||||
// TargetStrip > 0 we strip the left most TargetStrip labels from the
|
||||
// DNS name.
|
||||
TargetStrip int `json:"targetstrip,omitempty"`
|
||||
|
||||
// Group is used to group (or *not* to group) different services
|
||||
// together. Services with an identical Group are returned in the same
|
||||
// answer.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Etcd key where we found this service and ignored from json un-/marshalling
|
||||
Key string `json:"-"`
|
||||
}
|
||||
|
||||
type etcdClient struct {
|
||||
api etcd.KeysAPI
|
||||
}
|
||||
|
||||
var _ skyDNSClient = etcdClient{}
|
||||
|
||||
// GetService return all Service records stored in etcd stored anywhere under the given key (recursively)
|
||||
func (c etcdClient) GetServices(prefix string) ([]*Service, error) {
|
||||
var result []*Service
|
||||
opts := &etcd.GetOptions{Recursive: true}
|
||||
data, err := c.api.Get(context.Background(), prefix, opts)
|
||||
if err != nil {
|
||||
if etcd.IsKeyNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queue := list.New()
|
||||
queue.PushFront(data.Node)
|
||||
for queueNode := queue.Front(); queueNode != nil; queueNode = queueNode.Next() {
|
||||
node := queueNode.Value.(*etcd.Node)
|
||||
if node.Dir {
|
||||
for _, childNode := range node.Nodes {
|
||||
queue.PushBack(childNode)
|
||||
}
|
||||
continue
|
||||
}
|
||||
service := &Service{}
|
||||
err = json.Unmarshal([]byte(node.Value), service)
|
||||
if err != nil {
|
||||
log.Error("Cannot parse JSON value ", node.Value)
|
||||
continue
|
||||
}
|
||||
service.Key = node.Key
|
||||
result = append(result, service)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SaveService persists service data into etcd
|
||||
func (c etcdClient) SaveService(service *Service) error {
|
||||
value, err := json.Marshal(&service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.api.Set(context.Background(), service.Key, string(value), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteService deletes service record from etcd
|
||||
func (c etcdClient) DeleteService(key string) error {
|
||||
_, err := c.api.Delete(context.Background(), key, nil)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// loads TLS artifacts and builds tls.Clonfig object
|
||||
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
|
||||
}
|
||||
|
||||
// constructs http.Transport object for https protocol
|
||||
func newHTTPSTransport(cc *tls.Config) *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: cc,
|
||||
}
|
||||
}
|
||||
|
||||
// builds etcd client config depending on connection scheme and TLS parameters
|
||||
func getETCDConfig() (*etcd.Config, error) {
|
||||
etcdURLsStr := os.Getenv("ETCD_URLS")
|
||||
if etcdURLsStr == "" {
|
||||
etcdURLsStr = "http://localhost:2379"
|
||||
}
|
||||
etcdURLs := strings.Split(etcdURLsStr, ",")
|
||||
firstURL := strings.ToLower(etcdURLs[0])
|
||||
if strings.HasPrefix(firstURL, "http://") {
|
||||
return &etcd.Config{Endpoints: etcdURLs}, nil
|
||||
} else if strings.HasPrefix(firstURL, "https://") {
|
||||
caFile := os.Getenv("ETCD_CA_FILE")
|
||||
certFile := os.Getenv("ETCD_CERT_FILE")
|
||||
keyFile := os.Getenv("ETCD_KEY_FILE")
|
||||
serverName := os.Getenv("ETCD_TLS_SERVER_NAME")
|
||||
isInsecureStr := strings.ToLower(os.Getenv("ETCD_TLS_INSECURE"))
|
||||
isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
|
||||
tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &etcd.Config{
|
||||
Endpoints: etcdURLs,
|
||||
Transport: newHTTPSTransport(tlsConfig),
|
||||
}, nil
|
||||
} else {
|
||||
return nil, errors.New("etcd URLs must start with either http:// or https://")
|
||||
}
|
||||
}
|
||||
|
||||
//newETCDClient is an etcd client constructor
|
||||
func newETCDClient() (skyDNSClient, error) {
|
||||
cfg, err := getETCDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := etcd.New(*cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return etcdClient{etcd.NewKeysAPI(c)}, nil
|
||||
}
|
||||
|
||||
// NewCoreDNSProvider is a CoreDNS provider constructor
|
||||
func NewCoreDNSProvider(domainFilter DomainFilter, dryRun bool) (Provider, error) {
|
||||
client, err := newETCDClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return coreDNSProvider{
|
||||
client: client,
|
||||
dryRun: dryRun,
|
||||
domainFilter: domainFilter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Records returns all DNS records found in SkyDNS/CoreDNS etcd backend. Depending on the record fields
|
||||
// it may be mapped to one or two records of type A, CNAME, TXT, A+TXT, CNAME+TXT
|
||||
func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
var result []*endpoint.Endpoint
|
||||
services, err := p.client.GetServices("/skydns")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, service := range services {
|
||||
domains := strings.Split(strings.TrimPrefix(service.Key, "/skydns/"), "/")
|
||||
reverse(domains)
|
||||
dnsName := strings.Join(domains[service.TargetStrip:], ".")
|
||||
if !p.domainFilter.Match(dnsName) {
|
||||
continue
|
||||
}
|
||||
prefix := strings.Join(domains[:service.TargetStrip], ".")
|
||||
if service.Host != "" {
|
||||
ep := endpoint.NewEndpoint(
|
||||
dnsName,
|
||||
guessRecordType(service.Host),
|
||||
service.Host,
|
||||
)
|
||||
ep.Labels["originalText"] = service.Text
|
||||
ep.Labels["prefix"] = prefix
|
||||
result = append(result, ep)
|
||||
}
|
||||
if service.Text != "" {
|
||||
ep := endpoint.NewEndpoint(
|
||||
dnsName,
|
||||
endpoint.RecordTypeTXT,
|
||||
service.Text,
|
||||
)
|
||||
ep.Labels["prefix"] = prefix
|
||||
result = append(result, ep)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ApplyChanges stores changes back to etcd converting them to SkyDNS format and aggregating A/CNAME and TXT records
|
||||
func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
grouped := map[string][]*endpoint.Endpoint{}
|
||||
for _, ep := range changes.Create {
|
||||
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
|
||||
}
|
||||
for _, ep := range changes.UpdateNew {
|
||||
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
|
||||
}
|
||||
for dnsName, group := range grouped {
|
||||
if !p.domainFilter.Match(dnsName) {
|
||||
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", dnsName)
|
||||
continue
|
||||
}
|
||||
var services []Service
|
||||
for _, ep := range group {
|
||||
if ep.RecordType == endpoint.RecordTypeTXT {
|
||||
continue
|
||||
}
|
||||
prefix := ep.Labels["prefix"]
|
||||
if prefix == "" {
|
||||
prefix = fmt.Sprintf("%08x", rand.Int31())
|
||||
}
|
||||
service := Service{
|
||||
Host: ep.Targets[0],
|
||||
Text: ep.Labels["originalText"],
|
||||
Key: etcdKeyFor(prefix + "." + dnsName),
|
||||
TargetStrip: strings.Count(prefix, ".") + 1,
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
index := 0
|
||||
for _, ep := range group {
|
||||
if ep.RecordType != "TXT" {
|
||||
continue
|
||||
}
|
||||
if index >= len(services) {
|
||||
prefix := ep.Labels["prefix"]
|
||||
if prefix == "" {
|
||||
prefix = fmt.Sprintf("%08x", rand.Int31())
|
||||
}
|
||||
services = append(services, Service{
|
||||
Key: etcdKeyFor(prefix + "." + dnsName),
|
||||
TargetStrip: strings.Count(prefix, ".") + 1,
|
||||
})
|
||||
}
|
||||
services[index].Text = ep.Targets[0]
|
||||
index++
|
||||
}
|
||||
|
||||
for i := index; index > 0 && i < len(services); i++ {
|
||||
services[i].Text = ""
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
log.Infof("Add/set key %s to Host=%s, Text=%s", service.Key, service.Host, service.Text)
|
||||
if !p.dryRun {
|
||||
err := p.client.SaveService(&service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ep := range changes.Delete {
|
||||
dnsName := ep.DNSName
|
||||
if ep.Labels["prefix"] != "" {
|
||||
dnsName = ep.Labels["prefix"] + "." + dnsName
|
||||
}
|
||||
key := etcdKeyFor(dnsName)
|
||||
log.Infof("Delete key %s", key)
|
||||
if !p.dryRun {
|
||||
err := p.client.DeleteService(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func guessRecordType(target string) string {
|
||||
if net.ParseIP(target) != nil {
|
||||
return endpoint.RecordTypeA
|
||||
}
|
||||
return endpoint.RecordTypeCNAME
|
||||
}
|
||||
|
||||
func etcdKeyFor(dnsName string) string {
|
||||
domains := strings.Split(dnsName, ".")
|
||||
reverse(domains)
|
||||
return "/skydns/" + strings.Join(domains, "/")
|
||||
}
|
||||
|
||||
func reverse(slice []string) {
|
||||
for i := 0; i < len(slice)/2; i++ {
|
||||
j := len(slice) - i - 1
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
}
|
316
provider/coredns_test.go
Normal file
316
provider/coredns_test.go
Normal file
@ -0,0 +1,316 @@
|
||||
/*
|
||||
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 (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
type fakeETCDClient struct {
|
||||
services map[string]*Service
|
||||
}
|
||||
|
||||
func (c fakeETCDClient) GetServices(prefix string) ([]*Service, error) {
|
||||
var result []*Service
|
||||
for key, value := range c.services {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
value.Key = key
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c fakeETCDClient) SaveService(service *Service) error {
|
||||
c.services[service.Key] = service
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c fakeETCDClient) DeleteService(key string) error {
|
||||
delete(c.services, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAServiceTranslation(t *testing.T) {
|
||||
expectedTarget := "1.2.3.4"
|
||||
expectedDNSName := "example.com"
|
||||
expectedRecordType := endpoint.RecordTypeA
|
||||
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{
|
||||
"/skydns/com/example": {Host: expectedTarget},
|
||||
},
|
||||
}
|
||||
provider := coreDNSProvider{client: client}
|
||||
endpoints, err := provider.Records()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != 1 {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
if endpoints[0].DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCNAMEServiceTranslation(t *testing.T) {
|
||||
expectedTarget := "example.net"
|
||||
expectedDNSName := "example.com"
|
||||
expectedRecordType := endpoint.RecordTypeCNAME
|
||||
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{
|
||||
"/skydns/com/example": {Host: expectedTarget},
|
||||
},
|
||||
}
|
||||
provider := coreDNSProvider{client: client}
|
||||
endpoints, err := provider.Records()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != 1 {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
if endpoints[0].DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTXTServiceTranslation(t *testing.T) {
|
||||
expectedTarget := "string"
|
||||
expectedDNSName := "example.com"
|
||||
expectedRecordType := endpoint.RecordTypeTXT
|
||||
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{
|
||||
"/skydns/com/example": {Text: expectedTarget},
|
||||
},
|
||||
}
|
||||
provider := coreDNSProvider{client: client}
|
||||
endpoints, err := provider.Records()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != 1 {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
if endpoints[0].DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWithTXTServiceTranslation(t *testing.T) {
|
||||
expectedTargets := map[string]string{
|
||||
endpoint.RecordTypeA: "1.2.3.4",
|
||||
endpoint.RecordTypeTXT: "string",
|
||||
}
|
||||
expectedDNSName := "example.com"
|
||||
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{
|
||||
"/skydns/com/example": {Host: "1.2.3.4", Text: "string"},
|
||||
},
|
||||
}
|
||||
provider := coreDNSProvider{client: client}
|
||||
endpoints, err := provider.Records()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != len(expectedTargets) {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
expectedTarget := expectedTargets[ep.RecordType]
|
||||
if expectedTarget == "" {
|
||||
t.Errorf("got unexpected DNS record type: %s", ep.RecordType)
|
||||
continue
|
||||
}
|
||||
delete(expectedTargets, ep.RecordType)
|
||||
|
||||
if ep.DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
|
||||
}
|
||||
|
||||
if ep.Targets[0] != expectedTarget {
|
||||
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCNAMEWithTXTServiceTranslation(t *testing.T) {
|
||||
expectedTargets := map[string]string{
|
||||
endpoint.RecordTypeCNAME: "example.net",
|
||||
endpoint.RecordTypeTXT: "string",
|
||||
}
|
||||
expectedDNSName := "example.com"
|
||||
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{
|
||||
"/skydns/com/example": {Host: "example.net", Text: "string"},
|
||||
},
|
||||
}
|
||||
provider := coreDNSProvider{client: client}
|
||||
endpoints, err := provider.Records()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != len(expectedTargets) {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
expectedTarget := expectedTargets[ep.RecordType]
|
||||
if expectedTarget == "" {
|
||||
t.Errorf("got unexpected DNS record type: %s", ep.RecordType)
|
||||
continue
|
||||
}
|
||||
delete(expectedTargets, ep.RecordType)
|
||||
|
||||
if ep.DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
|
||||
}
|
||||
|
||||
if ep.Targets[0] != expectedTarget {
|
||||
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreDNSApplyChanges(t *testing.T) {
|
||||
client := fakeETCDClient{
|
||||
map[string]*Service{},
|
||||
}
|
||||
coredns := coreDNSProvider{client: client}
|
||||
|
||||
changes1 := &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
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)
|
||||
|
||||
expectedServices1 := map[string]*Service{
|
||||
"/skydns/local/domain1": {Host: "5.5.5.5", Text: "string1"},
|
||||
"/skydns/local/domain2": {Host: "site.local"},
|
||||
}
|
||||
validateServices(client.services, expectedServices1, t, 1)
|
||||
|
||||
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", endpoint.RecordTypeA, "7.7.7.7"),
|
||||
},
|
||||
UpdateNew: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("domain1.local", "A", "6.6.6.6"),
|
||||
},
|
||||
}
|
||||
applyServiceChanges(coredns, changes2)
|
||||
|
||||
expectedServices2 := map[string]*Service{
|
||||
"/skydns/local/domain1": {Host: "6.6.6.6", Text: "string1"},
|
||||
"/skydns/local/domain2": {Host: "site.local"},
|
||||
"/skydns/local/domain3": {Host: "7.7.7.7"},
|
||||
}
|
||||
validateServices(client.services, expectedServices2, t, 2)
|
||||
|
||||
changes3 := &plan.Changes{
|
||||
Delete: []*endpoint.Endpoint{
|
||||
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"),
|
||||
},
|
||||
}
|
||||
|
||||
applyServiceChanges(coredns, changes3)
|
||||
|
||||
expectedServices3 := map[string]*Service{
|
||||
"/skydns/local/domain2": {Host: "site.local"},
|
||||
}
|
||||
validateServices(client.services, expectedServices3, t, 3)
|
||||
}
|
||||
|
||||
func applyServiceChanges(provider coreDNSProvider, changes *plan.Changes) {
|
||||
records, _ := provider.Records()
|
||||
for _, col := range [][]*endpoint.Endpoint{changes.Create, changes.UpdateNew, changes.Delete} {
|
||||
for _, record := range col {
|
||||
for _, existingRecord := range records {
|
||||
if existingRecord.DNSName == record.DNSName && existingRecord.RecordType == record.RecordType {
|
||||
mergeLabels(record, existingRecord.Labels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
provider.ApplyChanges(changes)
|
||||
}
|
||||
|
||||
func validateServices(services, expectedServices map[string]*Service, t *testing.T, step int) {
|
||||
if len(services) != len(expectedServices) {
|
||||
t.Errorf("wrong number of records on step %d: %d != %d", step, len(services), len(expectedServices))
|
||||
}
|
||||
for key, value := range services {
|
||||
keyParts := strings.Split(key, "/")
|
||||
expectedKey := strings.Join(keyParts[:len(keyParts)-value.TargetStrip], "/")
|
||||
expectedService := expectedServices[expectedKey]
|
||||
if expectedService == nil {
|
||||
t.Errorf("unexpected service %s", key)
|
||||
continue
|
||||
}
|
||||
delete(expectedServices, key)
|
||||
if value.Host != expectedService.Host {
|
||||
t.Errorf("wrong host for service %s: %s != %s on step %d", key, value.Host, expectedService.Host, step)
|
||||
}
|
||||
if value.Text != expectedService.Text {
|
||||
t.Errorf("wrong text for service %s: %s != %s on step %d", key, value.Text, expectedService.Text, step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeLabels adds keys to labels if not defined for the endpoint
|
||||
func mergeLabels(e *endpoint.Endpoint, labels map[string]string) {
|
||||
for k, v := range labels {
|
||||
if e.Labels[k] == "" {
|
||||
e.Labels[k] = v
|
||||
}
|
||||
}
|
||||
}
|
@ -214,6 +214,12 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro
|
||||
change.ResourceRecordSet.Name = "@"
|
||||
}
|
||||
|
||||
// for some reason the DO API requires the '.' at the end of "data" in case of CNAME request
|
||||
// Example: {"type":"CNAME","name":"hello","data":"www.example.com."}
|
||||
if change.ResourceRecordSet.Type == endpoint.RecordTypeCNAME {
|
||||
change.ResourceRecordSet.Data += "."
|
||||
}
|
||||
|
||||
switch change.Action {
|
||||
case DigitalOceanCreate:
|
||||
_, _, err = p.Client.CreateRecord(context.TODO(), zoneName,
|
||||
|
@ -436,7 +436,7 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
|
||||
}
|
||||
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"}}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME"}}
|
||||
err := provider.ApplyChanges(changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
|
@ -91,7 +91,7 @@ func (c *cache) Get(link string) *endpoint.Endpoint {
|
||||
return result.ep
|
||||
}
|
||||
|
||||
// DynConfig hold connection parameters to dyn.com and interanl state
|
||||
// DynConfig hold connection parameters to dyn.com and internal state
|
||||
type DynConfig struct {
|
||||
DomainFilter DomainFilter
|
||||
ZoneIDFilter ZoneIDFilter
|
||||
|
255
provider/exoscale.go
Normal file
255
provider/exoscale.go
Normal file
@ -0,0 +1,255 @@
|
||||
/*
|
||||
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 (
|
||||
"strings"
|
||||
|
||||
"github.com/exoscale/egoscale"
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// EgoscaleClientI for replaceable implementation
|
||||
type EgoscaleClientI interface {
|
||||
GetRecords(string) ([]egoscale.DNSRecord, error)
|
||||
GetDomains() ([]egoscale.DNSDomain, error)
|
||||
CreateRecord(string, egoscale.DNSRecord) (*egoscale.DNSRecord, error)
|
||||
DeleteRecord(string, int64) error
|
||||
UpdateRecord(string, egoscale.UpdateDNSRecord) (*egoscale.DNSRecord, error)
|
||||
}
|
||||
|
||||
// ExoscaleProvider initialized as dns provider with no records
|
||||
type ExoscaleProvider struct {
|
||||
domain DomainFilter
|
||||
client EgoscaleClientI
|
||||
filter *zoneFilter
|
||||
OnApplyChanges func(changes *plan.Changes)
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
// ExoscaleOption for Provider options
|
||||
type ExoscaleOption func(*ExoscaleProvider)
|
||||
|
||||
// NewExoscaleProvider returns ExoscaleProvider DNS provider interface implementation
|
||||
func NewExoscaleProvider(endpoint, apiKey, apiSecret string, dryRun bool, opts ...ExoscaleOption) *ExoscaleProvider {
|
||||
client := egoscale.NewClient(endpoint, apiKey, apiSecret)
|
||||
return NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret, client, dryRun, opts...)
|
||||
}
|
||||
|
||||
// NewExoscaleProviderWithClient returns ExoscaleProvider DNS provider interface implementation (Client provided)
|
||||
func NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret string, client EgoscaleClientI, dryRun bool, opts ...ExoscaleOption) *ExoscaleProvider {
|
||||
ep := &ExoscaleProvider{
|
||||
filter: &zoneFilter{},
|
||||
OnApplyChanges: func(changes *plan.Changes) {},
|
||||
domain: NewDomainFilter([]string{""}),
|
||||
client: client,
|
||||
dryRun: dryRun,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(ep)
|
||||
}
|
||||
return ep
|
||||
}
|
||||
|
||||
func (ep *ExoscaleProvider) getZones() (map[int64]string, error) {
|
||||
dom, err := ep.client.GetDomains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zones := map[int64]string{}
|
||||
for _, d := range dom {
|
||||
zones[d.ID] = d.Name
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
// ApplyChanges simply modifies DNS via exoscale API
|
||||
func (ep *ExoscaleProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
ep.OnApplyChanges(changes)
|
||||
|
||||
if ep.dryRun {
|
||||
log.Infof("Will NOT delete these records: %+v", changes.Delete)
|
||||
log.Infof("Will NOT create these records: %+v", changes.Create)
|
||||
log.Infof("Will NOT update these records: %+v", merge(changes.UpdateOld, changes.UpdateNew))
|
||||
return nil
|
||||
}
|
||||
|
||||
zones, err := ep.getZones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, epoint := range changes.Create {
|
||||
if ep.domain.Match(epoint.DNSName) {
|
||||
if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 {
|
||||
rec := egoscale.DNSRecord{
|
||||
Name: name,
|
||||
RecordType: epoint.RecordType,
|
||||
TTL: int(epoint.RecordTTL),
|
||||
Content: epoint.Targets[0],
|
||||
}
|
||||
_, err := ep.client.CreateRecord(zones[zoneID], rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, epoint := range changes.UpdateNew {
|
||||
if ep.domain.Match(epoint.DNSName) {
|
||||
if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 {
|
||||
records, err := ep.client.GetRecords(zones[zoneID])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range records {
|
||||
if r.Name == name {
|
||||
rec := egoscale.UpdateDNSRecord{
|
||||
ID: r.ID,
|
||||
DomainID: r.DomainID,
|
||||
Name: name,
|
||||
RecordType: epoint.RecordType,
|
||||
TTL: int(epoint.RecordTTL),
|
||||
Content: epoint.Targets[0],
|
||||
Prio: r.Prio,
|
||||
}
|
||||
if _, err := ep.client.UpdateRecord(zones[zoneID], rec); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, epoint := range changes.UpdateOld {
|
||||
// Since Exoscale "Patches", we ignore UpdateOld
|
||||
// We leave this logging here for information
|
||||
log.Debugf("UPDATE-OLD (ignored) for epoint: %+v", epoint)
|
||||
}
|
||||
for _, epoint := range changes.Delete {
|
||||
if ep.domain.Match(epoint.DNSName) {
|
||||
if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 {
|
||||
records, err := ep.client.GetRecords(zones[zoneID])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if r.Name == name {
|
||||
if err := ep.client.DeleteRecord(zones[zoneID], r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Records returns the list of endpoints
|
||||
func (ep *ExoscaleProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
|
||||
dom, err := ep.client.GetDomains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range dom {
|
||||
record, err := ep.client.GetRecords(d.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range record {
|
||||
switch r.RecordType {
|
||||
case "A", "CNAME", "TXT":
|
||||
break
|
||||
default:
|
||||
continue
|
||||
}
|
||||
ep := endpoint.NewEndpointWithTTL(r.Name+"."+d.Name, r.RecordType, endpoint.TTL(r.TTL), r.Content)
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("called Records() with %d items", len(endpoints))
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ExoscaleWithDomain modifies the domain on which dns zones are filtered
|
||||
func ExoscaleWithDomain(domainFilter DomainFilter) ExoscaleOption {
|
||||
return func(p *ExoscaleProvider) {
|
||||
p.domain = domainFilter
|
||||
}
|
||||
}
|
||||
|
||||
// ExoscaleWithLogging injects logging when ApplyChanges is called
|
||||
func ExoscaleWithLogging() ExoscaleOption {
|
||||
return func(p *ExoscaleProvider) {
|
||||
p.OnApplyChanges = func(changes *plan.Changes) {
|
||||
for _, v := range changes.Create {
|
||||
log.Infof("CREATE: %v", v)
|
||||
}
|
||||
for _, v := range changes.UpdateOld {
|
||||
log.Infof("UPDATE (old): %v", v)
|
||||
}
|
||||
for _, v := range changes.UpdateNew {
|
||||
log.Infof("UPDATE (new): %v", v)
|
||||
}
|
||||
for _, v := range changes.Delete {
|
||||
log.Infof("DELETE: %v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type zoneFilter struct {
|
||||
domain string
|
||||
}
|
||||
|
||||
// Zones filters map[zoneID]zoneName for names having f.domain as suffix
|
||||
func (f *zoneFilter) Zones(zones map[int64]string) map[int64]string {
|
||||
result := map[int64]string{}
|
||||
for zoneID, zoneName := range zones {
|
||||
if strings.HasSuffix(zoneName, f.domain) {
|
||||
result[zoneID] = zoneName
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName
|
||||
// returns 0 if no match found
|
||||
func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64]string) (zoneID int64, name string) {
|
||||
var matchZoneID int64
|
||||
var matchZoneName string
|
||||
for zoneID, zoneName := range zones {
|
||||
if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) {
|
||||
matchZoneName = zoneName
|
||||
matchZoneID = zoneID
|
||||
name = strings.TrimSuffix(endpoint.DNSName, "."+zoneName)
|
||||
}
|
||||
}
|
||||
return matchZoneID, name
|
||||
}
|
189
provider/exoscale_test.go
Normal file
189
provider/exoscale_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/exoscale/egoscale"
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type createRecordExoscale struct {
|
||||
name string
|
||||
rec egoscale.DNSRecord
|
||||
}
|
||||
|
||||
type deleteRecordExoscale struct {
|
||||
name string
|
||||
recordID int64
|
||||
}
|
||||
|
||||
type updateRecordExoscale struct {
|
||||
name string
|
||||
updateDNSRecord egoscale.UpdateDNSRecord
|
||||
}
|
||||
|
||||
var createExoscale []createRecordExoscale
|
||||
var deleteExoscale []deleteRecordExoscale
|
||||
var updateExoscale []updateRecordExoscale
|
||||
|
||||
type ExoscaleClientStub struct {
|
||||
}
|
||||
|
||||
func NewExoscaleClientStub() EgoscaleClientI {
|
||||
ep := &ExoscaleClientStub{}
|
||||
return ep
|
||||
}
|
||||
|
||||
func (ep *ExoscaleClientStub) DeleteRecord(name string, recordID int64) error {
|
||||
deleteExoscale = append(deleteExoscale, deleteRecordExoscale{name: name, recordID: recordID})
|
||||
return nil
|
||||
}
|
||||
func (ep *ExoscaleClientStub) GetRecords(name string) ([]egoscale.DNSRecord, error) {
|
||||
init := []egoscale.DNSRecord{
|
||||
{ID: 0, Name: "v4.barfoo.com", RecordType: "ALIAS"},
|
||||
{ID: 1, Name: "v1.foo.com", RecordType: "TXT"},
|
||||
{ID: 2, Name: "v2.bar.com", RecordType: "A"},
|
||||
{ID: 3, Name: "v3.bar.com", RecordType: "ALIAS"},
|
||||
{ID: 4, Name: "v2.foo.com", RecordType: "CNAME"},
|
||||
{ID: 5, Name: "v1.foobar.com", RecordType: "TXT"},
|
||||
}
|
||||
|
||||
rec := make([]egoscale.DNSRecord, 0)
|
||||
for _, r := range init {
|
||||
if strings.HasSuffix(r.Name, "."+name) {
|
||||
r.Name = strings.TrimSuffix(r.Name, "."+name)
|
||||
rec = append(rec, r)
|
||||
}
|
||||
}
|
||||
|
||||
return rec, nil
|
||||
}
|
||||
func (ep *ExoscaleClientStub) UpdateRecord(name string, rec egoscale.UpdateDNSRecord) (*egoscale.DNSRecord, error) {
|
||||
updateExoscale = append(updateExoscale, updateRecordExoscale{name: name, updateDNSRecord: rec})
|
||||
return nil, nil
|
||||
}
|
||||
func (ep *ExoscaleClientStub) CreateRecord(name string, rec egoscale.DNSRecord) (*egoscale.DNSRecord, error) {
|
||||
createExoscale = append(createExoscale, createRecordExoscale{name: name, rec: rec})
|
||||
return nil, nil
|
||||
}
|
||||
func (ep *ExoscaleClientStub) GetDomains() ([]egoscale.DNSDomain, error) {
|
||||
dom := []egoscale.DNSDomain{
|
||||
{ID: 1, Name: "foo.com"},
|
||||
{ID: 2, Name: "bar.com"},
|
||||
}
|
||||
return dom, nil
|
||||
}
|
||||
|
||||
func contains(arr []*endpoint.Endpoint, name string) bool {
|
||||
for _, a := range arr {
|
||||
if a.DNSName == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestExoscaleGetRecords(t *testing.T) {
|
||||
provider := NewExoscaleProviderWithClient("", "", "", NewExoscaleClientStub(), false)
|
||||
|
||||
if recs, err := provider.Records(); err == nil {
|
||||
assert.Equal(t, 3, len(recs))
|
||||
assert.True(t, contains(recs, "v1.foo.com"))
|
||||
assert.True(t, contains(recs, "v2.bar.com"))
|
||||
assert.True(t, contains(recs, "v2.foo.com"))
|
||||
assert.False(t, contains(recs, "v3.bar.com"))
|
||||
assert.False(t, contains(recs, "v1.foobar.com"))
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExoscaleApplyChanges(t *testing.T) {
|
||||
provider := NewExoscaleProviderWithClient("", "", "", NewExoscaleClientStub(), false)
|
||||
|
||||
plan := &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "v1.foo.com",
|
||||
RecordType: "A",
|
||||
Targets: []string{""},
|
||||
},
|
||||
{
|
||||
DNSName: "v1.foobar.com",
|
||||
RecordType: "TXT",
|
||||
Targets: []string{""},
|
||||
},
|
||||
},
|
||||
Delete: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "v1.foo.com",
|
||||
RecordType: "A",
|
||||
Targets: []string{""},
|
||||
},
|
||||
{
|
||||
DNSName: "v1.foobar.com",
|
||||
RecordType: "TXT",
|
||||
Targets: []string{""},
|
||||
},
|
||||
},
|
||||
UpdateOld: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "v1.foo.com",
|
||||
RecordType: "A",
|
||||
Targets: []string{""},
|
||||
},
|
||||
{
|
||||
DNSName: "v1.foobar.com",
|
||||
RecordType: "TXT",
|
||||
Targets: []string{""},
|
||||
},
|
||||
},
|
||||
UpdateNew: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "v1.foo.com",
|
||||
RecordType: "A",
|
||||
Targets: []string{""},
|
||||
},
|
||||
{
|
||||
DNSName: "v1.foobar.com",
|
||||
RecordType: "TXT",
|
||||
Targets: []string{""},
|
||||
},
|
||||
},
|
||||
}
|
||||
createExoscale = make([]createRecordExoscale, 0)
|
||||
deleteExoscale = make([]deleteRecordExoscale, 0)
|
||||
|
||||
provider.ApplyChanges(plan)
|
||||
|
||||
assert.Equal(t, 1, len(createExoscale))
|
||||
assert.Equal(t, "foo.com", createExoscale[0].name)
|
||||
assert.Equal(t, "v1", createExoscale[0].rec.Name)
|
||||
|
||||
assert.Equal(t, 1, len(deleteExoscale))
|
||||
assert.Equal(t, "foo.com", deleteExoscale[0].name)
|
||||
assert.Equal(t, int64(1), deleteExoscale[0].recordID)
|
||||
|
||||
assert.Equal(t, 1, len(updateExoscale))
|
||||
assert.Equal(t, "foo.com", updateExoscale[0].name)
|
||||
assert.Equal(t, int64(1), updateExoscale[0].updateDNSRecord.ID)
|
||||
}
|
@ -143,10 +143,10 @@ func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter Z
|
||||
}
|
||||
|
||||
provider := &GoogleProvider{
|
||||
project: project,
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
project: project,
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
|
||||
managedZonesClient: managedZonesService{dnsClient.ManagedZones},
|
||||
changesClient: changesService{dnsClient.Changes},
|
||||
|
@ -569,10 +569,10 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected
|
||||
|
||||
func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider {
|
||||
provider := &GoogleProvider{
|
||||
project: "zalando-external-dns-test",
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: false,
|
||||
project: "zalando-external-dns-test",
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: false,
|
||||
resourceRecordSetsClient: &mockResourceRecordSetsClient{},
|
||||
managedZonesClient: &mockManagedZonesClient{},
|
||||
changesClient: &mockChangesClient{},
|
||||
|
@ -114,9 +114,9 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
|
||||
}
|
||||
|
||||
// Include Host records since they should be treated synonymously with A records
|
||||
var resH []ibclient.RecordHost
|
||||
objH := ibclient.NewRecordHost(
|
||||
ibclient.RecordHost{
|
||||
var resH []ibclient.HostRecord
|
||||
objH := ibclient.NewHostRecord(
|
||||
ibclient.HostRecord{
|
||||
Zone: zone.Fqdn,
|
||||
},
|
||||
)
|
||||
|
@ -62,18 +62,18 @@ func (client *mockIBConnector) CreateObject(obj ibclient.IBObject) (ref string,
|
||||
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordCNAME).Name)), obj.(*ibclient.RecordCNAME).Name)
|
||||
obj.(*ibclient.RecordCNAME).Ref = ref
|
||||
case "record:host":
|
||||
for _, i := range obj.(*ibclient.RecordHost).Ipv4Addrs {
|
||||
for _, i := range obj.(*ibclient.HostRecord).Ipv4Addrs {
|
||||
client.createdEndpoints = append(
|
||||
client.createdEndpoints,
|
||||
endpoint.NewEndpoint(
|
||||
obj.(*ibclient.RecordHost).Name,
|
||||
obj.(*ibclient.HostRecord).Name,
|
||||
endpoint.RecordTypeA,
|
||||
i.Ipv4Addr,
|
||||
),
|
||||
)
|
||||
}
|
||||
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.RecordHost).Name)), obj.(*ibclient.RecordHost).Name)
|
||||
obj.(*ibclient.RecordHost).Ref = ref
|
||||
ref = fmt.Sprintf("%s/%s:%s/default", obj.ObjectType(), base64.StdEncoding.EncodeToString([]byte(obj.(*ibclient.HostRecord).Name)), obj.(*ibclient.HostRecord).Name)
|
||||
obj.(*ibclient.HostRecord).Ref = ref
|
||||
case "record:txt":
|
||||
client.createdEndpoints = append(
|
||||
client.createdEndpoints,
|
||||
@ -128,21 +128,21 @@ func (client *mockIBConnector) GetObject(obj ibclient.IBObject, ref string, res
|
||||
}
|
||||
*res.(*[]ibclient.RecordCNAME) = result
|
||||
case "record:host":
|
||||
var result []ibclient.RecordHost
|
||||
var result []ibclient.HostRecord
|
||||
for _, object := range *client.mockInfobloxObjects {
|
||||
if object.ObjectType() == "record:host" {
|
||||
if ref != "" &&
|
||||
ref != object.(*ibclient.RecordHost).Ref {
|
||||
ref != object.(*ibclient.HostRecord).Ref {
|
||||
continue
|
||||
}
|
||||
if obj.(*ibclient.RecordHost).Name != "" &&
|
||||
obj.(*ibclient.RecordHost).Name != object.(*ibclient.RecordHost).Name {
|
||||
if obj.(*ibclient.HostRecord).Name != "" &&
|
||||
obj.(*ibclient.HostRecord).Name != object.(*ibclient.HostRecord).Name {
|
||||
continue
|
||||
}
|
||||
result = append(result, *object.(*ibclient.RecordHost))
|
||||
result = append(result, *object.(*ibclient.HostRecord))
|
||||
}
|
||||
}
|
||||
*res.(*[]ibclient.RecordHost) = result
|
||||
*res.(*[]ibclient.HostRecord) = result
|
||||
case "record:txt":
|
||||
var result []ibclient.RecordTXT
|
||||
for _, object := range *client.mockInfobloxObjects {
|
||||
@ -207,9 +207,9 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
|
||||
)
|
||||
}
|
||||
case "record:host":
|
||||
var records []ibclient.RecordHost
|
||||
obj := ibclient.NewRecordHost(
|
||||
ibclient.RecordHost{
|
||||
var records []ibclient.HostRecord
|
||||
obj := ibclient.NewHostRecord(
|
||||
ibclient.HostRecord{
|
||||
Name: result[2],
|
||||
},
|
||||
)
|
||||
@ -267,11 +267,11 @@ func (client *mockIBConnector) UpdateObject(obj ibclient.IBObject, ref string) (
|
||||
),
|
||||
)
|
||||
case "record:host":
|
||||
for _, i := range obj.(*ibclient.RecordHost).Ipv4Addrs {
|
||||
for _, i := range obj.(*ibclient.HostRecord).Ipv4Addrs {
|
||||
client.updatedEndpoints = append(
|
||||
client.updatedEndpoints,
|
||||
endpoint.NewEndpoint(
|
||||
obj.(*ibclient.RecordHost).Name,
|
||||
obj.(*ibclient.HostRecord).Name,
|
||||
i.Ipv4Addr,
|
||||
endpoint.RecordTypeA,
|
||||
),
|
||||
|
301
provider/oci.go
Normal file
301
provider/oci.go
Normal file
@ -0,0 +1,301 @@
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
"github.com/oracle/oci-go-sdk/dns"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
const ociRecordTTL = 300
|
||||
|
||||
// OCIAuthConfig holds connection parameters for the OCI API.
|
||||
type OCIAuthConfig struct {
|
||||
Region string `yaml:"region"`
|
||||
TenancyID string `yaml:"tenancy"`
|
||||
UserID string `yaml:"user"`
|
||||
PrivateKey string `yaml:"key"`
|
||||
Fingerprint string `yaml:"fingerprint"`
|
||||
Passphrase string `yaml:"passphrase"`
|
||||
}
|
||||
|
||||
// OCIConfig holds the configuration for the OCI Provider.
|
||||
type OCIConfig struct {
|
||||
Auth OCIAuthConfig `yaml:"auth"`
|
||||
CompartmentID string `yaml:"compartment"`
|
||||
}
|
||||
|
||||
// OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure
|
||||
// (OCI) DNS.
|
||||
type OCIProvider struct {
|
||||
client ociDNSClient
|
||||
cfg OCIConfig
|
||||
|
||||
domainFilter DomainFilter
|
||||
zoneIDFilter ZoneIDFilter
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
// ociDNSClient is the subset of the OCI DNS API required by the OCI Provider.
|
||||
type ociDNSClient interface {
|
||||
ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error)
|
||||
GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error)
|
||||
PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error)
|
||||
}
|
||||
|
||||
// LoadOCIConfig reads and parses the OCI ExternalDNS config file at the given
|
||||
// path.
|
||||
func LoadOCIConfig(path string) (*OCIConfig, error) {
|
||||
contents, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "reading OCI config file %q", path)
|
||||
}
|
||||
|
||||
cfg := OCIConfig{}
|
||||
if err := yaml.Unmarshal(contents, &cfg); err != nil {
|
||||
return nil, errors.Wrapf(err, "parsing OCI config file %q", path)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// NewOCIProvider initialises a new OCI DNS based Provider.
|
||||
func NewOCIProvider(cfg OCIConfig, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*OCIProvider, error) {
|
||||
var client ociDNSClient
|
||||
client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider(
|
||||
cfg.Auth.TenancyID,
|
||||
cfg.Auth.UserID,
|
||||
cfg.Auth.Region,
|
||||
cfg.Auth.Fingerprint,
|
||||
cfg.Auth.PrivateKey,
|
||||
&cfg.Auth.Passphrase,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "initialising OCI DNS API client")
|
||||
}
|
||||
|
||||
return &OCIProvider{
|
||||
client: client,
|
||||
cfg: cfg,
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OCIProvider) zones(ctx context.Context) (map[string]*dns.ZoneSummary, error) {
|
||||
zones := make(map[string]*dns.ZoneSummary)
|
||||
|
||||
log.Debugf("Matching zones against domain filters: %v", p.domainFilter.filters)
|
||||
var page *string
|
||||
for {
|
||||
resp, err := p.client.ListZones(ctx, dns.ListZonesRequest{
|
||||
CompartmentId: &p.cfg.CompartmentID,
|
||||
ZoneType: dns.ListZonesZoneTypePrimary,
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "listing zones in %q", p.cfg.CompartmentID)
|
||||
}
|
||||
|
||||
for _, zone := range resp.Items {
|
||||
if p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.Id) {
|
||||
zones[*zone.Name] = &zone
|
||||
log.Debugf("Matched %q (%q)", *zone.Name, *zone.Id)
|
||||
} else {
|
||||
log.Debugf("Filtered %q (%q)", *zone.Name, *zone.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if page = resp.OpcNextPage; resp.OpcNextPage == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(zones) == 0 {
|
||||
if p.domainFilter.IsConfigured() {
|
||||
log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter.filters)
|
||||
} else {
|
||||
log.Warnf("No zones found in compartment %q", p.cfg.CompartmentID)
|
||||
}
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *OCIProvider) newFilteredRecordOperations(endpoints []*endpoint.Endpoint, opType dns.RecordOperationOperationEnum) []dns.RecordOperation {
|
||||
ops := []dns.RecordOperation{}
|
||||
for _, endpoint := range endpoints {
|
||||
if p.domainFilter.Match(endpoint.DNSName) {
|
||||
ops = append(ops, newRecordOperation(endpoint, opType))
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
// Records returns the list of records in a given hosted zone.
|
||||
func (p *OCIProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
ctx := context.Background()
|
||||
zones, err := p.zones(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting zones")
|
||||
}
|
||||
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for _, zone := range zones {
|
||||
var page *string
|
||||
for {
|
||||
resp, err := p.client.GetZoneRecords(ctx, dns.GetZoneRecordsRequest{
|
||||
ZoneNameOrId: zone.Id,
|
||||
Page: page,
|
||||
CompartmentId: &p.cfg.CompartmentID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "getting records for zone %q", *zone.Id)
|
||||
}
|
||||
|
||||
for _, record := range resp.Items {
|
||||
if !supportedRecordType(*record.Rtype) {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints,
|
||||
endpoint.NewEndpointWithTTL(
|
||||
*record.Domain,
|
||||
*record.Rtype,
|
||||
endpoint.TTL(*record.Ttl),
|
||||
*record.Rdata,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if page = resp.OpcNextPage; resp.OpcNextPage == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes to a given zone.
|
||||
func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
log.Debugf("Processing chages: %+v", changes)
|
||||
|
||||
ops := []dns.RecordOperation{}
|
||||
ops = append(ops, p.newFilteredRecordOperations(changes.Create, dns.RecordOperationOperationAdd)...)
|
||||
|
||||
ops = append(ops, p.newFilteredRecordOperations(changes.UpdateNew, dns.RecordOperationOperationAdd)...)
|
||||
ops = append(ops, p.newFilteredRecordOperations(changes.UpdateOld, dns.RecordOperationOperationRemove)...)
|
||||
|
||||
ops = append(ops, p.newFilteredRecordOperations(changes.Delete, dns.RecordOperationOperationRemove)...)
|
||||
|
||||
if len(ops) == 0 {
|
||||
log.Info("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
zones, err := p.zones(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching zones")
|
||||
}
|
||||
|
||||
// Separate into per-zone change sets to be passed to OCI API.
|
||||
opsByZone := operationsByZone(zones, ops)
|
||||
for zoneID, ops := range opsByZone {
|
||||
log.Infof("Change zone: %q", zoneID)
|
||||
for _, op := range ops {
|
||||
log.Info(op)
|
||||
}
|
||||
}
|
||||
|
||||
if p.dryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
for zoneID, ops := range opsByZone {
|
||||
if _, err := p.client.PatchZoneRecords(ctx, dns.PatchZoneRecordsRequest{
|
||||
CompartmentId: &p.cfg.CompartmentID,
|
||||
ZoneNameOrId: &zoneID,
|
||||
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{Items: ops},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newRecordOperation returns a RecordOperation based on a given endpoint.
|
||||
func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperationEnum) dns.RecordOperation {
|
||||
targets := make([]string, len(ep.Targets))
|
||||
copy(targets, []string(ep.Targets))
|
||||
if ep.RecordType == endpoint.RecordTypeCNAME {
|
||||
targets[0] = ensureTrailingDot(targets[0])
|
||||
}
|
||||
rdata := strings.Join(targets, " ")
|
||||
|
||||
ttl := ociRecordTTL
|
||||
if ep.RecordTTL.IsConfigured() {
|
||||
ttl = int(ep.RecordTTL)
|
||||
}
|
||||
|
||||
return dns.RecordOperation{
|
||||
Domain: &ep.DNSName,
|
||||
Rdata: &rdata,
|
||||
Ttl: &ttl,
|
||||
Rtype: &ep.RecordType,
|
||||
Operation: opType,
|
||||
}
|
||||
}
|
||||
|
||||
// operationsByZone segments a slice of RecordOperations by their zone.
|
||||
func operationsByZone(zones map[string]*dns.ZoneSummary, ops []dns.RecordOperation) map[string][]dns.RecordOperation {
|
||||
changes := make(map[string][]dns.RecordOperation)
|
||||
|
||||
zoneNameIDMapper := zoneIDName{}
|
||||
for _, z := range zones {
|
||||
zoneNameIDMapper.Add(*z.Id, *z.Name)
|
||||
changes[*z.Id] = []dns.RecordOperation{}
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
if zoneID, _ := zoneNameIDMapper.FindZone(*op.Domain); zoneID != "" {
|
||||
changes[zoneID] = append(changes[zoneID], op)
|
||||
} else {
|
||||
log.Warnf("No matching zone for record operation %s", op)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove zones that don't have have any changes.
|
||||
for zone, ops := range changes {
|
||||
if len(ops) == 0 {
|
||||
delete(changes, zone)
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
839
provider/oci_test.go
Normal file
839
provider/oci_test.go
Normal file
@ -0,0 +1,839 @@
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
"github.com/oracle/oci-go-sdk/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockOCIDNSClient struct{}
|
||||
|
||||
func (c *mockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
|
||||
if request.Page == nil || *request.Page == "0" {
|
||||
return dns.ListZonesResponse{
|
||||
Items: []dns.ZoneSummary{
|
||||
{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
},
|
||||
OpcNextPage: common.String("1"),
|
||||
}, nil
|
||||
}
|
||||
return dns.ListZonesResponse{
|
||||
Items: []dns.ZoneSummary{
|
||||
{
|
||||
Id: common.String("ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"),
|
||||
Name: common.String("bar.com"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
|
||||
if request.ZoneNameOrId == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch *request.ZoneNameOrId {
|
||||
case "ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959":
|
||||
if request.Page == nil || *request.Page == "0" {
|
||||
response.Items = []dns.Record{{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}, {
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
Rtype: common.String(endpoint.RecordTypeTXT),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}}
|
||||
response.OpcNextPage = common.String("1")
|
||||
} else {
|
||||
response.Items = []dns.Record{{Domain: common.String("bar.foo.com"),
|
||||
Rdata: common.String("bar.com."),
|
||||
Rtype: common.String(endpoint.RecordTypeCNAME),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}}
|
||||
}
|
||||
case "ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404":
|
||||
if request.Page == nil || *request.Page == "0" {
|
||||
response.Items = []dns.Record{{
|
||||
Domain: common.String("foo.bar.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
|
||||
return // Provider does not use the response so nothing to do here.
|
||||
}
|
||||
|
||||
// newOCIProvider creates an OCI provider with API calls mocked out.
|
||||
func newOCIProvider(client ociDNSClient, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) *OCIProvider {
|
||||
return &OCIProvider{
|
||||
client: client,
|
||||
cfg: OCIConfig{
|
||||
CompartmentID: "ocid1.compartment.oc1..aaaaaaaaujjg4lf3v6uaqeml7xfk7stzvrxeweaeyolhh75exuoqxpqjb4qq",
|
||||
},
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
}
|
||||
}
|
||||
|
||||
func validateOCIZones(t *testing.T, actual, expected map[string]*dns.ZoneSummary) {
|
||||
require.Len(t, actual, len(expected))
|
||||
|
||||
for k, a := range actual {
|
||||
e, ok := expected[k]
|
||||
require.True(t, ok, "unexpected zone %q (%q)", *a.Name, *a.Id)
|
||||
require.Equal(t, e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOCIProvider(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
config OCIConfig
|
||||
err error
|
||||
}{
|
||||
"valid": {
|
||||
config: OCIConfig{
|
||||
Auth: OCIAuthConfig{
|
||||
TenancyID: "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
|
||||
UserID: "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
|
||||
Region: "us-ashburn-1",
|
||||
Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
|
||||
PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAv2JspZyO14kqcO/X4iz3ZdcyAf1GQJqYsBb6wyrlU0PB9Fee
|
||||
H23/HLtMSqeqo+2KQHmdV1OHFQ/S6tx7zcBaby/+2b+z3/gJO4PGxohe2812AJ/J
|
||||
W8Fp/4EnwbaRqDhoLN7ms0/e566zE3z40kCSW0NAIzv/F+0nNaka1xrypBqzvaNm
|
||||
N49dAGvqWRpzFFUb8CbvKmgE6c/H4a2zVNW3G7/K6Og4HQGeEP3NKSVvi0BiQlvd
|
||||
tVJTg7084kKcrngsS2N3qI3pzsr5wgpzPPefuPHWRKokZ20kpu8tXdFt+mAC2NHh
|
||||
eWbtY3jsR6JFaXCyZLMXInwDvRgdP0T5+uh8WwIDAQABAoIBAG0rr94omDLKw7L4
|
||||
naUfEWC+iIAqAdEIXuDTuudpqLb+h7zh3gj/re6tyK8tRWGNNrfgp6gQtZWGGUJv
|
||||
0w9jEjMqpa2AdRLlYh7Y5KKLV9D6Or3QaAQ3KEffXNZbVmsnAgXWgLL4dKakOPJ8
|
||||
71LAEryMeCGhL7puRVeOxwi9Dnwc4pcloimdggw/uwVHMK9eY5ylyt5ziiiWfhAo
|
||||
cnNJNPHRSTqSiCoEhk/8BLZT5gxf1YX0hVSEdQh2WNyxmPmVSC9uuzKOqcEBfHf5
|
||||
hmLnsUET1REM9IxCLqC9ebW263lIO/KdGiCu+YgIdwIi3wrLhaKXAZQmp4oMvWlE
|
||||
n5eYlcECgYEA5AhctPWCQBCJhcD39pSWgnSq1O9bt8yQi2P2stqlxKV9ZBepCK49
|
||||
OT42OYPUgWn7/y//6/LLzsPY58VTDHF3xZN1qu+fU0IM22D3Jqc19pnfVEb6TXSc
|
||||
0jJIiaYCWTdqRQ4p2DuDcI+EzRB+V1Z7tFWxshZWXwNvtMXNoYPOYaUCgYEA1ttn
|
||||
R3pCuGYJ5XbBwPzD5J+hvdZ6TQf8oTDraUBPxjtFOr7ea42T6KeYRFvnK2AQDnKL
|
||||
Mw3I55lNO4I2W9gahUFG28dhxEuxeyvXGqXEJvPCUYePstab/BkUrm7/jkS3CLcJ
|
||||
dlRXjqOfGwi5+NPUZMoOkZ54ZR4ZpdhIAeEpBf8CgYEAyMyMRlVCowNs9jkcoSfq
|
||||
+Wme3O8BhvI9/mDCZnCfNHC94Bvtn1U/WF7uBOuPf35Ch05PQAiHa8WOBVn/bZ+l
|
||||
ZngZT7K+S+SHyc6zFHh9zm9k96Og2f/r8DSTJ5Ll0oY3sCNuuZh+f+oBeUoi1umy
|
||||
+PPVDAsbd4NhJIBiOO4GGHkCgYA1p4i9Es0Cm4ixItzzwqtwtmR/scXM4se1wS+o
|
||||
kwTY7gg1yWBl328mVGPz/jdWX6Di2rvkPfcDzwa4a6YDfY3x5QE69Sl3CagCqEoJ
|
||||
P4giahEGpyG9eVZuuBywCswKzSIgLQVR5XIQDtA2whEfEFcj7EmDF93c8o1ZGw+w
|
||||
WHgUJQKBgEXr0HgxGG+v8bsXdrJ87Avx/nuA2rrFfECDPa4zuPkEK+cSFibdAq/H
|
||||
u6OIV+z59AD2s84gxR+KLzEDfQAqBt7cVA5ZH6hrO+bkCtK9ycLL+koOuB+1EV+Y
|
||||
hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid": {
|
||||
config: OCIConfig{
|
||||
Auth: OCIAuthConfig{
|
||||
TenancyID: "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
|
||||
UserID: "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
|
||||
Region: "us-ashburn-1",
|
||||
Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
|
||||
PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
|
||||
`,
|
||||
},
|
||||
},
|
||||
err: errors.New("initialising OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"),
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewOCIProvider(
|
||||
tc.config,
|
||||
NewDomainFilter([]string{"com"}),
|
||||
NewZoneIDFilter([]string{""}),
|
||||
false,
|
||||
)
|
||||
if err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Equal(t, tc.err.Error(), err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIZones(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
domainFilter DomainFilter
|
||||
zoneIDFilter ZoneIDFilter
|
||||
expected map[string]*dns.ZoneSummary
|
||||
}{
|
||||
{
|
||||
name: "DomainFilter_com",
|
||||
domainFilter: NewDomainFilter([]string{"com"}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{""}),
|
||||
expected: map[string]*dns.ZoneSummary{
|
||||
"foo.com": {
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
"bar.com": {
|
||||
Id: common.String("ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"),
|
||||
Name: common.String("bar.com"),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "DomainFilter_foo.com",
|
||||
domainFilter: NewDomainFilter([]string{"foo.com"}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{""}),
|
||||
expected: map[string]*dns.ZoneSummary{
|
||||
"foo.com": {
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959",
|
||||
domainFilter: NewDomainFilter([]string{""}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}),
|
||||
expected: map[string]*dns.ZoneSummary{
|
||||
"foo.com": {
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, false)
|
||||
zones, err := provider.zones(context.Background())
|
||||
require.NoError(t, err)
|
||||
validateOCIZones(t, zones, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIRecords(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
domainFilter DomainFilter
|
||||
zoneIDFilter ZoneIDFilter
|
||||
expected []*endpoint.Endpoint
|
||||
}{
|
||||
{
|
||||
name: "unfiltered",
|
||||
domainFilter: NewDomainFilter([]string{""}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{""}),
|
||||
expected: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
|
||||
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
|
||||
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
|
||||
},
|
||||
}, {
|
||||
name: "DomainFilter_foo.com",
|
||||
domainFilter: NewDomainFilter([]string{"foo.com"}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{""}),
|
||||
expected: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
|
||||
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
|
||||
},
|
||||
}, {
|
||||
name: "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404",
|
||||
domainFilter: NewDomainFilter([]string{""}),
|
||||
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
|
||||
expected: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, false)
|
||||
endpoints, err := provider.Records()
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tc.expected, endpoints)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRecordOperation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ep *endpoint.Endpoint
|
||||
opType dns.RecordOperationOperationEnum
|
||||
expected dns.RecordOperation
|
||||
}{
|
||||
{
|
||||
name: "A_record",
|
||||
opType: dns.RecordOperationOperationAdd,
|
||||
ep: endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1"),
|
||||
expected: dns.RecordOperation{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
}, {
|
||||
name: "TXT_record",
|
||||
opType: dns.RecordOperationOperationAdd,
|
||||
ep: endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeTXT,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
expected: dns.RecordOperation{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
Rtype: common.String("TXT"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
}, {
|
||||
name: "CNAME_record",
|
||||
opType: dns.RecordOperationOperationAdd,
|
||||
ep: endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeCNAME,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"bar.com."),
|
||||
expected: dns.RecordOperation{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("bar.com."),
|
||||
Rtype: common.String("CNAME"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := newRecordOperation(tc.ep, tc.opType)
|
||||
require.Equal(t, tc.expected, op)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationsByZone(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
zones map[string]*dns.ZoneSummary
|
||||
ops []dns.RecordOperation
|
||||
expected map[string][]dns.RecordOperation
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
zones: map[string]*dns.ZoneSummary{
|
||||
"foo": {
|
||||
Id: common.String("foo"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
"bar": {
|
||||
Id: common.String("bar"),
|
||||
Name: common.String("bar.com"),
|
||||
},
|
||||
},
|
||||
ops: []dns.RecordOperation{
|
||||
{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
{
|
||||
Domain: common.String("foo.bar.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
expected: map[string][]dns.RecordOperation{
|
||||
"foo": {
|
||||
{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
"bar": {
|
||||
{
|
||||
Domain: common.String("foo.bar.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "does_not_include_zones_with_no_changes",
|
||||
zones: map[string]*dns.ZoneSummary{
|
||||
"foo": {
|
||||
Id: common.String("foo"),
|
||||
Name: common.String("foo.com"),
|
||||
},
|
||||
"bar": {
|
||||
Id: common.String("bar"),
|
||||
Name: common.String("bar.com"),
|
||||
},
|
||||
},
|
||||
ops: []dns.RecordOperation{
|
||||
{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
expected: map[string][]dns.RecordOperation{
|
||||
"foo": {
|
||||
{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := operationsByZone(tc.zones, tc.ops)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mutableMockOCIDNSClient struct {
|
||||
zones map[string]dns.ZoneSummary
|
||||
records map[string]map[string]dns.Record
|
||||
}
|
||||
|
||||
func newMutableMockOCIDNSClient(zones []dns.ZoneSummary, recordsByZone map[string][]dns.Record) *mutableMockOCIDNSClient {
|
||||
c := &mutableMockOCIDNSClient{
|
||||
zones: make(map[string]dns.ZoneSummary),
|
||||
records: make(map[string]map[string]dns.Record),
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
c.zones[*zone.Id] = zone
|
||||
c.records[*zone.Id] = make(map[string]dns.Record)
|
||||
}
|
||||
|
||||
for zoneID, records := range recordsByZone {
|
||||
for _, record := range records {
|
||||
c.records[zoneID][ociRecordKey(*record.Rtype, *record.Domain)] = record
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
|
||||
var zones []dns.ZoneSummary
|
||||
for _, v := range c.zones {
|
||||
zones = append(zones, v)
|
||||
}
|
||||
return dns.ListZonesResponse{Items: zones}, nil
|
||||
}
|
||||
|
||||
func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
|
||||
if request.ZoneNameOrId == nil {
|
||||
err = errors.New("no name or id")
|
||||
return
|
||||
}
|
||||
|
||||
records, ok := c.records[*request.ZoneNameOrId]
|
||||
if !ok {
|
||||
err = errors.New("zone not found")
|
||||
return
|
||||
}
|
||||
|
||||
var items []dns.Record
|
||||
for _, v := range records {
|
||||
items = append(items, v)
|
||||
}
|
||||
|
||||
response.Items = items
|
||||
return
|
||||
}
|
||||
|
||||
func ociRecordKey(rType, domain string) string {
|
||||
return rType + "/" + domain
|
||||
}
|
||||
|
||||
func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
|
||||
if request.ZoneNameOrId == nil {
|
||||
err = errors.New("no name or id")
|
||||
return
|
||||
}
|
||||
|
||||
records, ok := c.records[*request.ZoneNameOrId]
|
||||
if !ok {
|
||||
err = errors.New("zone not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that ADD operations occur after REMOVE.
|
||||
sort.Slice(request.Items, func(i, j int) bool {
|
||||
return request.Items[i].Operation > request.Items[j].Operation
|
||||
})
|
||||
|
||||
for _, op := range request.Items {
|
||||
k := ociRecordKey(*op.Rtype, *op.Domain)
|
||||
switch op.Operation {
|
||||
case dns.RecordOperationOperationAdd:
|
||||
records[k] = dns.Record{
|
||||
Domain: op.Domain,
|
||||
Rtype: op.Rtype,
|
||||
Rdata: op.Rdata,
|
||||
Ttl: op.Ttl,
|
||||
}
|
||||
case dns.RecordOperationOperationRemove:
|
||||
delete(records, k)
|
||||
default:
|
||||
err = errors.Errorf("unsupported operation %q", op.Operation)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TestMutableMockOCIDNSClient exists because one must always test one's tests
|
||||
// right...?
|
||||
func TestMutableMockOCIDNSClient(t *testing.T) {
|
||||
zones := []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}}
|
||||
records := map[string][]dns.Record{
|
||||
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}, {
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
Rtype: common.String(endpoint.RecordTypeTXT),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}},
|
||||
}
|
||||
client := newMutableMockOCIDNSClient(zones, records)
|
||||
|
||||
// First ListZones.
|
||||
zonesResponse, err := client.ListZones(context.Background(), dns.ListZonesRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, zonesResponse.Items, 1)
|
||||
require.Equal(t, zonesResponse.Items, zones)
|
||||
|
||||
// GetZoneRecords for that zone.
|
||||
recordsResponse, err := client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
|
||||
ZoneNameOrId: zones[0].Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, recordsResponse.Items, 2)
|
||||
require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
|
||||
|
||||
// Remove the A record.
|
||||
_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
|
||||
ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
|
||||
Items: []dns.RecordOperation{{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationRemove,
|
||||
}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// GetZoneRecords again and check the A record was removed.
|
||||
recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
|
||||
ZoneNameOrId: zones[0].Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, recordsResponse.Items, 1)
|
||||
require.Equal(t, recordsResponse.Items[0], records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"][1])
|
||||
|
||||
// Add the A record back.
|
||||
_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
|
||||
ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
|
||||
Items: []dns.RecordOperation{{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String("A"),
|
||||
Ttl: common.Int(300),
|
||||
Operation: dns.RecordOperationOperationAdd,
|
||||
}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// GetZoneRecords and check we're back in the origional state
|
||||
recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
|
||||
ZoneNameOrId: zones[0].Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, recordsResponse.Items, 2)
|
||||
require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
|
||||
}
|
||||
|
||||
func TestOCIApplyChanges(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
zones []dns.ZoneSummary
|
||||
records map[string][]dns.Record
|
||||
changes *plan.Changes
|
||||
dryRun bool
|
||||
err error
|
||||
expectedEndpoints []*endpoint.Endpoint
|
||||
}{
|
||||
{
|
||||
name: "add",
|
||||
zones: []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}},
|
||||
changes: &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
},
|
||||
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
}, {
|
||||
name: "remove",
|
||||
zones: []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}},
|
||||
records: map[string][]dns.Record{
|
||||
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}, {
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
|
||||
Rtype: common.String(endpoint.RecordTypeTXT),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}},
|
||||
},
|
||||
changes: &plan.Changes{
|
||||
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeTXT,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
},
|
||||
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
}, {
|
||||
name: "update",
|
||||
zones: []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}},
|
||||
records: map[string][]dns.Record{
|
||||
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}},
|
||||
},
|
||||
changes: &plan.Changes{
|
||||
UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"10.0.0.1",
|
||||
)},
|
||||
},
|
||||
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"10.0.0.1",
|
||||
)},
|
||||
}, {
|
||||
name: "dry_run_no_changes",
|
||||
zones: []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}},
|
||||
records: map[string][]dns.Record{
|
||||
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}},
|
||||
},
|
||||
changes: &plan.Changes{
|
||||
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
},
|
||||
dryRun: true,
|
||||
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
}, {
|
||||
name: "add_remove_update",
|
||||
zones: []dns.ZoneSummary{{
|
||||
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
|
||||
Name: common.String("foo.com"),
|
||||
}},
|
||||
records: map[string][]dns.Record{
|
||||
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
|
||||
Domain: common.String("foo.foo.com"),
|
||||
Rdata: common.String("127.0.0.1"),
|
||||
Rtype: common.String(endpoint.RecordTypeA),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}, {
|
||||
Domain: common.String("bar.foo.com"),
|
||||
Rdata: common.String("bar.com."),
|
||||
Rtype: common.String(endpoint.RecordTypeCNAME),
|
||||
Ttl: common.Int(ociRecordTTL),
|
||||
}},
|
||||
},
|
||||
changes: &plan.Changes{
|
||||
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"foo.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"baz.com.",
|
||||
)},
|
||||
UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"bar.foo.com",
|
||||
endpoint.RecordTypeCNAME,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"baz.com.",
|
||||
)},
|
||||
UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"bar.foo.com",
|
||||
endpoint.RecordTypeCNAME,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"foo.bar.com.",
|
||||
)},
|
||||
Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
|
||||
"baz.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1",
|
||||
)},
|
||||
},
|
||||
expectedEndpoints: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpointWithTTL(
|
||||
"bar.foo.com",
|
||||
endpoint.RecordTypeCNAME,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"foo.bar.com.",
|
||||
),
|
||||
endpoint.NewEndpointWithTTL(
|
||||
"baz.foo.com",
|
||||
endpoint.RecordTypeA,
|
||||
endpoint.TTL(ociRecordTTL),
|
||||
"127.0.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := newMutableMockOCIDNSClient(tc.zones, tc.records)
|
||||
provider := newOCIProvider(
|
||||
client,
|
||||
NewDomainFilter([]string{""}),
|
||||
NewZoneIDFilter([]string{""}),
|
||||
tc.dryRun,
|
||||
)
|
||||
err := provider.ApplyChanges(tc.changes)
|
||||
require.Equal(t, tc.err, err)
|
||||
endpoints, err := provider.Records()
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tc.expectedEndpoints, endpoints)
|
||||
})
|
||||
}
|
||||
}
|
@ -29,9 +29,12 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"crypto/tls"
|
||||
pgo "github.com/ffledgling/pdns-go"
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/pkg/tlsutils"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
"net"
|
||||
)
|
||||
|
||||
type pdnsChangeType string
|
||||
@ -57,6 +60,60 @@ const (
|
||||
retryAfterTime = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
// PDNSConfig is comprised of the fields necessary to create a new PDNSProvider
|
||||
type PDNSConfig struct {
|
||||
DomainFilter DomainFilter
|
||||
DryRun bool
|
||||
Server string
|
||||
APIKey string
|
||||
TLSConfig TLSConfig
|
||||
}
|
||||
|
||||
// TLSConfig is comprised of the TLS-related fields necessary to create a new PDNSProvider
|
||||
type TLSConfig struct {
|
||||
TLSEnabled bool
|
||||
CAFilePath string
|
||||
ClientCertFilePath string
|
||||
ClientCertKeyFilePath string
|
||||
}
|
||||
|
||||
func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) error {
|
||||
if !tlsConfig.TLSEnabled {
|
||||
log.Debug("Skipping TLS for PDNS Provider.")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("Configuring TLS for PDNS Provider.")
|
||||
if tlsConfig.CAFilePath == "" {
|
||||
return errors.New("certificate authority file path must be specified if TLS is enabled")
|
||||
}
|
||||
|
||||
tlsClientConfig, err := tlsutils.NewTLSConfig(tlsConfig.ClientCertFilePath, tlsConfig.ClientCertKeyFilePath, tlsConfig.CAFilePath, "", false, tls.VersionTLS12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Timeouts taken from net.http.DefaultTransport
|
||||
transporter := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
}
|
||||
pdnsClientConfig.HTTPClient = &http.Client{
|
||||
Transport: transporter,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Function for debug printing
|
||||
func stringifyHTTPResponseBody(r *http.Response) (body string) {
|
||||
|
||||
@ -151,37 +208,40 @@ type PDNSProvider struct {
|
||||
}
|
||||
|
||||
// NewPDNSProvider initializes a new PowerDNS based Provider.
|
||||
func NewPDNSProvider(server string, apikey string, domainFilter DomainFilter, dryRun bool) (*PDNSProvider, error) {
|
||||
func NewPDNSProvider(config PDNSConfig) (*PDNSProvider, error) {
|
||||
|
||||
// Do some input validation
|
||||
|
||||
if apikey == "" {
|
||||
if config.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] != "" {
|
||||
if len(config.DomainFilter.filters) != 1 && config.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 {
|
||||
if config.DryRun {
|
||||
return nil, errors.New("PDNS Provider does not currently support dry-run")
|
||||
}
|
||||
|
||||
if server == "localhost" {
|
||||
if config.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
|
||||
pdnsClientConfig := pgo.NewConfiguration()
|
||||
pdnsClientConfig.Host = config.Server
|
||||
pdnsClientConfig.BasePath = config.Server + apiBase
|
||||
if err := config.TLSConfig.setHTTPClient(pdnsClientConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := &PDNSProvider{
|
||||
client: &PDNSAPIClient{
|
||||
dryRun: dryRun,
|
||||
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: apikey}),
|
||||
client: pgo.NewAPIClient(cfg),
|
||||
dryRun: config.DryRun,
|
||||
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}),
|
||||
client: pgo.NewAPIClient(pdnsClientConfig),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -476,22 +476,128 @@ type NewPDNSProviderTestSuite struct {
|
||||
}
|
||||
|
||||
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)
|
||||
_, err := NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
})
|
||||
assert.Error(suite.T(), err, "--pdns-api-key should be specified")
|
||||
|
||||
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{"example.com", "example.org"}), false)
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{"example.com", "example.org"}),
|
||||
})
|
||||
assert.Error(suite.T(), err, "--domainfilter should raise an error")
|
||||
|
||||
_, err = NewPDNSProvider("http://localhost:8081", "foo", NewDomainFilter([]string{""}), true)
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
DryRun: 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)
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Regular case should raise no error")
|
||||
}
|
||||
|
||||
func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreateTLS() {
|
||||
|
||||
_, err := NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Omitted TLS Config case should raise no error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: false,
|
||||
},
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Disabled TLS Config should raise no error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: false,
|
||||
CAFilePath: "/path/to/ca.crt",
|
||||
ClientCertFilePath: "/path/to/cert.pem",
|
||||
ClientCertKeyFilePath: "/path/to/cert-key.pem",
|
||||
},
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Disabled TLS Config with additional flags should raise no error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: true,
|
||||
},
|
||||
})
|
||||
assert.Error(suite.T(), err, "Enabled TLS Config without --tls-ca should raise an error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: true,
|
||||
CAFilePath: "../internal/testresources/ca.pem",
|
||||
},
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Enabled TLS Config with --tls-ca should raise no error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: true,
|
||||
CAFilePath: "../internal/testresources/ca.pem",
|
||||
ClientCertFilePath: "../internal/testresources/client-cert.pem",
|
||||
},
|
||||
})
|
||||
assert.Error(suite.T(), err, "Enabled TLS Config with --tls-client-cert only should raise an error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: true,
|
||||
CAFilePath: "../internal/testresources/ca.pem",
|
||||
ClientCertKeyFilePath: "../internal/testresources/client-cert-key.pem",
|
||||
},
|
||||
})
|
||||
assert.Error(suite.T(), err, "Enabled TLS Config with --tls-client-cert-key only should raise an error")
|
||||
|
||||
_, err = NewPDNSProvider(PDNSConfig{
|
||||
Server: "http://localhost:8081",
|
||||
APIKey: "foo",
|
||||
DomainFilter: NewDomainFilter([]string{""}),
|
||||
TLSConfig: TLSConfig{
|
||||
TLSEnabled: true,
|
||||
CAFilePath: "../internal/testresources/ca.pem",
|
||||
ClientCertFilePath: "../internal/testresources/client-cert.pem",
|
||||
ClientCertKeyFilePath: "../internal/testresources/client-cert-key.pem",
|
||||
},
|
||||
})
|
||||
assert.Nil(suite.T(), err, "Enabled TLS Config with all flags should raise no error")
|
||||
}
|
||||
|
||||
func (suite *NewPDNSProviderTestSuite) TestPDNSRRSetToEndpoints() {
|
||||
// Function definition: convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error)
|
||||
|
||||
|
@ -17,10 +17,10 @@ limitations under the License.
|
||||
package provider
|
||||
|
||||
// supportedRecordType returns true only for supported record types.
|
||||
// Currently only A, CNAME and TXT record types are supported.
|
||||
// Currently A, CNAME, SRV, and TXT record types are supported.
|
||||
func supportedRecordType(recordType string) bool {
|
||||
switch recordType {
|
||||
case "A", "CNAME", "TXT":
|
||||
case "A", "CNAME", "SRV", "TXT":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -18,12 +18,14 @@ package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
"github.com/kubernetes-incubator/external-dns/provider"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
|
||||
@ -31,10 +33,15 @@ type TXTRegistry struct {
|
||||
provider provider.Provider
|
||||
ownerID string //refers to the owner id of the current instance
|
||||
mapper nameMapper
|
||||
|
||||
// cache the records in memory and update on an interval instead.
|
||||
recordsCache []*endpoint.Endpoint
|
||||
recordsCacheRefreshTime time.Time
|
||||
cacheInterval time.Duration
|
||||
}
|
||||
|
||||
// NewTXTRegistry returns new TXTRegistry object
|
||||
func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string) (*TXTRegistry, error) {
|
||||
func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
|
||||
if ownerID == "" {
|
||||
return nil, errors.New("owner id cannot be empty")
|
||||
}
|
||||
@ -42,9 +49,10 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string) (*TXT
|
||||
mapper := newPrefixNameMapper(txtPrefix)
|
||||
|
||||
return &TXTRegistry{
|
||||
provider: provider,
|
||||
ownerID: ownerID,
|
||||
mapper: mapper,
|
||||
provider: provider,
|
||||
ownerID: ownerID,
|
||||
mapper: mapper,
|
||||
cacheInterval: cacheInterval,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -52,6 +60,13 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, ownerID string) (*TXT
|
||||
// If TXT records was created previously to indicate ownership its corresponding value
|
||||
// will be added to the endpoints Labels map
|
||||
func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
|
||||
// If we have the zones cached AND we have refreshed the cache since the
|
||||
// last given interval, then just use the cached results.
|
||||
if im.recordsCache != nil && time.Since(im.recordsCacheRefreshTime) < im.cacheInterval {
|
||||
log.Debug("Using cached records.")
|
||||
return im.recordsCache, nil
|
||||
}
|
||||
|
||||
records, err := im.provider.Records()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -91,6 +106,12 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the cache.
|
||||
if im.cacheInterval > 0 {
|
||||
im.recordsCache = endpoints
|
||||
im.recordsCacheRefreshTime = time.Now()
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
@ -107,6 +128,10 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
|
||||
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
|
||||
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
|
||||
filteredChanges.Create = append(filteredChanges.Create, txt)
|
||||
|
||||
if im.cacheInterval > 0 {
|
||||
im.addToCache(r)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range filteredChanges.Delete {
|
||||
@ -115,19 +140,32 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
|
||||
// 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
|
||||
filteredChanges.Delete = append(filteredChanges.Delete, txt)
|
||||
|
||||
if im.cacheInterval > 0 {
|
||||
im.removeFromCache(r)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure TXT records are consistently updated as well
|
||||
for _, r := range filteredChanges.UpdateNew {
|
||||
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), 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)
|
||||
// remove old version of record from cache
|
||||
if im.cacheInterval > 0 {
|
||||
im.removeFromCache(r)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure TXT records are consistently updated as well
|
||||
for _, r := range filteredChanges.UpdateNew {
|
||||
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true))
|
||||
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
|
||||
// add new version of record to cache
|
||||
if im.cacheInterval > 0 {
|
||||
im.addToCache(r)
|
||||
}
|
||||
}
|
||||
|
||||
return im.provider.ApplyChanges(filteredChanges)
|
||||
@ -167,3 +205,24 @@ func (pr prefixNameMapper) toEndpointName(txtDNSName string) string {
|
||||
func (pr prefixNameMapper) toTXTName(endpointDNSName string) string {
|
||||
return pr.prefix + endpointDNSName
|
||||
}
|
||||
|
||||
func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {
|
||||
if im.recordsCache != nil {
|
||||
im.recordsCache = append(im.recordsCache, ep)
|
||||
}
|
||||
}
|
||||
|
||||
func (im *TXTRegistry) removeFromCache(ep *endpoint.Endpoint) {
|
||||
if im.recordsCache == nil || ep == nil {
|
||||
// return early.
|
||||
return
|
||||
}
|
||||
|
||||
for i, e := range im.recordsCache {
|
||||
if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.Targets.Same(ep.Targets) {
|
||||
// We found a match delete the endpoint from the cache.
|
||||
im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,9 @@ limitations under the License.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/internal/testutils"
|
||||
@ -40,10 +42,10 @@ func TestTXTRegistry(t *testing.T) {
|
||||
|
||||
func testTXTRegistryNew(t *testing.T) {
|
||||
p := provider.NewInMemoryProvider()
|
||||
_, err := NewTXTRegistry(p, "txt", "")
|
||||
_, err := NewTXTRegistry(p, "txt", "", time.Hour)
|
||||
require.Error(t, err)
|
||||
|
||||
r, err := NewTXTRegistry(p, "txt", "owner")
|
||||
r, err := NewTXTRegistry(p, "txt", "owner", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := r.mapper.(prefixNameMapper)
|
||||
@ -51,7 +53,7 @@ func testTXTRegistryNew(t *testing.T) {
|
||||
assert.Equal(t, "owner", r.ownerID)
|
||||
assert.Equal(t, p, r.provider)
|
||||
|
||||
r, err = NewTXTRegistry(p, "", "owner")
|
||||
r, err = NewTXTRegistry(p, "", "owner", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok = r.mapper.(prefixNameMapper)
|
||||
@ -130,7 +132,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
r, _ := NewTXTRegistry(p, "txt.", "owner")
|
||||
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
|
||||
records, _ := r.Records()
|
||||
|
||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
||||
@ -204,7 +206,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
r, _ := NewTXTRegistry(p, "", "owner")
|
||||
r, _ := NewTXTRegistry(p, "", "owner", time.Hour)
|
||||
records, _ := r.Records()
|
||||
|
||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
||||
@ -231,7 +233,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
|
||||
newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||
},
|
||||
})
|
||||
r, _ := NewTXTRegistry(p, "txt.", "owner")
|
||||
r, _ := NewTXTRegistry(p, "txt.", "owner", time.Hour)
|
||||
|
||||
changes := &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
@ -300,7 +302,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
|
||||
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||
},
|
||||
})
|
||||
r, _ := NewTXTRegistry(p, "", "owner")
|
||||
r, _ := NewTXTRegistry(p, "", "owner", time.Hour)
|
||||
|
||||
changes := &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
@ -347,6 +349,67 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCacheMethods(t *testing.T) {
|
||||
cache := []*endpoint.Endpoint{
|
||||
newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"),
|
||||
newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"),
|
||||
newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"),
|
||||
}
|
||||
registry := &TXTRegistry{
|
||||
recordsCache: cache,
|
||||
cacheInterval: time.Hour,
|
||||
}
|
||||
|
||||
expectedCacheAfterAdd := []*endpoint.Endpoint{
|
||||
newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"),
|
||||
newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"),
|
||||
newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"),
|
||||
}
|
||||
|
||||
expectedCacheAfterUpdate := []*endpoint.Endpoint{
|
||||
newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"),
|
||||
newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"),
|
||||
newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"),
|
||||
newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2"),
|
||||
}
|
||||
|
||||
expectedCacheAfterDelete := []*endpoint.Endpoint{
|
||||
newEndpointWithOwner("thing1.com", "1.2.3.6", "A", "owner"),
|
||||
newEndpointWithOwner("thing2.com", "1.2.3.4", "CNAME", "owner"),
|
||||
newEndpointWithOwner("thing3.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing4.com", "1.2.3.4", "A", "owner"),
|
||||
newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"),
|
||||
}
|
||||
// test add cache
|
||||
registry.addToCache(newEndpointWithOwner("thing5.com", "1.2.3.5", "A", "owner"))
|
||||
|
||||
if !reflect.DeepEqual(expectedCacheAfterAdd, registry.recordsCache) {
|
||||
t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterAdd, registry.recordsCache)
|
||||
}
|
||||
|
||||
// test update cache
|
||||
registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.4", "A", "owner"))
|
||||
registry.addToCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2"))
|
||||
// ensure it was updated
|
||||
if !reflect.DeepEqual(expectedCacheAfterUpdate, registry.recordsCache) {
|
||||
t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterUpdate, registry.recordsCache)
|
||||
}
|
||||
|
||||
// test deleting a record
|
||||
registry.removeFromCache(newEndpointWithOwner("thing.com", "1.2.3.6", "A", "owner2"))
|
||||
// ensure it was deleted
|
||||
if !reflect.DeepEqual(expectedCacheAfterDelete, registry.recordsCache) {
|
||||
t.Fatalf("expected endpoints should match endpoints from cache: expected %v, but got %v", expectedCacheAfterDelete, registry.recordsCache)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
helper methods
|
||||
|
@ -233,6 +233,15 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
|
||||
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl)...)
|
||||
}
|
||||
|
||||
for _, tls := range ing.Spec.TLS {
|
||||
for _, host := range tls.Hosts {
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl)...)
|
||||
}
|
||||
}
|
||||
|
||||
hostnameList := getHostnamesFromAnnotations(ing.Annotations)
|
||||
for _, hostname := range hostnameList {
|
||||
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...)
|
||||
|
@ -615,6 +615,83 @@ func testIngressEndpoints(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ingress rules with single tls having single hostname",
|
||||
targetNamespace: "",
|
||||
ingressItems: []fakeIngress{
|
||||
{
|
||||
name: "fake1",
|
||||
namespace: namespace,
|
||||
tlsdnsnames: [][]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,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ingress rules with single tls having multiple hostnames",
|
||||
targetNamespace: "",
|
||||
ingressItems: []fakeIngress{
|
||||
{
|
||||
name: "fake1",
|
||||
namespace: namespace,
|
||||
tlsdnsnames: [][]string{{"example.org", "example2.org"}},
|
||||
ips: []string{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
expected: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "example.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
{
|
||||
DNSName: "example2.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ingress rules with multiple tls having multiple hostnames",
|
||||
targetNamespace: "",
|
||||
ingressItems: []fakeIngress{
|
||||
{
|
||||
name: "fake1",
|
||||
namespace: namespace,
|
||||
tlsdnsnames: [][]string{{"example.org", "example2.org"}, {"example3.org", "example4.org"}},
|
||||
ips: []string{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
expected: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "example.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
{
|
||||
DNSName: "example2.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
{
|
||||
DNSName: "example3.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
{
|
||||
DNSName: "example4.org",
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ingress rules with hostname annotation",
|
||||
targetNamespace: "",
|
||||
@ -828,6 +905,7 @@ func testIngressEndpoints(t *testing.T) {
|
||||
// ingress specific helper functions
|
||||
type fakeIngress struct {
|
||||
dnsnames []string
|
||||
tlsdnsnames [][]string
|
||||
ips []string
|
||||
hostnames []string
|
||||
namespace string
|
||||
@ -856,6 +934,11 @@ func (ing fakeIngress) Ingress() *v1beta1.Ingress {
|
||||
Host: dnsname,
|
||||
})
|
||||
}
|
||||
for _, hosts := range ing.tlsdnsnames {
|
||||
ingress.Spec.TLS = append(ingress.Spec.TLS, v1beta1.IngressTLS{
|
||||
Hosts: hosts,
|
||||
})
|
||||
}
|
||||
for _, ip := range ing.ips {
|
||||
ingress.Status.LoadBalancer.Ingress = append(ingress.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{
|
||||
IP: ip,
|
||||
|
@ -90,6 +90,12 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the ip addresses of all the nodes and cache them for this run
|
||||
nodeTargets, err := sc.extractNodeTargets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
|
||||
for _, svc := range services.Items {
|
||||
@ -101,7 +107,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
svcEndpoints := sc.endpoints(&svc)
|
||||
svcEndpoints := sc.endpoints(&svc, nodeTargets)
|
||||
|
||||
// process legacy annotations if no endpoints were returned and compatibility mode is enabled.
|
||||
if len(svcEndpoints) == 0 && sc.compatibility != "" {
|
||||
@ -110,7 +116,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
|
||||
// apply template if none of the above is found
|
||||
if (sc.combineFQDNAnnotation || len(svcEndpoints) == 0) && sc.fqdnTemplate != nil {
|
||||
sEndpoints, err := sc.endpointsFromTemplate(&svc)
|
||||
sEndpoints, err := sc.endpointsFromTemplate(&svc, nodeTargets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -169,7 +175,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
|
||||
|
||||
return endpoints
|
||||
}
|
||||
func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.Endpoint, error) {
|
||||
|
||||
func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service, nodeTargets endpoint.Targets) ([]*endpoint.Endpoint, error) {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
// Process the whole template string
|
||||
@ -181,19 +188,19 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
|
||||
|
||||
hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
|
||||
for _, hostname := range hostnameList {
|
||||
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
|
||||
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets)...)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// endpointsFromService extracts the endpoints from a service object
|
||||
func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
|
||||
func (sc *serviceSource) endpoints(svc *v1.Service, nodeTargets endpoint.Targets) []*endpoint.Endpoint {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
hostnameList := getHostnamesFromAnnotations(svc.Annotations)
|
||||
for _, hostname := range hostnameList {
|
||||
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
|
||||
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets)...)
|
||||
}
|
||||
|
||||
return endpoints
|
||||
@ -236,7 +243,7 @@ func (sc *serviceSource) setResourceLabel(service v1.Service, endpoints []*endpo
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
|
||||
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nodeTargets endpoint.Targets) []*endpoint.Endpoint {
|
||||
hostname = strings.TrimSuffix(hostname, ".")
|
||||
ttl, err := getTTLFromAnnotations(svc.Annotations)
|
||||
if err != nil {
|
||||
@ -272,7 +279,10 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*
|
||||
if svc.Spec.ClusterIP == v1.ClusterIPNone {
|
||||
endpoints = append(endpoints, sc.extractHeadlessEndpoints(svc, hostname, ttl)...)
|
||||
}
|
||||
|
||||
case v1.ServiceTypeNodePort:
|
||||
// add the nodeTargets and extract an SRV endpoint
|
||||
targets = append(targets, nodeTargets...)
|
||||
endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, nodeTargets, hostname, ttl)...)
|
||||
}
|
||||
|
||||
for _, t := range targets {
|
||||
@ -316,3 +326,68 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
func (sc *serviceSource) extractNodeTargets() (endpoint.Targets, error) {
|
||||
var (
|
||||
internalIPs endpoint.Targets
|
||||
externalIPs endpoint.Targets
|
||||
)
|
||||
|
||||
nodes, err := sc.client.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range nodes.Items {
|
||||
for _, address := range node.Status.Addresses {
|
||||
switch address.Type {
|
||||
case v1.NodeExternalIP:
|
||||
externalIPs = append(externalIPs, address.Address)
|
||||
case v1.NodeInternalIP:
|
||||
internalIPs = append(internalIPs, address.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(externalIPs) > 0 {
|
||||
return externalIPs, nil
|
||||
}
|
||||
|
||||
return internalIPs, nil
|
||||
}
|
||||
|
||||
func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets endpoint.Targets, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
for _, port := range svc.Spec.Ports {
|
||||
if port.NodePort > 0 {
|
||||
// build a target with a priority of 0, weight of 0, and pointing the given port on the given host
|
||||
target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname)
|
||||
|
||||
// figure out the portname
|
||||
portName := port.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("%d", port.NodePort)
|
||||
}
|
||||
|
||||
// figure out the protocol
|
||||
protocol := strings.ToLower(string(port.Protocol))
|
||||
if protocol == "" {
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
recordName := fmt.Sprintf("_%s._%s.%s", portName, protocol, hostname)
|
||||
|
||||
var ep *endpoint.Endpoint
|
||||
if ttl.IsConfigured() {
|
||||
ep = endpoint.NewEndpointWithTTL(recordName, endpoint.RecordTypeSRV, ttl, target)
|
||||
} else {
|
||||
ep = endpoint.NewEndpoint(recordName, endpoint.RecordTypeSRV, target)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
@ -1022,6 +1022,201 @@ func TestClusterIpServices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// testNodePortServices tests that various services generate the correct endpoints.
|
||||
func TestNodePortServices(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
|
||||
lbs []string
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
nodes []*v1.Node
|
||||
}{
|
||||
{
|
||||
"annotated NodePort services return an endpoint with IP addresses of the cluster's nodes",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
"",
|
||||
"",
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
"non-annotated NodePort services with set fqdnTemplate return an endpoint with target IP",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
"",
|
||||
"{{.Name}}.bar.example.com",
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
"annotated NodePort services return an endpoint with IP addresses of the private cluster's nodes",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
"",
|
||||
"",
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
hostnameAnnotationKey: "foo.example.org.",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
// Create a Kubernetes testing client
|
||||
kubernetes := fake.NewSimpleClientset()
|
||||
|
||||
// Create the nodes
|
||||
for _, node := range tc.nodes {
|
||||
if _, err := kubernetes.Core().Nodes().Create(node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a service to test against
|
||||
service := &v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: tc.svcType,
|
||||
Ports: []v1.ServicePort{
|
||||
{
|
||||
NodePort: 30192,
|
||||
},
|
||||
},
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: tc.svcNamespace,
|
||||
Name: tc.svcName,
|
||||
Labels: tc.labels,
|
||||
Annotations: tc.annotations,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create our object under test and get the endpoints.
|
||||
client, _ := NewServiceSource(
|
||||
kubernetes,
|
||||
tc.targetNamespace,
|
||||
tc.annotationFilter,
|
||||
tc.fqdnTemplate,
|
||||
false,
|
||||
tc.compatibility,
|
||||
true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
endpoints, err := client.Endpoints()
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Validate returned endpoints against desired endpoints.
|
||||
validateEndpoints(t, endpoints, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHeadlessServices tests that headless services generate the correct endpoints.
|
||||
func TestHeadlessServices(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
|
Loading…
Reference in New Issue
Block a user