mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-05-05 06:36:11 +02:00
Add Initial BlueCat Provider Support
The new BlueCat provider uses the BlueCat API Gateway(REST API). Not the legacy XML based BlueCat API. https://github.com/bluecatlabs/gateway-workflows
This commit is contained in:
parent
e2cb36c070
commit
e20aea4d5f
@ -28,6 +28,7 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
|
||||
* [AWS Route 53](https://aws.amazon.com/route53/)
|
||||
* [AWS Cloud Map](https://docs.aws.amazon.com/cloud-map/)
|
||||
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
|
||||
* [BlueCat](https://bluecatnetworks.com)
|
||||
* [CloudFlare](https://www.cloudflare.com/dns)
|
||||
* [RcodeZero](https://www.rcodezero.at/)
|
||||
* [DigitalOcean](https://www.digitalocean.com/products/networking)
|
||||
@ -82,6 +83,7 @@ The following table clarifies the current status of the providers according to t
|
||||
| AWS Cloud Map | Beta | |
|
||||
| Akamai Edge DNS | Beta | |
|
||||
| AzureDNS | Beta | |
|
||||
| BlueCat | Alpha | @seanmalloy @vinny-sabatini |
|
||||
| CloudFlare | Beta | |
|
||||
| RcodeZero | Alpha | |
|
||||
| DigitalOcean | Alpha | |
|
||||
|
||||
64
docs/tutorials/bluecat.md
Normal file
64
docs/tutorials/bluecat.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Setting up external-dns for BlueCat
|
||||
|
||||
## Prerequisites
|
||||
Install the BlueCat Gateway product and deploy the [community gateway workflows](https://github.com/bluecatlabs/gateway-workflows).
|
||||
|
||||
## Deploy
|
||||
Setup configuration file as k8s `Secret`.
|
||||
```
|
||||
cat << EOF > ~/bluecat.json
|
||||
{
|
||||
"gatewayHost": "https://bluecatgw.example.com",
|
||||
"gatewayUsername": "user",
|
||||
"GatewayPassword": "pass",
|
||||
"dnsConfiguration": "Example",
|
||||
"dnsView": "Internal",
|
||||
"rootZone": "example.com"
|
||||
}
|
||||
EOF
|
||||
kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example
|
||||
```
|
||||
|
||||
Setup up deployment/service account:
|
||||
```
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: bluecat-example
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: bluecat-example
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
volumes:
|
||||
- name: bluecatconfig
|
||||
secret:
|
||||
secretName: bluecatconfig
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:$TAG # no released versions include the bluecat provider yet
|
||||
volumeMounts:
|
||||
- name: bluecatconfig
|
||||
mountPath: "/etc/external-dns/"
|
||||
readOnly: true
|
||||
args:
|
||||
- --log-level=debug
|
||||
- --source=service
|
||||
- --provider=bluecat
|
||||
- --txt-owner-id=bluecat-example
|
||||
- --bluecat-config-file=/etc/external-dns/bluecat.json
|
||||
```
|
||||
20
go.sum
20
go.sum
@ -92,6 +92,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357 h1:3ynCSeUh9OtJLd/OzLapM1DLDv2g+0yyDdkLqSfZCaQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
@ -127,8 +128,6 @@ github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bodgit/tsig v0.0.2 h1:seNt23SrPW8dkWoyRYzdeuqFEzr+lDc0dAJvo94xB8U=
|
||||
github.com/bodgit/tsig v0.0.2/go.mod h1:0mYe0t9it36SOvDQyeFekc7bLtvljFz7H9vHS/nYbgc=
|
||||
github.com/bodgit/tsig v1.1.1 h1:SViReRa8KyaweqdJ3ojdYqIE3xDyJlR3G+6wAsSbLCo=
|
||||
github.com/bodgit/tsig v1.1.1/go.mod h1:8LZ3Mn7AVZHH8GN2ArvzB7msHfLjoptWsdPEJRSw/uo=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
|
||||
@ -427,7 +426,9 @@ github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
@ -510,8 +511,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.1/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZ
|
||||
github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/copier v0.1.0 h1:Vh8xALtH3rrKGB/XIRe5d0yCTHPZFauWPLvdpDAbi88=
|
||||
github.com/jinzhu/copier v0.1.0/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
@ -599,8 +598,6 @@ github.com/maxatome/go-testdeep v1.4.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ys
|
||||
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1 h1:kZZmnTeY2r+88mDNCVV/uCXL2gG3rkVPTN9jcYfGQcI=
|
||||
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
@ -690,6 +687,7 @@ github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae/go.mod h1:l6TGeqJ92D
|
||||
github.com/openshift/build-machinery-go v0.0.0-20200424080330-082bf86082cc/go.mod h1:1CkcsT3aVebzRBzVTSbiKSkJMsC/CASqxesfqEMfJEc=
|
||||
github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73 h1:JePLt9EpNLF/30KsSsArrzxGWPaUIvYUt8Fwnw9wlgM=
|
||||
github.com/openshift/client-go v0.0.0-20200608144219-584632b8fc73/go.mod h1:+66gk3dEqw9e+WoiXjJFzWlS1KGhj9ZRHi/RI/YG/ZM=
|
||||
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk=
|
||||
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
@ -948,8 +946,6 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1062,7 +1058,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -1073,9 +1068,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1225,12 +1217,8 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
istio.io/api v0.0.0-20200529165953-72dad51d4ffc h1:cR9GmbIBAz3FnY3tgs1SRn/uiznhtvG+mZBfD1p2vIA=
|
||||
istio.io/api v0.0.0-20200529165953-72dad51d4ffc/go.mod h1:kyq3g5w42zl/AKlbzDGppYpGMQYMYMyZKeq0/eexML8=
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f h1:zUFsawgPj5oI9p5cf91YCExRlxLIVsEkIunN9ODUSJs=
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64=
|
||||
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751 h1:yH62fTmV+5l1XVTWcomsc1jjH/oH9u/tTgn5NVmdIac=
|
||||
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751/go.mod h1:4SGvmmus5HNFdqQsIL+uQO1PbAhjQKtSjMTqwsvYHlg=
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 h1:ZA8Y2gKkKtEeYuKfqlEzIBDfU4IE5uIAdsXDeD41T9w=
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02/go.mod h1:oXMjFUWhxlReUSbg4i3GjKgOhSX1WgD68ZNlHQEcmQg=
|
||||
istio.io/gogo-genproto v0.0.0-20190904133402-ee07f2785480/go.mod h1:uKtbae4K9k2rjjX4ToV0l6etglbc1i7gqQ94XdkshzY=
|
||||
|
||||
3
main.go
3
main.go
@ -39,6 +39,7 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/aws"
|
||||
"sigs.k8s.io/external-dns/provider/awssd"
|
||||
"sigs.k8s.io/external-dns/provider/azure"
|
||||
"sigs.k8s.io/external-dns/provider/bluecat"
|
||||
"sigs.k8s.io/external-dns/provider/cloudflare"
|
||||
"sigs.k8s.io/external-dns/provider/coredns"
|
||||
"sigs.k8s.io/external-dns/provider/designate"
|
||||
@ -194,6 +195,8 @@ func main() {
|
||||
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
|
||||
case "azure-private-dns":
|
||||
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
|
||||
case "bluecat":
|
||||
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "vinyldns":
|
||||
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "vultr":
|
||||
|
||||
@ -82,6 +82,7 @@ type Config struct {
|
||||
AzureResourceGroup string
|
||||
AzureSubscriptionID string
|
||||
AzureUserAssignedIdentityClientID string
|
||||
BluecatConfigFile string
|
||||
CloudflareProxied bool
|
||||
CloudflareZonesPerPage int
|
||||
CoreDNSPrefix string
|
||||
@ -199,6 +200,7 @@ var defaultConfig = &Config{
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
CloudflareProxied: false,
|
||||
CloudflareZonesPerPage: 50,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
@ -352,7 +354,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
|
||||
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("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
|
||||
app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
|
||||
@ -375,6 +377,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
|
||||
app.Flag("azure-subscription-id", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure-private-dns)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
|
||||
app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
|
||||
app.Flag("bluecat-config-file", "When using the Bluecat provider, specify the Bluecat configuration file (required when --provider=bluecat").Default(defaultConfig.BluecatConfigFile).StringVar(&cfg.BluecatConfigFile)
|
||||
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
|
||||
app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)
|
||||
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
|
||||
|
||||
@ -61,6 +61,7 @@ var (
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
CloudflareProxied: false,
|
||||
CloudflareZonesPerPage: 50,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
@ -142,6 +143,7 @@ var (
|
||||
AzureConfigFile: "azure.json",
|
||||
AzureResourceGroup: "arg",
|
||||
AzureSubscriptionID: "arg",
|
||||
BluecatConfigFile: "bluecat.json",
|
||||
CloudflareProxied: true,
|
||||
CloudflareZonesPerPage: 20,
|
||||
CoreDNSPrefix: "/coredns/",
|
||||
@ -236,6 +238,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--azure-config-file=azure.json",
|
||||
"--azure-resource-group=arg",
|
||||
"--azure-subscription-id=arg",
|
||||
"--bluecat-config-file=bluecat.json",
|
||||
"--cloudflare-proxied",
|
||||
"--cloudflare-zones-per-page=20",
|
||||
"--coredns-prefix=/coredns/",
|
||||
@ -331,6 +334,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_AZURE_CONFIG_FILE": "azure.json",
|
||||
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
|
||||
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
|
||||
"EXTERNAL_DNS_BLUECAT_CONFIG_FILE": "bluecat.json",
|
||||
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
|
||||
"EXTERNAL_DNS_CLOUDFLARE_ZONES_PER_PAGE": "20",
|
||||
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
|
||||
|
||||
6
provider/bluecat/OWNERS
Normal file
6
provider/bluecat/OWNERS
Normal file
@ -0,0 +1,6 @@
|
||||
approvers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
||||
reviewers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
||||
969
provider/bluecat/bluecat.go
Normal file
969
provider/bluecat/bluecat.go
Normal file
@ -0,0 +1,969 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
|
||||
// TODO: Ensure we have proper error handling/logging for API calls to Bluecat. getBluecatGatewayToken has a good example of this
|
||||
|
||||
package bluecat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
type bluecatConfig struct {
|
||||
GatewayHost string `json:"gatewayHost"`
|
||||
GatewayUsername string `json:"gatewayUsername"`
|
||||
GatewayPassword string `json:"gatewayPassword"`
|
||||
DNSConfiguration string `json:"dnsConfiguration"`
|
||||
View string `json:"dnsView"`
|
||||
RootZone string `json:"rootZone"`
|
||||
}
|
||||
|
||||
// BluecatProvider implements the DNS provider for Bluecat DNS
|
||||
type BluecatProvider struct {
|
||||
provider.BaseProvider
|
||||
domainFilter endpoint.DomainFilter
|
||||
zoneIDFilter provider.ZoneIDFilter
|
||||
dryRun bool
|
||||
RootZone string
|
||||
DNSConfiguration string
|
||||
View string
|
||||
gatewayClient GatewayClient
|
||||
}
|
||||
|
||||
type GatewayClient interface {
|
||||
getBluecatZones(zoneName string) ([]BluecatZone, error)
|
||||
getHostRecords(zone string, records *[]BluecatHostRecord) error
|
||||
getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error
|
||||
getHostRecord(name string, record *BluecatHostRecord) error
|
||||
getCNAMERecord(name string, record *BluecatCNAMERecord) error
|
||||
createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error)
|
||||
createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error)
|
||||
deleteHostRecord(name string) (err error)
|
||||
deleteCNAMERecord(name string) (err error)
|
||||
buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error)
|
||||
getTXTRecords(zone string, records *[]BluecatTXTRecord) error
|
||||
getTXTRecord(name string, record *BluecatTXTRecord) error
|
||||
createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error)
|
||||
deleteTXTRecord(name string) error
|
||||
}
|
||||
|
||||
// GatewayClientConfig defines new client on bluecat gateway
|
||||
type GatewayClientConfig struct {
|
||||
Cookie http.Cookie
|
||||
Token string
|
||||
Host string
|
||||
DNSConfiguration string
|
||||
View string
|
||||
RootZone string
|
||||
}
|
||||
|
||||
// BluecatZone defines a zone to hold records
|
||||
type BluecatZone struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatHostRecord defines dns Host record
|
||||
type BluecatHostRecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatCNAMERecord defines dns CNAME record
|
||||
type BluecatCNAMERecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties string `json:"properties"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// BluecatTXTRecord defines dns TXT record
|
||||
type BluecatTXTRecord struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type bluecatRecordSet struct {
|
||||
obj interface{}
|
||||
res interface{}
|
||||
}
|
||||
|
||||
type bluecatCreateHostRecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
IP4Address string `json:"ip4_address"`
|
||||
TTL int `json:"ttl"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type bluecatCreateCNAMERecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
LinkedRecord string `json:"linked_record"`
|
||||
TTL int `json:"ttl"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type bluecatCreateTXTRecordRequest struct {
|
||||
AbsoluteName string `json:"absolute_name"`
|
||||
Text string `json:"txt"`
|
||||
}
|
||||
|
||||
// NewBluecatProvider creates a new Bluecat provider.
|
||||
//
|
||||
// Returns a pointer to the provider or an error if a provider could not be created.
|
||||
func NewBluecatProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*BluecatProvider, error) {
|
||||
contents, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
|
||||
}
|
||||
|
||||
cfg := bluecatConfig{}
|
||||
err = json.Unmarshal(contents, &cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read Bluecat config file %v", configFile)
|
||||
}
|
||||
|
||||
token, cookie, err := getBluecatGatewayToken(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway")
|
||||
}
|
||||
gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone)
|
||||
|
||||
provider := &BluecatProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
gatewayClient: gatewayClient,
|
||||
DNSConfiguration: cfg.DNSConfiguration,
|
||||
View: cfg.View,
|
||||
RootZone: cfg.RootZone,
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// NewGatewayClient creates and returns a new Bluecat gateway client
|
||||
func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone string) GatewayClientConfig {
|
||||
// Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user
|
||||
// doesn't provide one via the config file we'll assume it's 'com'
|
||||
if rootZone == "" {
|
||||
rootZone = "com"
|
||||
}
|
||||
return GatewayClientConfig{
|
||||
Cookie: cookie,
|
||||
Token: token,
|
||||
Host: gatewayHost,
|
||||
DNSConfiguration: dnsConfiguration,
|
||||
View: view,
|
||||
RootZone: rootZone,
|
||||
}
|
||||
}
|
||||
|
||||
// Records fetches Host, CNAME, and TXT records from bluecat gateway
|
||||
func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
||||
zones, err := p.zones()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not fetch zones")
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
log.Debugf("fetching records from zone '%s'", zone)
|
||||
var resH []BluecatHostRecord
|
||||
err = p.gatewayClient.getHostRecords(zone, &resH)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch host records for zone: %v", zone)
|
||||
}
|
||||
for _, rec := range resH {
|
||||
propMap := splitProperties(rec.Properties)
|
||||
ips := strings.Split(propMap["addresses"], ",")
|
||||
for _, ip := range ips {
|
||||
ep := endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeA, ip)
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
var resC []BluecatCNAMERecord
|
||||
err = p.gatewayClient.getCNAMERecords(zone, &resC)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch CNAME records for zone: %v", zone)
|
||||
}
|
||||
for _, rec := range resC {
|
||||
propMap := splitProperties(rec.Properties)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeCNAME, propMap["linkedRecordName"]))
|
||||
}
|
||||
|
||||
var resT []BluecatTXTRecord
|
||||
err = p.gatewayClient.getTXTRecords(zone, &resT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not fetch TXT records for zone: %v", zone)
|
||||
}
|
||||
for _, rec := range resT {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(rec.Name, endpoint.RecordTypeTXT, rec.Text))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("fetched %d records from Bluecat", len(endpoints))
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges updates necessary zones and replaces old records with new ones
|
||||
//
|
||||
// Returns nil upon success and err is there is an error
|
||||
func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
zones, err := p.zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("zones is: %+v\n", zones)
|
||||
log.Infof("changes: %+v\n", changes)
|
||||
created, deleted := p.mapChanges(zones, changes)
|
||||
log.Infof("created: %+v\n", created)
|
||||
log.Infof("deleted: %+v\n", deleted)
|
||||
p.deleteRecords(deleted)
|
||||
p.createRecords(created)
|
||||
|
||||
// TODO: add bluecat deploy API call here
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bluecatChangeMap map[string][]*endpoint.Endpoint
|
||||
|
||||
func (p *BluecatProvider) mapChanges(zones []string, changes *plan.Changes) (bluecatChangeMap, bluecatChangeMap) {
|
||||
created := bluecatChangeMap{}
|
||||
deleted := bluecatChangeMap{}
|
||||
|
||||
mapChange := func(changeMap bluecatChangeMap, change *endpoint.Endpoint) {
|
||||
zone := p.findZone(zones, change.DNSName)
|
||||
if zone == "" {
|
||||
log.Debugf("ignoring changes to '%s' because a suitable Bluecat DNS zone was not found", change.DNSName)
|
||||
return
|
||||
}
|
||||
changeMap[zone] = append(changeMap[zone], change)
|
||||
}
|
||||
|
||||
for _, change := range changes.Delete {
|
||||
mapChange(deleted, change)
|
||||
}
|
||||
for _, change := range changes.UpdateOld {
|
||||
mapChange(deleted, change)
|
||||
}
|
||||
for _, change := range changes.Create {
|
||||
mapChange(created, change)
|
||||
}
|
||||
for _, change := range changes.UpdateNew {
|
||||
mapChange(created, change)
|
||||
}
|
||||
|
||||
return created, deleted
|
||||
}
|
||||
|
||||
// findZone finds the most specific matching zone for a given record 'name' from a list of all zones
|
||||
func (p *BluecatProvider) findZone(zones []string, name string) string {
|
||||
var result string
|
||||
|
||||
for _, zone := range zones {
|
||||
if strings.HasSuffix(name, "."+zone) {
|
||||
if result == "" || len(zone) > len(result) {
|
||||
result = zone
|
||||
}
|
||||
} else if strings.EqualFold(name, zone) {
|
||||
if result == "" || len(zone) > len(result) {
|
||||
result = zone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) zones() ([]string, error) {
|
||||
log.Debugf("retrieving Bluecat zones for configuration: %s, view: %s", p.DNSConfiguration, p.View)
|
||||
var zones []string
|
||||
|
||||
zonelist, err := p.gatewayClient.getBluecatZones(p.RootZone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zonelist {
|
||||
if !p.domainFilter.Match(zone.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: match to absoluteName(string) not Id(int)
|
||||
if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) {
|
||||
continue
|
||||
}
|
||||
|
||||
zoneProps := splitProperties(zone.Properties)
|
||||
|
||||
zones = append(zones, zoneProps["absoluteName"])
|
||||
}
|
||||
log.Debugf("found %d zones", len(zones))
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) createRecords(created bluecatChangeMap) {
|
||||
for zone, endpoints := range created {
|
||||
for _, ep := range endpoints {
|
||||
if p.dryRun {
|
||||
log.Infof("would create %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("creating %s record named '%s' to '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.recordSet(ep, false)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
var response interface{}
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
response, err = p.gatewayClient.createHostRecord(zone, recordSet.obj.(*bluecatCreateHostRecordRequest))
|
||||
case endpoint.RecordTypeCNAME:
|
||||
response, err = p.gatewayClient.createCNAMERecord(zone, recordSet.obj.(*bluecatCreateCNAMERecordRequest))
|
||||
case endpoint.RecordTypeTXT:
|
||||
response, err = p.gatewayClient.createTXTRecord(zone, recordSet.obj.(*bluecatCreateTXTRecordRequest))
|
||||
}
|
||||
log.Debugf("Response from create: %v", response)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to create %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) deleteRecords(deleted bluecatChangeMap) {
|
||||
// run deletions first
|
||||
for zone, endpoints := range deleted {
|
||||
for _, ep := range endpoints {
|
||||
if p.dryRun {
|
||||
log.Infof("would delete %s record named '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
} else {
|
||||
log.Infof("deleting %s record named '%s' for Bluecat DNS zone '%s'.",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.recordSet(ep, true)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to retrieve %s record named '%s' to '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
for _, record := range *recordSet.res.(*[]BluecatHostRecord) {
|
||||
err = p.gatewayClient.deleteHostRecord(record.Name)
|
||||
}
|
||||
case endpoint.RecordTypeCNAME:
|
||||
for _, record := range *recordSet.res.(*[]BluecatCNAMERecord) {
|
||||
err = p.gatewayClient.deleteCNAMERecord(record.Name)
|
||||
}
|
||||
case endpoint.RecordTypeTXT:
|
||||
for _, record := range *recordSet.res.(*[]BluecatTXTRecord) {
|
||||
err = p.gatewayClient.deleteTXTRecord(record.Name)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete %s record named '%s' for Bluecat DNS zone '%s': %v",
|
||||
ep.RecordType,
|
||||
ep.DNSName,
|
||||
zone,
|
||||
err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (recordSet bluecatRecordSet, err error) {
|
||||
switch ep.RecordType {
|
||||
case endpoint.RecordTypeA:
|
||||
var res []BluecatHostRecord
|
||||
// TODO Allow configurable properties/ttl
|
||||
obj := bluecatCreateHostRecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
IP4Address: ep.Targets[0],
|
||||
TTL: 0,
|
||||
Properties: "",
|
||||
}
|
||||
if getObject {
|
||||
var record BluecatHostRecord
|
||||
err = p.gatewayClient.getHostRecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
case endpoint.RecordTypeCNAME:
|
||||
var res []BluecatCNAMERecord
|
||||
obj := bluecatCreateCNAMERecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
LinkedRecord: ep.Targets[0],
|
||||
TTL: 0,
|
||||
Properties: "",
|
||||
}
|
||||
if getObject {
|
||||
var record BluecatCNAMERecord
|
||||
err = p.gatewayClient.getCNAMERecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
case endpoint.RecordTypeTXT:
|
||||
var res []BluecatTXTRecord
|
||||
obj := bluecatCreateTXTRecordRequest{
|
||||
AbsoluteName: ep.DNSName,
|
||||
Text: ep.Targets[0],
|
||||
}
|
||||
if getObject {
|
||||
var record BluecatTXTRecord
|
||||
err = p.gatewayClient.getTXTRecord(ep.DNSName, &record)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = append(res, record)
|
||||
}
|
||||
recordSet = bluecatRecordSet{
|
||||
obj: &obj,
|
||||
res: &res,
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getBluecatGatewayToken retrieves a Bluecat Gateway API token.
|
||||
func getBluecatGatewayToken(cfg bluecatConfig) (string, http.Cookie, error) {
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"username": cfg.GatewayUsername,
|
||||
"password": cfg.GatewayPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config")
|
||||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // ignore self-signed SSL cert check
|
||||
}}
|
||||
|
||||
resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
details, _ := ioutil.ReadAll(resp.Body)
|
||||
return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", resp.StatusCode, string(details))
|
||||
}
|
||||
|
||||
res, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "error reading get_token response from bluecat gateway")
|
||||
}
|
||||
|
||||
resJSON := map[string]string{}
|
||||
err = json.Unmarshal(res, &resJSON)
|
||||
if err != nil {
|
||||
return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway")
|
||||
}
|
||||
|
||||
// Example response: {"access_token": "BAMAuthToken: abc123"}
|
||||
// We only care about the actual token string - i.e. abc123
|
||||
// The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least
|
||||
// for now.
|
||||
return strings.Split(resJSON["access_token"], " ")[1], *resp.Cookies()[0], nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getBluecatZones(zoneName string) ([]BluecatZone, error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
zonePath := expandZone(zoneName)
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error retrieving zone(s) from gateway: %v, %v", url, zoneName)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
zones := []BluecatZone{}
|
||||
json.NewDecoder(resp.Body).Decode(&zones)
|
||||
|
||||
// Bluecat Gateway only returns subzones one level deeper than the provided zone
|
||||
// so this recursion is needed to traverse subzones until none are returned
|
||||
for _, zone := range zones {
|
||||
zoneProps := splitProperties(zone.Properties)
|
||||
subZones, err := c.getBluecatZones(zoneProps["absoluteName"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName)
|
||||
}
|
||||
zones = append(zones, subZones...)
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getHostRecords(zone string, records *[]BluecatHostRecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(records)
|
||||
log.Debugf("Get Host Records Response: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(records)
|
||||
log.Debugf("Get CName Records Response: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getTXTRecords(zone string, records *[]BluecatTXTRecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error building http request")
|
||||
}
|
||||
log.Debugf("Request: %v", req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone)
|
||||
}
|
||||
log.Debugf("Get Txt Records response: %v", resp)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(records)
|
||||
log.Debugf("Get TXT Records Body: %v", records)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getHostRecord(name string, record *BluecatHostRecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"host_records/" + name + "/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error building http request: %v", name)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(record)
|
||||
log.Debugf("Get Host Record Response: %v", record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getCNAMERecord(name string, record *BluecatCNAMERecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"cname_records/" + name + "/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error building http request: %v", name)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(record)
|
||||
log.Debugf("Get CName Record Response: %v", record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) getTXTRecord(name string, record *BluecatTXTRecord) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"text_records/" + name + "/"
|
||||
req, err := c.buildHTTPRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(record)
|
||||
log.Debugf("Get TXT Record Response: %v", record)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
|
||||
body, _ := json.Marshal(req)
|
||||
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error building http request")
|
||||
}
|
||||
hreq.Header.Add("Content-Type", "application/json")
|
||||
res, err = client.Do(hreq)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
|
||||
body, _ := json.Marshal(req)
|
||||
|
||||
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
hreq.Header.Add("Content-Type", "application/json")
|
||||
res, err = client.Do(hreq)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (interface{}, error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
zonePath := expandZone(zone)
|
||||
// Remove the trailing 'zones/'
|
||||
zonePath = strings.TrimSuffix(zonePath, "zones/")
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
|
||||
body, _ := json.Marshal(req)
|
||||
hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hreq.Header.Add("Content-Type", "application/json")
|
||||
res, err := client.Do(hreq)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) deleteHostRecord(name string) (err error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"host_records/" + name + "/"
|
||||
req, err := c.buildHTTPRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error building http request: %v", name)
|
||||
}
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) deleteCNAMERecord(name string) (err error) {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"cname_records/" + name + "/"
|
||||
req, err := c.buildHTTPRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error building http request: %v", name)
|
||||
}
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c GatewayClientConfig) deleteTXTRecord(name string) error {
|
||||
transportCfg := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //ignore self-signed SSL cert check
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transportCfg,
|
||||
}
|
||||
|
||||
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
|
||||
"/views/" + c.View + "/" +
|
||||
"text_records/" + name + "/"
|
||||
|
||||
req, err := c.buildHTTPRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error building http request")
|
||||
}
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway
|
||||
func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Authorization", "Basic "+c.Token)
|
||||
req.AddCookie(&c.Cookie)
|
||||
return req, err
|
||||
}
|
||||
|
||||
//splitProperties is a helper function to break a '|' separated string into key/value pairs
|
||||
// i.e. "foo=bar|baz=mop"
|
||||
func splitProperties(props string) map[string]string {
|
||||
propMap := make(map[string]string)
|
||||
|
||||
// remove trailing | character before we split
|
||||
props = strings.TrimSuffix(props, "|")
|
||||
|
||||
splits := strings.Split(props, "|")
|
||||
for _, pair := range splits {
|
||||
items := strings.Split(pair, "=")
|
||||
propMap[items[0]] = items[1]
|
||||
}
|
||||
|
||||
return propMap
|
||||
}
|
||||
|
||||
//expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway,
|
||||
//such as '/zones/com/zones/example/zones/'
|
||||
func expandZone(zone string) string {
|
||||
ze := "zones/"
|
||||
parts := strings.Split(zone, ".")
|
||||
if len(parts) > 1 {
|
||||
last := len(parts) - 1
|
||||
for i := range parts {
|
||||
ze = ze + parts[last-i] + "/zones/"
|
||||
}
|
||||
} else {
|
||||
ze = ze + zone + "/zones/"
|
||||
}
|
||||
return ze
|
||||
}
|
||||
390
provider/bluecat/bluecat_test.go
Normal file
390
provider/bluecat/bluecat_test.go
Normal file
@ -0,0 +1,390 @@
|
||||
/*
|
||||
Copyright 2020 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 bluecat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
type mockGatewayClient struct {
|
||||
mockBluecatZones *[]BluecatZone
|
||||
mockBluecatHosts *[]BluecatHostRecord
|
||||
mockBluecatCNAMEs *[]BluecatCNAMERecord
|
||||
mockBluecatTXTs *[]BluecatTXTRecord
|
||||
}
|
||||
|
||||
type Changes struct {
|
||||
// Records that need to be created
|
||||
Create []*endpoint.Endpoint
|
||||
// Records that need to be updated (current data)
|
||||
UpdateOld []*endpoint.Endpoint
|
||||
// Records that need to be updated (desired data)
|
||||
UpdateNew []*endpoint.Endpoint
|
||||
// Records that need to be deleted
|
||||
Delete []*endpoint.Endpoint
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) getBluecatZones(zoneName string) ([]BluecatZone, error) {
|
||||
return *g.mockBluecatZones, nil
|
||||
}
|
||||
func (g mockGatewayClient) getHostRecords(zone string, records *[]BluecatHostRecord) error {
|
||||
*records = *g.mockBluecatHosts
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
|
||||
*records = *g.mockBluecatCNAMEs
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) getHostRecord(name string, record *BluecatHostRecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatHosts {
|
||||
if currentRecord.Name == strings.Split(name, ".")[0] {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) getCNAMERecord(name string, record *BluecatCNAMERecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatCNAMEs {
|
||||
if currentRecord.Name == strings.Split(name, ".")[0] {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (g mockGatewayClient) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (g mockGatewayClient) deleteHostRecord(name string) (err error) {
|
||||
*g.mockBluecatHosts = nil
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) deleteCNAMERecord(name string) (err error) {
|
||||
*g.mockBluecatCNAMEs = nil
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) getTXTRecords(zone string, records *[]BluecatTXTRecord) error {
|
||||
*records = *g.mockBluecatTXTs
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) getTXTRecord(name string, record *BluecatTXTRecord) error {
|
||||
for _, currentRecord := range *g.mockBluecatTXTs {
|
||||
if currentRecord.Name == name {
|
||||
*record = currentRecord
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (g mockGatewayClient) deleteTXTRecord(name string) error {
|
||||
*g.mockBluecatTXTs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/users", "http://some.com/api/v1"), nil)
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func createMockBluecatZone(fqdn string) BluecatZone {
|
||||
props := "absoluteName=" + fqdn
|
||||
return BluecatZone{
|
||||
Properties: props,
|
||||
Name: fqdn,
|
||||
ID: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatHostRecord(fqdn, target string) BluecatHostRecord {
|
||||
props := "absoluteName=" + fqdn + "|addresses=" + target + "|"
|
||||
nameParts := strings.Split(fqdn, ".")
|
||||
return BluecatHostRecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
ID: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatCNAME(alias, target string) BluecatCNAMERecord {
|
||||
props := "absoluteName=" + alias + "|linkedRecordName=" + target + "|"
|
||||
nameParts := strings.Split(alias, ".")
|
||||
return BluecatCNAMERecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatTXT(fqdn, txt string) BluecatTXTRecord {
|
||||
return BluecatTXTRecord{
|
||||
Name: fqdn,
|
||||
Text: txt,
|
||||
}
|
||||
}
|
||||
|
||||
func newBluecatProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client GatewayClient) *BluecatProvider {
|
||||
return &BluecatProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
gatewayClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
type bluecatTestData []struct {
|
||||
TestDescription string
|
||||
Endpoints []*endpoint.Endpoint
|
||||
}
|
||||
|
||||
var tests = bluecatTestData{
|
||||
{
|
||||
"first test case", // TODO: better test description
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.122"},
|
||||
},
|
||||
{
|
||||
DNSName: "nginx.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.123"},
|
||||
},
|
||||
{
|
||||
DNSName: "whitespace.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.124"},
|
||||
},
|
||||
{
|
||||
DNSName: "hack.example.com",
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
Targets: endpoint.Targets{"bluecatnetworks.com"},
|
||||
},
|
||||
{
|
||||
DNSName: "abc.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{"hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestBluecatRecords(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122"),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, ti.Endpoints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatApplyChangesCreate(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
err := provider.ApplyChanges(context.Background(), &plan.Changes{Create: ti.Endpoints})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, []*endpoint.Endpoint{})
|
||||
}
|
||||
}
|
||||
func TestBluecatApplyChangesDelete(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122"),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: ti.Endpoints})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := provider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateEndpoints(t, actual, []*endpoint.Endpoint{})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: ensure mapChanges method is tested
|
||||
// TODO: ensure findZone method is tested
|
||||
// TODO: ensure zones method is tested
|
||||
// TODO: ensure createRecords method is tested
|
||||
// TODO: ensure deleteRecords method is tested
|
||||
// TODO: ensure recordSet method is tested
|
||||
|
||||
// TODO: Figure out why recordSet.res is not being set properly
|
||||
func TestBluecatRecordset(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122"),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123"),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
// Test txt records for recordSet function
|
||||
testTxtEndpoint := endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeTXT, "hello")
|
||||
txtObj := bluecatCreateTXTRecordRequest{
|
||||
AbsoluteName: testTxtEndpoint.DNSName,
|
||||
Text: testTxtEndpoint.Targets[0],
|
||||
}
|
||||
txtRecords := []BluecatTXTRecord{
|
||||
createMockBluecatTXT("abc.example.com", "hello"),
|
||||
}
|
||||
expected := bluecatRecordSet{
|
||||
obj: &txtObj,
|
||||
res: &txtRecords,
|
||||
}
|
||||
actual, err := provider.recordSet(testTxtEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, actual.obj, expected.obj)
|
||||
assert.Equal(t, actual.res, expected.res)
|
||||
|
||||
// Test a records for recordSet function
|
||||
testHostEndpoint := endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124")
|
||||
hostObj := bluecatCreateHostRecordRequest{
|
||||
AbsoluteName: testHostEndpoint.DNSName,
|
||||
IP4Address: testHostEndpoint.Targets[0],
|
||||
}
|
||||
hostRecords := []BluecatHostRecord{
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124"),
|
||||
}
|
||||
hostExpected := bluecatRecordSet{
|
||||
obj: &hostObj,
|
||||
res: &hostRecords,
|
||||
}
|
||||
hostActual, err := provider.recordSet(testHostEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, hostActual.obj, hostExpected.obj)
|
||||
assert.Equal(t, hostActual.res, hostExpected.res)
|
||||
|
||||
// Test CName records for recordSet function
|
||||
testCnameEndpoint := endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "bluecatnetworks.com")
|
||||
cnameObj := bluecatCreateCNAMERecordRequest{
|
||||
AbsoluteName: testCnameEndpoint.DNSName,
|
||||
LinkedRecord: testCnameEndpoint.Targets[0],
|
||||
}
|
||||
cnameRecords := []BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com"),
|
||||
}
|
||||
cnameExpected := bluecatRecordSet{
|
||||
obj: &cnameObj,
|
||||
res: &cnameRecords,
|
||||
}
|
||||
cnameActual, err := provider.recordSet(testCnameEndpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, cnameActual.obj, cnameExpected.obj)
|
||||
assert.Equal(t, cnameActual.res, cnameExpected.res)
|
||||
}
|
||||
|
||||
func validateEndpoints(t *testing.T, actual, expected []*endpoint.Endpoint) {
|
||||
assert.True(t, testutils.SameEndpoints(actual, expected), "actual and expected endpoints don't match. %s:%s", actual, expected)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user